Advanced Testing: Fuzzing and Property-Based Testing

Advanced testing techniques for blockchain: fuzzing, property-based testing, and security auditing.

Advanced⏱️ 50 min📚 Prerequisites: 2

Advanced Testing: Fuzzing and Property-Based Testing

Advanced testing techniques are crucial for blockchain security and reliability.

Why Advanced Testing?

Blockchain code handles value - bugs can lead to:

  • Financial Loss: Stolen or locked funds
  • Network Disruption: Consensus failures
  • Reputation Damage: Loss of trust

Traditional unit tests aren't enough. We need:

  • Fuzzing: Random input testing
  • Property-Based Testing: Verify invariants
  • Formal Verification: Mathematical proofs
  • Security Audits: Expert review

Fuzzing

Fuzzing generates random inputs to find edge cases and bugs.

Rust Fuzzing Tools

  • cargo-fuzz: Official Rust fuzzing tool
  • honggfuzz: Fast, efficient fuzzer
  • AFL: American Fuzzy Lop

Example: Fuzzing a Hash Function

RUST
#![no_main]
use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &[u8]| {
    // Test that hash function doesn't panic
    let hash1 = hash(data);
    
    // Test determinism
    let hash2 = hash(data);
    assert_eq!(hash1, hash2);
    
    // Test collision resistance (simplified)
    if data.len() > 0 {
        let mut modified = data.to_vec();
        modified[0] ^= 1;
        let hash3 = hash(&modified);
        assert_ne!(hash1, hash3);
    }
});

Fuzzing Blockchain Components

RUST
// Fuzz transaction validation
fuzz_target!(|tx_data: TransactionData| {
    let mut blockchain = Blockchain::new();
    
    // Should never panic on any input
    let result = blockchain.validate_transaction(&tx_data);
    
    // Result should be consistent
    let result2 = blockchain.validate_transaction(&tx_data);
    assert_eq!(result, result2);
});

// Fuzz block creation
fuzz_target!(|transactions: Vec<Transaction>| {
    let mut blockchain = Blockchain::new();
    
    // Should handle any number/type of transactions
    match blockchain.create_block(transactions) {
        Ok(block) => {
            // Block should be valid
            assert!(blockchain.validate_block(&block).is_ok());
        }
        Err(_) => {
            // If creation fails, validation should also fail
        }
    }
});

Property-Based Testing

Property-based testing verifies that properties (invariants) hold for all inputs.

Using Proptest

RUST
use proptest::prelude::*;

proptest! {
    #[test]
    fn test_balance_invariant(
        transactions in prop::collection::vec(
            (0u64..1000, 0u64..1000, 0u64..100),
            0..100
        )
    ) {
        let mut balances = HashMap::new();
        let mut total = 0u64;
        
        for (from, to, amount) in transactions {
            // Property: total supply never changes
            let before_total: u64 = balances.values().sum();
            
            *balances.entry(from).or_insert(0) -= amount;
            *balances.entry(to).or_insert(0) += amount;
            
            let after_total: u64 = balances.values().sum();
            
            // Total should remain constant (no minting/burning)
            assert_eq!(before_total, after_total);
        }
    }
}

Blockchain Properties

RUST
// Property: Blockchain height always increases
proptest! {
    #[test]
    fn height_monotonic(blocks in prop::collection::vec(block_strategy(), 0..50)) {
        let mut blockchain = Blockchain::new();
        let mut last_height = 0;
        
        for block in blocks {
            if blockchain.add_block(block).is_ok() {
                let height = blockchain.height();
                assert!(height > last_height);
                last_height = height;
            }
        }
    }
}

// Property: Valid blocks are always accepted
proptest! {
    #[test]
    fn valid_blocks_accepted(block in valid_block_strategy()) {
        let mut blockchain = Blockchain::new();
        assert!(blockchain.add_block(block).is_ok());
    }
}

Security Testing

Reentrancy Testing

RUST
#[test]
fn test_reentrancy_protection() {
    let mut contract = VulnerableContract::new();
    
    // Try to reenter during withdrawal
    let attacker = Attacker::new();
    
    // Should fail or be protected
    assert!(contract.withdraw(&attacker).is_err());
}

Integer Overflow Testing

RUST
#[test]
fn test_overflow_protection() {
    let mut token = Token::new();
    
    // Try to cause overflow
    let max_amount = u64::MAX;
    
    // Should use checked arithmetic
    assert!(token.transfer("alice", "bob", max_amount).is_err());
}

Test Coverage

Measuring Coverage

BASH
# Install cargo-tarpaulin
cargo install cargo-tarpaulin

# Generate coverage report
cargo tarpaulin --out Html

Coverage Goals

  • Critical Paths: 100% coverage
  • Error Handling: 90%+ coverage
  • Overall: 80%+ coverage

Best Practices

  1. Start Early: Write tests alongside code
  2. Test Edge Cases: Zero, max, negative values
  3. Test Failures: Ensure errors are handled
  4. Property Tests: Verify invariants
  5. Fuzz Critical Code: Hash functions, validators
  6. Integration Tests: Test full workflows
  7. Performance Tests: Ensure scalability

Real-World Example

RUST
#[cfg(test)]
mod tests {
    use super::*;
    use proptest::prelude::*;
    
    #[test]
    fn test_basic_transfer() {
        // Unit test
    }
    
    proptest! {
        #[test]
        fn test_transfer_properties(
            amount in 0u64..1000000,
            from_balance in amount..10000000
        ) {
            // Property: transfer never creates tokens
            let mut token = Token::new();
            token.balance_of("alice") = from_balance;
            
            let total_before: u64 = token.all_balances().sum();
            token.transfer("alice", "bob", amount).unwrap();
            let total_after: u64 = token.all_balances().sum();
            
            prop_assert_eq!(total_before, total_after);
        }
    }
    
    #[test]
    #[ignore] // Run manually with: cargo test -- --ignored
    fn fuzz_transaction_validation() {
        // Fuzzing test
    }
}

Code Examples

Property-Based Test

Property-based testing for blockchain invariants

RUST
use std::collections::HashMap;

struct Token {
    balances: HashMap<String, u64>,
    total_supply: u64,
}

impl Token {
    fn new(initial_supply: u64) -> Self {
        let mut balances = HashMap::new();
        balances.insert(String::from("owner"), initial_supply);
        Token {
            balances,
            total_supply: initial_supply,
        }
    }
    
    fn transfer(&mut self, from: &str, to: &str, amount: u64) -> Result<(), String> {
        let balance = *self.balances.get(from).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(())
    }
    
    fn total_balance(&self) -> u64 {
        self.balances.values().sum()
    }
}

// Property: Total supply never changes (no minting/burning)
#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_total_supply_invariant() {
        let mut token = Token::new(1000);
        let initial_total = token.total_balance();
        
        // Perform multiple transfers
        token.transfer("owner", "alice", 100).unwrap();
        token.transfer("alice", "bob", 50).unwrap();
        token.transfer("owner", "charlie", 200).unwrap();
        
        // Property: total should remain constant
        assert_eq!(initial_total, token.total_balance());
        assert_eq!(token.total_supply, token.total_balance());
    }
    
    #[test]
    fn test_balance_consistency() {
        let mut token = Token::new(1000);
        
        // Property: sum of all balances equals total supply
        token.transfer("owner", "alice", 300).unwrap();
        token.transfer("owner", "bob", 200).unwrap();
        
        let sum: u64 = token.balances.values().sum();
        assert_eq!(sum, token.total_supply);
    }
    
    #[test]
    fn test_no_negative_balances() {
        let mut token = Token::new(1000);
        
        // Property: balances never go negative
        token.transfer("owner", "alice", 500).unwrap();
        
        // Try to transfer more than balance
        assert!(token.transfer("alice", "bob", 600).is_err());
        
        // Verify balance is still positive
        assert!(token.balances.get("alice").unwrap_or(&0) > &0);
    }
}

fn main() {
    let mut token = Token::new(1000);
    
    println!("Initial total: {}", token.total_balance());
    
    token.transfer("owner", "alice", 100).unwrap();
    token.transfer("alice", "bob", 50).unwrap();
    
    println!("After transfers total: {}", token.total_balance());
    println!("Property: Total supply remains constant!");
}

Explanation:

Property-based tests verify invariants that should always hold. Key properties: total supply never changes, balances sum to total supply, no negative balances. These tests catch bugs that unit tests might miss.

Fuzzing Concept

Conceptual fuzzing test for transaction validation

RUST
struct Transaction {
    from: String,
    to: String,
    amount: u64,
    fee: u64,
}

struct Blockchain {
    balances: std::collections::HashMap<String, u64>,
}

impl Blockchain {
    fn new() -> Self {
        Blockchain {
            balances: std::collections::HashMap::new(),
        }
    }
    
    fn validate_transaction(&self, tx: &Transaction) -> Result<(), String> {
        // Should never panic on any input
        
        if tx.amount == 0 && tx.fee == 0 {
            return Err(String::from("Zero amount transaction"));
        }
        
        // Check for overflow (simplified)
        if tx.amount > u64::MAX / 2 || tx.fee > u64::MAX / 2 {
            return Err(String::from("Amount too large"));
        }
        
        let balance = self.balances.get(&tx.from).copied().unwrap_or(0);
        
        if balance < tx.amount + tx.fee {
            return Err(String::from("Insufficient balance"));
        }
        
        Ok(())
    }
    
    fn execute_transaction(&mut self, tx: &Transaction) -> Result<(), String> {
        self.validate_transaction(tx)?;
        
        *self.balances.entry(tx.from.clone()).or_insert(0) -= tx.amount + tx.fee;
        *self.balances.entry(tx.to.clone()).or_insert(0) += tx.amount;
        
        Ok(())
    }
}

// Simulated fuzzing test
#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn fuzz_transaction_validation() {
        // Simulate fuzzer generating random transactions
        let test_cases = vec![
            Transaction { from: String::new(), to: String::new(), amount: 0, fee: 0 },
            Transaction { from: String::from("a"), to: String::from("b"), amount: u64::MAX, fee: 0 },
            Transaction { from: String::from("a"), to: String::from("b"), amount: 0, fee: u64::MAX },
            Transaction { from: String::from("a"), to: String::from("b"), amount: 100, fee: 10 },
        ];
        
        let blockchain = Blockchain::new();
        
        for tx in test_cases {
            // Should never panic
            let result = blockchain.validate_transaction(&tx);
            
            // Result should be consistent
            let result2 = blockchain.validate_transaction(&tx);
            assert_eq!(result.is_ok(), result2.is_ok());
        }
    }
}

fn main() {
    let mut blockchain = Blockchain::new();
    blockchain.balances.insert(String::from("alice"), 1000);
    
    let tx = Transaction {
        from: String::from("alice"),
        to: String::from("bob"),
        amount: 100,
        fee: 10,
    };
    
    match blockchain.validate_transaction(&tx) {
        Ok(_) => println!("Transaction is valid"),
        Err(e) => println!("Transaction invalid: {}", e),
    }
}

Explanation:

Fuzzing generates random inputs to find edge cases. The validator should never panic and should return consistent results. This catches bugs like integer overflow, underflow, and edge cases that manual testing might miss.

Exercises

Property Test

Create a property test for a simple function!

Medium

Starter Code:

RUST
fn add(a: u64, b: u64) -> u64 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_add_property() {
        // Test that add is commutative: add(a, b) == add(b, a)
    }
}