Rust Blockchain Interview Q&A
Common interview questions and answers for Rust blockchain developers at junior and medior levels.
Rust Blockchain Interview Q&A
This guide covers common interview questions for Rust blockchain developers, organized by difficulty level.
Junior Level Questions
1. Why is Rust used for blockchain development?
Answer:
Rust is ideal for blockchain development because:
- Memory Safety: Prevents common bugs (buffer overflows, use-after-free) that could lead to exploits
- Performance: Zero-cost abstractions provide C/C++ level performance
- Concurrency: Safe parallel processing essential for consensus mechanisms
- No Garbage Collector: Predictable performance for real-time blockchain operations
- Type Safety: Catches errors at compile time, preventing runtime failures
- Deterministic: Same input always produces same output (critical for consensus)
Example:
RUST// Rust's ownership system prevents double-spending bugs fn transfer(balance: &mut u64, amount: u64) -> Result<(), String> { if *balance < amount { return Err(String::from("Insufficient balance")); } *balance -= amount; // Compiler ensures this is safe Ok(()) }
2. What is ownership in Rust?
Answer:
Ownership is Rust's memory management system:
- Each value has one owner
- When owner goes out of scope, value is automatically freed
- No garbage collector needed
- Prevents memory leaks and use-after-free bugs
Example:
RUSTfn main() { let s = String::from("hello"); // s owns the string let s2 = s; // Ownership moves to s2 // println!("{}", s); // ERROR: s no longer owns the value println!("{}", s2); // OK: s2 owns it } // s2 goes out of scope, memory is freed
3. What is the difference between &str and String?
Answer:
&str: String slice, borrowed reference, immutable, fixed sizeString: Owned string, heap-allocated, mutable, growable
Example:
RUSTfn process_string(s: &str) { // Accepts both &str and &String println!("{}", s); } fn main() { let string = String::from("hello"); // Owned let slice = "world"; // &str process_string(&string); // &String coerces to &str process_string(slice); // &str directly }
4. Explain Result<T, E> in Rust.
Answer:
Result is Rust's way of handling errors:
Ok(T): Success case, contains valueErr(E): Error case, contains error- Forces explicit error handling
- No exceptions, all errors are explicit
Example:
RUSTfn divide(a: f64, b: f64) -> Result<f64, String> { if b == 0.0 { Err(String::from("Division by zero")) } else { Ok(a / b) } } fn main() { match divide(10.0, 2.0) { Ok(result) => println!("Result: {}", result), Err(e) => println!("Error: {}", e), } }
5. What is a smart contract?
Answer:
A smart contract is:
- Self-executing code deployed on blockchain
- Automatic execution when conditions are met
- Immutable once deployed (unless upgradeable)
- Transparent - code visible to everyone
- Trustless - no intermediaries needed
Example:
RUSTstruct TokenContract { balances: HashMap<String, u64>, } impl TokenContract { fn transfer(&mut self, from: &str, to: &str, amount: u64) -> Result<(), String> { // Automatic execution when called let balance = self.balances.get(from).copied().unwrap_or(0); if balance < amount { return Err(String::from("Insufficient balance")); } *self.balances.entry(from.to_string()).or_insert(0) -= amount; *self.balances.entry(to.to_string()).or_insert(0) += amount; Ok(()) } }
6. What is WebAssembly (WASM) in blockchain?
Answer:
WASM is a binary format for smart contracts:
- Near-native performance
- Sandboxed execution for security
- Language agnostic (Rust, C++, etc.)
- Deterministic execution
- Used by: Polkadot, NEAR, Cosmos, Ethereum 2.0
7. Explain borrowing in Rust.
Answer:
Borrowing allows using values without taking ownership:
&T: Immutable reference (read-only)&mut T: Mutable reference (can modify)- Rules:
- Multiple immutable references OR one mutable reference
- References must be valid (no dangling pointers)
Example:
RUSTfn calculate_length(s: &String) -> usize { // Borrows, doesn't own s.len() } fn main() { let s = String::from("hello"); let len = calculate_length(&s); // Pass reference println!("{}", s); // s still valid! }
8. What is the difference between Vec and array?
Answer:
- Array
[T; N]: Fixed size, stack-allocated, known at compile time - Vec
Vec<T>: Dynamic size, heap-allocated, can grow/shrink
Example:
RUSTlet array: [i32; 3] = [1, 2, 3]; // Fixed size let mut vec = Vec::new(); // Dynamic vec.push(1); vec.push(2); vec.push(3);
9. What is a struct in Rust?
Answer:
A struct is a custom data type that groups related data:
- Named fields: Each field has a name and type
- Methods: Functions associated with the struct
- Perfect for blockchain: Represent blocks, transactions, accounts
Example:
RUSTstruct Transaction { from: String, to: String, amount: u64, fee: u64, } impl Transaction { fn total_value(&self) -> u64 { self.amount + self.fee } }
10. How do you handle errors in Rust?
Answer:
Rust uses Result and Option for error handling:
Result<T, E>: For recoverable errorsOption<T>: For optional values (Some/None)?operator: Propagate errorsunwrap(): Panic on error (use carefully)match: Pattern matching for error handling
Example:
RUSTfn process_transaction(tx: Transaction) -> Result<(), String> { validate_transaction(&tx)?; // ? propagates error execute_transaction(tx)?; Ok(()) }
Medior Level Questions
1. Explain the difference between Box<T>, Rc<T>, and Arc<T>.
Answer:
Box<T>: Single ownership, heap allocation, no reference countingRc<T>: Reference counted, single-threaded, shared ownershipArc<T>: Atomic reference counted, thread-safe, shared ownership
Use cases:
Box<T>: When you need heap allocation, single ownerRc<T>: Shared ownership in single thread (blockchain state)Arc<T>: Shared ownership across threads (consensus, networking)
Example:
RUSTuse std::sync::Arc; use std::thread; struct BlockchainState { blocks: Vec<Block>, } fn main() { let state = Arc::new(BlockchainState { blocks: Vec::new() }); // Share state across threads let state1 = Arc::clone(&state); thread::spawn(move || { // Use state1 in thread }); }
2. What is reentrancy and how do you prevent it?
Answer:
Reentrancy occurs when a contract calls an external contract that calls back before state is updated.
Prevention:
- Checks-Effects-Interactions pattern: Update state before external calls
- Reentrancy guard: Lock mechanism
- Pull over push: Let users withdraw instead of sending
Example:
RUSTstruct ReentrancyGuard { locked: bool, } impl ReentrancyGuard { fn lock(&mut self) -> Result<(), String> { if self.locked { return Err(String::from("Reentrancy detected")); } self.locked = true; Ok(()) } } // Checks-Effects-Interactions fn withdraw(&mut self, amount: u64) -> Result<(), String> { // CHECK if self.balance < amount { return Err(String::from("Insufficient")); } // EFFECTS: Update state FIRST self.balance -= amount; // INTERACTIONS: External call LAST external_transfer(amount)?; Ok(()) }
3. Explain async/await in Rust for blockchain.
Answer:
Async/await enables non-blocking I/O:
async fn: Returns aFuture.await: Waits for future to complete- Tokio: Popular async runtime
- Use cases: Network I/O, multiple peer connections, transaction processing
Example:
RUSTuse tokio; async fn fetch_block_from_peer(peer: &str, height: u64) -> Result<Block, String> { // Non-blocking network request // ... Ok(block) } #[tokio::main] async fn main() { let block = fetch_block_from_peer("peer1", 100).await?; println!("Block: {:?}", block); }
4. How do you implement a Merkle tree in Rust?
Answer:
A Merkle tree is a binary tree of hashes:
- Leaves: Hash of data
- Nodes: Hash of children
- Root: Single hash representing all data
- Use: Efficient verification of large datasets
Example:
RUSTuse sha2::{Sha256, Digest}; fn hash(data: &[u8]) -> Vec<u8> { let mut hasher = Sha256::new(); hasher.update(data); hasher.finalize().to_vec() } fn merkle_root(transactions: &[Transaction]) -> Vec<u8> { if transactions.is_empty() { return vec![0; 32]; } if transactions.len() == 1 { return hash(&transactions[0].serialize()); } let mut hashes: Vec<Vec<u8>> = transactions .iter() .map(|tx| hash(&tx.serialize())) .collect(); while hashes.len() > 1 { let mut next_level = Vec::new(); for chunk in hashes.chunks(2) { if chunk.len() == 2 { let combined = [chunk[0].as_slice(), chunk[1].as_slice()].concat(); next_level.push(hash(&combined)); } else { next_level.push(chunk[0].clone()); } } hashes = next_level; } hashes[0].clone() }
5. What is the difference between Proof of Work and Proof of Stake?
Answer:
Proof of Work (PoW):
- Miners solve cryptographic puzzles
- Requires computational power (energy intensive)
- First to solve creates block
- Example: Bitcoin
Proof of Stake (PoS):
- Validators stake tokens
- Selected based on stake amount/age
- More energy efficient
- Example: Ethereum 2.0, Polkadot
Rust Implementation (PoS):
RUSTstruct Validator { address: String, stake: u64, staking_time: u64, } fn select_validator(validators: &[Validator]) -> &Validator { // Weighted selection based on stake and time validators.iter() .max_by_key(|v| v.stake * v.staking_time) .unwrap() }
6. How do you optimize gas in smart contracts?
Answer:
Gas optimization techniques:
- Pack structs: Order fields to minimize storage slots
- Use events: Instead of storage for logs
- Batch operations: Combine multiple operations
- Cache storage reads: Read once, use multiple times
- Use libraries: Reusable code
- Minimize loops: Especially with storage operations
Example:
RUST// BAD: Multiple storage reads fn bad_function(&self) -> u64 { self.balance + self.balance + self.balance // 3 reads } // GOOD: Cache the read fn good_function(&self) -> u64 { let balance = self.balance; // 1 read balance + balance + balance }
7. Explain trait objects vs generics.
Answer:
Generics (Static Dispatch):
- Compile-time polymorphism
- Zero runtime cost
- Code duplication (monomorphization)
- Faster execution
Trait Objects (Dynamic Dispatch):
- Runtime polymorphism
- Uses vtable (virtual method table)
- Single code, multiple types
- Slight runtime overhead
Example:
RUST// Generics: Static dispatch fn process<T: Processable>(item: T) { item.process(); // Inlined at compile time } // Trait objects: Dynamic dispatch fn process_dyn(item: Box<dyn Processable>) { item.process(); // Lookup in vtable at runtime }
8. How do you implement a transaction pool?
Answer:
A transaction pool manages pending transactions:
- Add transactions: Validate and store
- Remove transactions: When included in block
- Prioritize: By fee, age, etc.
- Evict: Remove invalid/expired transactions
Example:
RUSTuse std::collections::BTreeMap; struct TransactionPool { transactions: BTreeMap<u64, Transaction>, // Sorted by priority by_sender: HashMap<String, Vec<u64>>, max_size: usize, } impl TransactionPool { fn add(&mut self, tx: Transaction) -> Result<(), String> { if self.transactions.len() >= self.max_size { return Err(String::from("Pool full")); } // Validate self.validate(&tx)?; // Add with priority (fee * age) let priority = tx.fee * tx.age; self.transactions.insert(priority, tx); Ok(()) } fn get_next(&mut self, count: usize) -> Vec<Transaction> { self.transactions.values() .take(count) .cloned() .collect() } }
9. What is a zero-knowledge proof?
Answer:
Zero-knowledge proofs allow proving knowledge without revealing the knowledge:
- Prover: Has secret information
- Verifier: Verifies proof without seeing secret
- Properties: Completeness, soundness, zero-knowledge
- Use cases: Privacy, scalability (zk-rollups)
Types:
- zk-SNARKs: Succinct, non-interactive
- zk-STARKs: Transparent (no trusted setup)
10. How do you test smart contracts?
Answer:
Testing strategies:
- Unit tests: Test individual functions
- Integration tests: Test contract interactions
- Property-based tests: Test invariants
- Fuzz testing: Random inputs
- Formal verification: Mathematical proofs
Example:
RUST#[cfg(test)] mod tests { use super::*; #[test] fn test_transfer() { let mut contract = TokenContract::new(1000); contract.transfer("alice", "bob", 100).unwrap(); assert_eq!(contract.balance_of("bob"), 100); } #[test] fn test_insufficient_balance() { let mut contract = TokenContract::new(100); assert!(contract.transfer("alice", "bob", 200).is_err()); } #[test] fn test_total_supply_invariant() { let mut contract = TokenContract::new(1000); let initial_total = contract.total_supply(); contract.transfer("alice", "bob", 100).unwrap(); // Invariant: Total supply should never change assert_eq!(contract.total_supply(), initial_total); } }
Common Follow-up Questions
"How would you design a DEX (Decentralized Exchange)?"
Answer:
Key components:
- Liquidity Pools: AMM (Automated Market Maker)
- Swap Function: Constant product formula (x * y = k)
- LP Tokens: Represent liquidity provider shares
- Fee Mechanism: Trading fees to LPs
Example:
RUSTstruct AMMPool { token_a: u64, token_b: u64, lp_tokens: u64, } impl AMMPool { fn swap_a_for_b(&mut self, amount_a: u64) -> Result<u64, String> { let k = self.token_a * self.token_b; let new_token_a = self.token_a + amount_a; let new_token_b = k / new_token_a; let amount_b = self.token_b - new_token_b; self.token_a = new_token_a; self.token_b = new_token_b; Ok(amount_b) } }
"Explain how you would implement a lending protocol."
Answer:
Components:
- Liquidity Pools: Users deposit assets
- Interest Rates: Algorithmic based on utilization
- Collateralization: Over-collateralized loans
- Liquidation: Under-collateralized positions
Example:
RUSTstruct LendingPool { total_supplied: u64, total_borrowed: u64, collateral_factor: u64, // e.g., 150% = 150 } impl LendingPool { fn borrow(&mut self, amount: u64, collateral: u64) -> Result<(), String> { // Check collateralization let required_collateral = (amount * self.collateral_factor) / 100; if collateral < required_collateral { return Err(String::from("Insufficient collateral")); } self.total_borrowed += amount; Ok(()) } }
Tips for Interview Success
- Understand the basics: Ownership, borrowing, error handling
- Know blockchain concepts: Smart contracts, consensus, cryptography
- Practice coding: Write code during interview, explain your thinking
- Ask questions: Clarify requirements before coding
- Think about security: Always consider edge cases and attacks
- Explain trade-offs: Discuss performance vs safety, gas optimization
- Show experience: Reference real projects if possible
Resources for Preparation
- Rust Book: Official Rust documentation
- Blockchain Basics: Understand consensus, hashing, Merkle trees
- Smart Contract Security: Common vulnerabilities and patterns
- Practice Problems: LeetCode, HackerRank with Rust
- Open Source: Study real blockchain projects (Polkadot, NEAR)
Summary
Junior Level Focus:
- Rust fundamentals (ownership, borrowing, types)
- Basic blockchain concepts
- Simple smart contract structure
- Error handling
Medior Level Focus:
- Advanced Rust (traits, async, concurrency)
- Security patterns (reentrancy, overflow)
- Performance optimization
- Complex blockchain systems (consensus, networking)
- Architecture and design patterns
Code Examples
Ownership in Interview Context
How to explain ownership during an interview
// Interview question: "Explain ownership"
// Key points to mention:
// 1. Each value has one owner
// 2. Ownership moves when assigned
// 3. Memory automatically freed when owner goes out of scope
// 4. Prevents memory leaks and use-after-free
fn demonstrate_ownership() {
let s1 = String::from("hello"); // s1 owns the string
// Ownership moves to s2
let s2 = s1;
// println!("{}", s1); // ERROR: s1 no longer owns
println!("{}", s2); // OK: s2 owns it
// When s2 goes out of scope, memory is automatically freed
}
fn demonstrate_borrowing() {
let s = String::from("hello");
// Borrow instead of move
let len = calculate_length(&s);
// s still owns the value
println!("Length: {}, String: {}", len, s);
}
fn calculate_length(s: &String) -> usize {
s.len() // Borrows, doesn't take ownership
}
fn main() {
demonstrate_ownership();
demonstrate_borrowing();
}Explanation:
During interviews, explain ownership clearly: it's Rust's way of managing memory without a garbage collector. Show how it prevents common bugs and ensures memory safety.
Reentrancy Prevention Pattern
How to explain and implement reentrancy protection
// Interview question: "How do you prevent reentrancy?"
struct Bank {
balances: std::collections::HashMap<String, u64>,
locked: bool, // Simple reentrancy guard
}
impl Bank {
fn withdraw(&mut self, user: &str, amount: u64) -> Result<(), String> {
// Reentrancy guard
if self.locked {
return Err(String::from("Reentrancy detected"));
}
self.locked = true;
// CHECKS: Validate
let balance = self.balances.get(user).copied().unwrap_or(0);
if balance < amount {
self.locked = false;
return Err(String::from("Insufficient balance"));
}
// EFFECTS: Update state FIRST
*self.balances.entry(user.to_string()).or_insert(0) -= amount;
// Unlock before external call
self.locked = false;
// INTERACTIONS: External call LAST
// external_transfer(user, amount)?;
Ok(())
}
}
fn main() {
let mut bank = Bank {
balances: std::collections::HashMap::new(),
locked: false,
};
bank.balances.insert(String::from("alice"), 1000);
match bank.withdraw("alice", 500) {
Ok(_) => println!("Withdrawal successful"),
Err(e) => println!("Error: {}", e),
}
}Explanation:
Explain the Checks-Effects-Interactions pattern: always update state before making external calls. This prevents reentrancy attacks where external contracts call back before state is updated.
Error Handling Patterns
Common error handling patterns in Rust blockchain development
// Interview question: "How do you handle errors in smart contracts?"
// Pattern 1: Result for recoverable errors
fn transfer(
balance: &mut u64,
amount: u64,
) -> Result<(), String> {
if *balance < amount {
return Err(String::from("Insufficient balance"));
}
*balance -= amount;
Ok(())
}
// Pattern 2: Option for optional values
fn find_transaction(
transactions: &[Transaction],
tx_hash: &str,
) -> Option<&Transaction> {
transactions.iter().find(|tx| tx.hash == tx_hash)
}
// Pattern 3: Error propagation with ?
fn process_transaction(
tx: Transaction,
) -> Result<(), String> {
validate_transaction(&tx)?; // Propagates error
execute_transaction(tx)?; // Propagates error
Ok(())
}
// Pattern 4: Custom error types
#[derive(Debug)]
enum ContractError {
InsufficientBalance,
InvalidAddress,
Unauthorized,
}
fn secure_transfer(
balance: &mut u64,
amount: u64,
) -> Result<(), ContractError> {
if *balance < amount {
return Err(ContractError::InsufficientBalance);
}
*balance -= amount;
Ok(())
}
fn main() {
let mut balance = 1000u64;
// Pattern 1: Result
match transfer(&mut balance, 500) {
Ok(_) => println!("Transfer successful"),
Err(e) => println!("Error: {}", e),
}
// Pattern 4: Custom errors
match secure_transfer(&mut balance, 1000) {
Ok(_) => println!("Transfer successful"),
Err(ContractError::InsufficientBalance) => {
println!("Insufficient balance")
}
Err(e) => println!("Error: {:?}", e),
}
}Explanation:
Show different error handling patterns: Result for recoverable errors, Option for optional values, ? for propagation, and custom error types for better error messages.
Exercises
Explain Ownership
Practice explaining ownership in your own words!
Starter Code:
// Write a function that demonstrates ownership
// Explain what happens at each step
fn main() {
// Your code here
}Implement Reentrancy Protection
Implement a function with reentrancy protection!
Starter Code:
struct Contract {
balance: u64,
// Add reentrancy guard here
}
impl Contract {
fn withdraw(&mut self, amount: u64) -> Result<(), String> {
// Implement checks-effects-interactions pattern
Ok(())
}
}
fn main() {
let mut contract = Contract { balance: 1000 };
contract.withdraw(500).unwrap();
}