Advanced Testing: Fuzzing and Property-Based Testing
Advanced testing techniques for blockchain: fuzzing, property-based testing, and security auditing.
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
RUSTuse 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
- Start Early: Write tests alongside code
- Test Edge Cases: Zero, max, negative values
- Test Failures: Ensure errors are handled
- Property Tests: Verify invariants
- Fuzz Critical Code: Hash functions, validators
- Integration Tests: Test full workflows
- 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
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
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!
Starter Code:
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)
}
}