Rust Blockchain Interview Q&A

Common interview questions and answers for Rust blockchain developers at junior and medior levels.

Intermediate⏱️ 90 min📚 Prerequisites: 2

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:

RUST
fn 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 size
  • String: Owned string, heap-allocated, mutable, growable

Example:

RUST
fn 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 value
  • Err(E): Error case, contains error
  • Forces explicit error handling
  • No exceptions, all errors are explicit

Example:

RUST
fn 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:

RUST
struct 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:

RUST
fn 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:

RUST
let 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:

RUST
struct 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 errors
  • Option<T>: For optional values (Some/None)
  • ? operator: Propagate errors
  • unwrap(): Panic on error (use carefully)
  • match: Pattern matching for error handling

Example:

RUST
fn 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 counting
  • Rc<T>: Reference counted, single-threaded, shared ownership
  • Arc<T>: Atomic reference counted, thread-safe, shared ownership

Use cases:

  • Box<T>: When you need heap allocation, single owner
  • Rc<T>: Shared ownership in single thread (blockchain state)
  • Arc<T>: Shared ownership across threads (consensus, networking)

Example:

RUST
use 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:

RUST
struct 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 a Future
  • .await: Waits for future to complete
  • Tokio: Popular async runtime
  • Use cases: Network I/O, multiple peer connections, transaction processing

Example:

RUST
use 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:

RUST
use 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):

RUST
struct 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:

RUST
use 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:

  1. Liquidity Pools: AMM (Automated Market Maker)
  2. Swap Function: Constant product formula (x * y = k)
  3. LP Tokens: Represent liquidity provider shares
  4. Fee Mechanism: Trading fees to LPs

Example:

RUST
struct 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:

  1. Liquidity Pools: Users deposit assets
  2. Interest Rates: Algorithmic based on utilization
  3. Collateralization: Over-collateralized loans
  4. Liquidation: Under-collateralized positions

Example:

RUST
struct 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

  1. Understand the basics: Ownership, borrowing, error handling
  2. Know blockchain concepts: Smart contracts, consensus, cryptography
  3. Practice coding: Write code during interview, explain your thinking
  4. Ask questions: Clarify requirements before coding
  5. Think about security: Always consider edge cases and attacks
  6. Explain trade-offs: Discuss performance vs safety, gas optimization
  7. 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

RUST
// 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

RUST
// 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

RUST
// 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!

Easy

Starter Code:

RUST
// 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!

Medium

Starter Code:

RUST
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();
}