rabia-banking-example

Crates.iorabia-banking-example
lib.rsrabia-banking-example
version0.4.1
created_at2025-08-22 14:18:24.832124+00
updated_at2025-08-22 14:18:24.832124+00
descriptionBanking ledger state machine implementation example using the Rabia SMR protocol
homepagehttps://github.com/rabia-rs/rabia
repositoryhttps://github.com/rabia-rs/rabia
max_upload_size
id1806404
size57,439
Sergiy Yevtushenko (siy)

documentation

https://docs.rs/rabia

README

Banking SMR Example

This example demonstrates how to build a sophisticated financial ledger using State Machine Replication (SMR) with the Rabia consensus protocol. It showcases complex business logic, validation, and transaction management in a distributed system.

What This Example Shows

The Banking SMR demonstrates advanced SMR concepts for real-world applications:

  1. Complex Business Logic: Account management with validation and business rules
  2. Multi-Entity State: Managing accounts, transactions, and relationships
  3. Atomic Operations: Transfer operations that must succeed or fail completely
  4. Validation and Error Handling: Comprehensive input validation and business rule enforcement
  5. Audit Trail: Complete transaction history for compliance and debugging
  6. Consistent State: Financial invariants maintained across all replicas

State Machine Implementation

The banking system implements these operations:

Account Management

  • CreateAccount { account_id, initial_balance } - Create new account with validation
  • GetAccount { account_id } - Get complete account information
  • ListAccounts - Get all accounts (admin operation)

Financial Operations

  • Deposit { account_id, amount } - Add funds to an account
  • Withdraw { account_id, amount } - Remove funds with balance checking
  • Transfer { from_account, to_account, amount } - Atomic transfer between accounts
  • GetBalance { account_id } - Get current account balance

Reporting and Audit

  • GetTransactionHistory { account_id, limit } - Get transaction history with filtering

Key SMR Features Demonstrated

Complex State Management

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct BankingState {
    /// All accounts indexed by account ID
    pub accounts: HashMap<String, Account>,
    /// Complete transaction history for audit trail
    pub transactions: Vec<Transaction>,
    /// Operation counter for metrics
    pub operation_count: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Account {
    pub account_id: String,
    pub balance: i64,  // Using cents to avoid floating point issues
    pub created_at: u64,
    pub last_transaction_at: u64,
    pub transaction_count: u64,
}

Business Logic Validation

async fn apply_command(&mut self, command: Self::Command) -> Self::Response {
    self.state.operation_count += 1;
    
    match command {
        BankingCommand::Transfer { from_account, to_account, amount } => {
            // Validate transfer amount
            if let Err(e) = Self::validate_amount(amount) {
                return BankingResponse::error(e);
            }
            
            // Prevent self-transfers
            if from_account == to_account {
                return BankingResponse::error("Cannot transfer to same account".to_string());
            }
            
            // Verify both accounts exist
            let from_balance = match self.state.accounts.get(&from_account) {
                Some(account) => account.balance,
                None => return BankingResponse::error("Source account not found".to_string()),
            };
            
            if !self.state.accounts.contains_key(&to_account) {
                return BankingResponse::error("Destination account not found".to_string());
            }
            
            // Check sufficient funds
            if from_balance < amount {
                return BankingResponse::error("Insufficient funds".to_string());
            }
            
            // Atomically update both accounts
            self.state.accounts.get_mut(&from_account).unwrap()
                .update_balance(from_balance - amount);
            self.state.accounts.get_mut(&to_account).unwrap()
                .update_balance(self.state.accounts[&to_account].balance + amount);
            
            // Record transaction for audit trail
            let transaction = Transaction {
                transaction_id: Self::generate_transaction_id(),
                from_account: Some(from_account),
                to_account: Some(to_account),
                amount,
                timestamp: current_timestamp(),
                transaction_type: TransactionType::Transfer,
            };
            self.state.transactions.push(transaction);
            
            BankingResponse::success_with_transaction(None, transaction_id)
        }
        // ... other operations
    }
}

Audit Trail and Compliance

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Transaction {
    pub transaction_id: String,       // Unique identifier  
    pub from_account: Option<String>, // None for deposits
    pub to_account: Option<String>,   // None for withdrawals
    pub amount: i64,                  // Amount in cents
    pub timestamp: u64,               // UTC timestamp
    pub transaction_type: TransactionType,
}

// Every financial operation creates an immutable audit record
BankingCommand::GetTransactionHistory { account_id, limit } => {
    let mut transactions: Vec<Transaction> = if let Some(account_id) = account_id {
        // Filter transactions for specific account
        self.state.transactions.iter()
            .filter(|tx| tx.involves_account(&account_id))
            .cloned().collect()
    } else {
        // Return all transactions
        self.state.transactions.clone()
    };
    
    // Sort by timestamp (newest first) for consistent ordering
    transactions.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
    
    // Apply limit for pagination
    if let Some(limit) = limit {
        transactions.truncate(limit);
    }
    
    BankingResponse::success(Some(BankingData::Transactions(transactions)))
}

Financial Invariants

The banking SMR maintains critical financial invariants:

  1. Conservation of Money: Total balance across all accounts never changes unless money is deposited or withdrawn
  2. Non-negative Balances: Accounts cannot have negative balances (no overdrafts)
  3. Atomic Transfers: Transfers either complete fully or fail completely
  4. Audit Trail: Every balance change is recorded in the transaction history

Running the Example

# Run the banking SMR example
cargo run --bin banking_smr_example

# Run with multiple replicas to see consensus
cargo run --bin banking_smr_cluster  

# Run comprehensive tests
cargo test -p banking_smr

# Run stress tests with concurrent operations
cargo test -p banking_smr -- --test-threads=1 test_concurrent_operations

Use Cases

This pattern is ideal for:

  • Financial Systems: Banks, payment processors, digital wallets
  • Trading Platforms: Order books, settlement systems, clearing houses
  • Accounting Systems: General ledgers, bookkeeping, financial reporting
  • Resource Management: Credits, quotas, resource allocation
  • Gaming Economies: Virtual currencies, item trading, player balances
  • Supply Chain: Inventory tracking, asset transfers, ownership records

Advanced Features

Concurrent Operations with Consistency

// All replicas process these operations in the same order
let batch_operations = vec![
    BankingCommand::Transfer { 
        from_account: "alice".to_string(), 
        to_account: "bob".to_string(), 
        amount: 100 
    },
    BankingCommand::Transfer { 
        from_account: "bob".to_string(), 
        to_account: "charlie".to_string(), 
        amount: 50 
    },
    BankingCommand::Deposit { 
        account_id: "alice".to_string(), 
        amount: 200 
    },
];

// All operations applied atomically in order across all replicas
let results = banking_smr.apply_commands(batch_operations).await;

Financial Reporting and Analytics

// The banking state provides rich analytics
impl BankingSMR {
    pub fn total_value(&self) -> i64 {
        self.state.accounts.values().map(|a| a.balance).sum()
    }
    
    pub fn account_count(&self) -> usize {
        self.state.accounts.len()
    }
    
    pub fn transaction_volume(&self) -> i64 {
        self.state.transactions.iter().map(|t| t.amount).sum()
    }
    
    pub fn most_active_accounts(&self) -> Vec<(String, u64)> {
        let mut accounts: Vec<_> = self.state.accounts.iter()
            .map(|(id, account)| (id.clone(), account.transaction_count))
            .collect();
        accounts.sort_by(|a, b| b.1.cmp(&a.1));
        accounts
    }
}

Compliance and Monitoring

// Transaction monitoring for compliance
impl Transaction {
    pub fn is_large_transaction(&self) -> bool {
        self.amount > 1_000_000 // $10,000+ requires reporting
    }
    
    pub fn involves_account(&self, account_id: &str) -> bool {
        self.from_account.as_ref() == Some(account_id) || 
        self.to_account.as_ref() == Some(account_id)
    }
    
    pub fn transaction_age_days(&self) -> u64 {
        let now = current_timestamp();
        (now - self.timestamp) / (24 * 60 * 60 * 1000)
    }
}

Performance Considerations

Memory Management

  • Account Storage: Efficient HashMap storage for fast account lookups
  • Transaction History: Consider archiving old transactions for large systems
  • State Snapshots: Compressed serialization for efficient persistence

Throughput Optimization

  • Batch Processing: Group multiple operations for higher throughput
  • Read Replicas: Balance queries can be served from any replica
  • Operation Ordering: Independent operations can be processed concurrently

Consistency Guarantees

  • Strong Consistency: All replicas see the same account balances
  • Linearizable Operations: Operations appear to execute atomically
  • Durable Transactions: All committed transactions survive node failures

Security Considerations

Input Validation

fn validate_amount(amount: i64) -> Result<(), String> {
    if amount <= 0 {
        return Err("Amount must be positive".to_string());
    }
    if amount > 1_000_000_000 { // $10M limit
        return Err("Amount exceeds maximum limit".to_string());
    }
    Ok(())
}

fn validate_account_id(account_id: &str) -> Result<(), String> {
    if account_id.is_empty() {
        return Err("Account ID cannot be empty".to_string());
    }
    if account_id.len() > 50 {
        return Err("Account ID too long".to_string());
    }
    if !account_id.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-') {
        return Err("Invalid characters in account ID".to_string());
    }
    Ok(())
}

Audit Requirements

  • Immutable History: Transaction records are never modified or deleted
  • Complete Trail: Every balance change is recorded with timestamps
  • Deterministic IDs: Transaction IDs are generated deterministically for consistency

Implementation Notes

Why This Works Well for SMR

  1. Clear State Model: Accounts and transactions have well-defined relationships
  2. Deterministic Operations: All operations produce predictable results
  3. Strong Invariants: Financial rules are enforced consistently
  4. Atomic Operations: Complex operations (transfers) succeed or fail completely
  5. Rich Audit Trail: Complete history enables debugging and compliance

SMR Considerations

  1. State Size: Large transaction histories require careful management
  2. Snapshot Strategy: Balance between recovery time and storage overhead
  3. Validation Logic: All business rules must be deterministic across replicas
  4. Error Handling: Failed operations must not corrupt the state machine

Testing Strategy

Unit Tests

  • Individual operation correctness
  • Edge case handling (insufficient funds, invalid accounts)
  • State serialization/deserialization

Integration Tests

  • Multi-operation scenarios
  • Concurrent transfer testing
  • State consistency verification

Stress Tests

  • High-volume transaction processing
  • Memory usage under load
  • Performance benchmarking

Next Steps

After understanding the banking example, explore:

This banking example demonstrates how SMR can provide the strong consistency and fault tolerance required for financial applications while maintaining high performance and scalability.

Commit count: 31

cargo fmt