//! ZK-Rollup manager for batch processing and proof coordination. //! //! This module orchestrates the entire rollup workflow: //! 1. Collect transactions into batches //! 2. Apply state transitions //! 3. Generate validity proofs //! 4. Submit to L1 bridge //! //! # Architecture //! //! ```text //! User Transactions //! │ //! ▼ //! ┌──────────────┐ ┌──────────────┐ //! │ Transaction │────▶│ Mempool │ //! │ Validator │ │ (pending) │ //! └──────────────┘ └──────────────┘ //! │ //! ▼ (batch full or timeout) //! ┌──────────────┐ //! │ Batch Builder│ //! └──────────────┘ //! │ //! ┌─────────────────┼─────────────────┐ //! ▼ ▼ ▼ //! ┌──────────┐ ┌──────────┐ ┌──────────┐ //! │ State │ │ Circuit │ │ Proof │ //! │ Update │ │ Builder │ │ Generator│ //! └──────────┘ └──────────┘ └──────────┘ //! │ │ │ //! └─────────────────┴─────────────────┘ //! │ //! ▼ //! ┌──────────────┐ //! │ L1 Submitter │ //! └──────────────┘ //! ``` use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use std::collections::VecDeque; use std::time::{Duration, Instant}; use thiserror::Error; use crate::circuit::BatchCircuit; use crate::proof::{Proof, ProofError, ProofSystem, ProvingKey, VerificationKey}; use crate::state::{AccountState, StateError, StateRoot, StateTree}; /// Rollup configuration. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RollupConfig { /// Maximum transactions per batch pub max_batch_size: usize, /// Minimum transactions before generating proof pub min_batch_size: usize, /// Maximum time to wait for batch fill pub batch_timeout: Duration, /// State tree depth pub tree_depth: usize, /// L1 bridge address pub bridge_address: Option, } impl Default for RollupConfig { fn default() -> Self { Self { max_batch_size: crate::constants::MAX_BATCH_SIZE, min_batch_size: crate::constants::MIN_BATCH_SIZE, batch_timeout: Duration::from_secs(60), tree_depth: crate::constants::STATE_TREE_DEPTH, bridge_address: None, } } } /// Transaction types for the rollup. #[derive(Clone, Debug, Serialize, Deserialize)] pub enum BatchTransaction { /// Transfer between L2 accounts Transfer { from: u64, to: u64, amount: u128, nonce: u64, signature: Vec, }, /// Deposit from L1 to L2 Deposit { to: u64, amount: u128, l1_tx_hash: [u8; 32], }, /// Withdrawal from L2 to L1 Withdraw { from: u64, l1_recipient: [u8; 20], amount: u128, nonce: u64, signature: Vec, }, } impl BatchTransaction { /// Creates a transfer transaction. pub fn transfer(from: u64, to: u64, amount: u128) -> Self { Self::Transfer { from, to, amount, nonce: 0, signature: Vec::new(), } } /// Creates a deposit transaction. pub fn deposit(to: u64, amount: u128, l1_tx_hash: [u8; 32]) -> Self { Self::Deposit { to, amount, l1_tx_hash, } } /// Creates a withdrawal transaction. pub fn withdraw(from: u64, l1_recipient: [u8; 20], amount: u128) -> Self { Self::Withdraw { from, l1_recipient, amount, nonce: 0, signature: Vec::new(), } } /// Sets the signature for this transaction. pub fn with_signature(mut self, signature: Vec) -> Self { match &mut self { Self::Transfer { signature: sig, .. } => *sig = signature, Self::Withdraw { signature: sig, .. } => *sig = signature, Self::Deposit { .. } => {} // Deposits don't need L2 signatures } self } /// Sets the nonce for this transaction. pub fn with_nonce(mut self, nonce: u64) -> Self { match &mut self { Self::Transfer { nonce: n, .. } => *n = nonce, Self::Withdraw { nonce: n, .. } => *n = nonce, Self::Deposit { .. } => {} } self } /// Returns the hash of this transaction. pub fn hash(&self) -> [u8; 32] { let mut hasher = blake3::Hasher::new(); match self { Self::Transfer { from, to, amount, nonce, .. } => { hasher.update(b"transfer"); hasher.update(&from.to_le_bytes()); hasher.update(&to.to_le_bytes()); hasher.update(&amount.to_le_bytes()); hasher.update(&nonce.to_le_bytes()); } Self::Deposit { to, amount, l1_tx_hash, } => { hasher.update(b"deposit"); hasher.update(&to.to_le_bytes()); hasher.update(&amount.to_le_bytes()); hasher.update(l1_tx_hash); } Self::Withdraw { from, l1_recipient, amount, nonce, .. } => { hasher.update(b"withdraw"); hasher.update(&from.to_le_bytes()); hasher.update(l1_recipient); hasher.update(&amount.to_le_bytes()); hasher.update(&nonce.to_le_bytes()); } } *hasher.finalize().as_bytes() } } /// A batch of transactions with proof. #[derive(Clone, Debug)] pub struct RollupBatch { /// Batch number pub batch_number: u64, /// Transactions in this batch pub transactions: Vec, /// State root before batch pub pre_state_root: StateRoot, /// State root after batch pub post_state_root: StateRoot, /// Validity proof pub proof: Option, /// Batch hash (for L1 submission) pub batch_hash: [u8; 32], /// Timestamp pub timestamp: u64, } impl RollupBatch { /// Creates a new batch. pub fn new(batch_number: u64, pre_state_root: StateRoot) -> Self { Self { batch_number, transactions: Vec::new(), pre_state_root, post_state_root: pre_state_root, proof: None, batch_hash: [0u8; 32], timestamp: std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs(), } } /// Computes the batch hash. pub fn compute_hash(&mut self) { let mut hasher = blake3::Hasher::new(); hasher.update(&self.batch_number.to_le_bytes()); hasher.update(&self.pre_state_root.0); hasher.update(&self.post_state_root.0); hasher.update(&(self.transactions.len() as u64).to_le_bytes()); for tx in &self.transactions { hasher.update(&tx.hash()); } self.batch_hash = *hasher.finalize().as_bytes(); } /// Returns the transaction count. pub fn tx_count(&self) -> usize { self.transactions.len() } /// Returns whether the batch has a proof. pub fn is_proven(&self) -> bool { self.proof.is_some() } } /// Rollup manager state. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum RollupState { /// Accepting transactions Accepting, /// Building batch, not accepting new transactions Building, /// Generating proof Proving, /// Ready for L1 submission Ready, /// Paused (e.g., L1 congestion) Paused, } /// Rollup manager for coordinating batches and proofs. pub struct RollupManager { config: RollupConfig, state_tree: StateTree, proof_system: ProofSystem, proving_key: Option, verification_key: Option, pending_txs: RwLock>, current_batch: RwLock>, committed_batches: RwLock>, next_batch_number: RwLock, state: RwLock, batch_start_time: RwLock>, } impl RollupManager { /// Creates a new rollup manager. pub fn new() -> Self { let config = RollupConfig::default(); let state_tree = StateTree::new(config.tree_depth); Self { config, state_tree, proof_system: ProofSystem::groth16(), proving_key: None, verification_key: None, pending_txs: RwLock::new(VecDeque::new()), current_batch: RwLock::new(None), committed_batches: RwLock::new(Vec::new()), next_batch_number: RwLock::new(0), state: RwLock::new(RollupState::Accepting), batch_start_time: RwLock::new(None), } } /// Creates a rollup manager with custom config. pub fn with_config(config: RollupConfig) -> Self { let state_tree = StateTree::new(config.tree_depth); Self { config, state_tree, proof_system: ProofSystem::groth16(), proving_key: None, verification_key: None, pending_txs: RwLock::new(VecDeque::new()), current_batch: RwLock::new(None), committed_batches: RwLock::new(Vec::new()), next_batch_number: RwLock::new(0), state: RwLock::new(RollupState::Accepting), batch_start_time: RwLock::new(None), } } /// Sets up the proving/verification keys. pub fn setup(&mut self) -> Result<(), RollupError> { let circuit = BatchCircuit::new(StateRoot::zero(), StateRoot::zero()); let (pk, vk) = self.proof_system.setup(&circuit)?; self.proving_key = Some(pk); self.verification_key = Some(vk); Ok(()) } /// Returns the current rollup state. pub fn state(&self) -> RollupState { *self.state.read() } /// Returns the current state root. pub fn state_root(&self) -> StateRoot { self.state_tree.root() } /// Returns the config. pub fn config(&self) -> &RollupConfig { &self.config } /// Returns the verification key. pub fn verification_key(&self) -> Option<&VerificationKey> { self.verification_key.as_ref() } /// Returns the number of pending transactions. pub fn pending_count(&self) -> usize { self.pending_txs.read().len() } /// Returns the number of committed batches. pub fn batch_count(&self) -> usize { self.committed_batches.read().len() } /// Adds a transaction to the pending pool. pub fn add_transaction(&self, tx: BatchTransaction) -> Result<(), RollupError> { let state = *self.state.read(); if state != RollupState::Accepting { return Err(RollupError::NotAccepting); } // Validate transaction self.validate_transaction(&tx)?; // Add to pending self.pending_txs.write().push_back(tx); // Start batch timer if this is the first tx let mut batch_start = self.batch_start_time.write(); if batch_start.is_none() { *batch_start = Some(Instant::now()); } // Check if we should start building if self.pending_txs.read().len() >= self.config.max_batch_size { drop(batch_start); self.start_building()?; } Ok(()) } /// Validates a transaction before adding to pool. fn validate_transaction(&self, tx: &BatchTransaction) -> Result<(), RollupError> { match tx { BatchTransaction::Transfer { from, amount, .. } => { let account = self .state_tree .get_account(*from) .ok_or(RollupError::AccountNotFound(*from))?; if account.balance < *amount { return Err(RollupError::InsufficientBalance); } } BatchTransaction::Withdraw { from, amount, .. } => { let account = self .state_tree .get_account(*from) .ok_or(RollupError::AccountNotFound(*from))?; if account.balance < *amount { return Err(RollupError::InsufficientBalance); } } BatchTransaction::Deposit { .. } => { // Deposits are validated against L1 } } Ok(()) } /// Checks if batch should be finalized due to timeout. pub fn check_timeout(&self) -> bool { let batch_start = self.batch_start_time.read(); if let Some(start) = *batch_start { let pending = self.pending_txs.read().len(); if pending >= self.config.min_batch_size && start.elapsed() >= self.config.batch_timeout { return true; } } false } /// Starts building a batch from pending transactions. fn start_building(&self) -> Result<(), RollupError> { *self.state.write() = RollupState::Building; let batch_number = *self.next_batch_number.read(); let pre_state_root = self.state_tree.root(); let mut batch = RollupBatch::new(batch_number, pre_state_root); // Move transactions from pending to batch let mut pending = self.pending_txs.write(); while batch.transactions.len() < self.config.max_batch_size { if let Some(tx) = pending.pop_front() { batch.transactions.push(tx); } else { break; } } *self.current_batch.write() = Some(batch); *self.batch_start_time.write() = None; Ok(()) } /// Finalizes the current batch and generates a proof. pub fn finalize_batch(&self) -> Result { // If still accepting, start building if *self.state.read() == RollupState::Accepting { self.start_building()?; } *self.state.write() = RollupState::Proving; let mut batch = self .current_batch .write() .take() .ok_or(RollupError::NoBatch)?; // Apply transactions to state tree for tx in &batch.transactions { match tx { BatchTransaction::Transfer { from, to, amount, .. } => { self.state_tree.apply_transfer(*from, *to, *amount)?; } BatchTransaction::Deposit { to, amount, .. } => { self.state_tree.apply_deposit(*to, *amount)?; } BatchTransaction::Withdraw { from, amount, .. } => { self.state_tree.apply_withdrawal(*from, *amount)?; } } } batch.post_state_root = self.state_tree.root(); batch.compute_hash(); // Generate proof if let Some(pk) = &self.proving_key { let circuit = BatchCircuit::new(batch.pre_state_root, batch.post_state_root); let proof = self.proof_system.prove(&circuit, pk)?; batch.proof = Some(proof); } // Commit batch self.committed_batches.write().push(batch.clone()); *self.next_batch_number.write() += 1; // Ready for submission *self.state.write() = RollupState::Ready; Ok(batch) } /// Verifies a batch proof. pub fn verify_batch(&self, batch: &RollupBatch) -> Result { let proof = batch.proof.as_ref().ok_or(RollupError::NoProof)?; let vk = self .verification_key .as_ref() .ok_or(RollupError::NoVerificationKey)?; Ok(self.proof_system.verify(proof, vk)?) } /// Registers a new account. pub fn register_account( &self, index: u64, pubkey_hash: [u8; 32], ) -> Result { let state = AccountState::new(pubkey_hash); Ok(self.state_tree.set_account(index, state)?) } /// Gets an account by index. pub fn get_account(&self, index: u64) -> Option { self.state_tree.get_account(index) } /// Returns to accepting state after batch submission. pub fn resume(&self) { *self.state.write() = RollupState::Accepting; } /// Pauses the rollup. pub fn pause(&self) { *self.state.write() = RollupState::Paused; } } impl Default for RollupManager { fn default() -> Self { Self::new() } } impl std::fmt::Debug for RollupManager { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("RollupManager") .field("state", &self.state()) .field("pending_txs", &self.pending_count()) .field("batch_count", &self.batch_count()) .field("state_root", &self.state_root().to_hex()) .finish() } } /// Rollup errors. #[derive(Debug, Error)] pub enum RollupError { #[error("Rollup is not accepting transactions")] NotAccepting, #[error("No batch to finalize")] NoBatch, #[error("No proof available")] NoProof, #[error("No verification key available")] NoVerificationKey, #[error("Account not found: {0}")] AccountNotFound(u64), #[error("Insufficient balance")] InsufficientBalance, #[error("Invalid transaction: {0}")] InvalidTransaction(String), #[error("Proof error: {0}")] ProofError(#[from] ProofError), #[error("State error: {0}")] StateError(#[from] StateError), } #[cfg(test)] mod tests { use super::*; // ============================================================================ // RollupConfig Tests // ============================================================================ #[test] fn test_rollup_config_default() { let config = RollupConfig::default(); assert_eq!(config.max_batch_size, crate::constants::MAX_BATCH_SIZE); assert_eq!(config.min_batch_size, crate::constants::MIN_BATCH_SIZE); assert_eq!(config.tree_depth, crate::constants::STATE_TREE_DEPTH); assert!(config.bridge_address.is_none()); } #[test] fn test_rollup_config_batch_size_order() { let config = RollupConfig::default(); assert!(config.min_batch_size < config.max_batch_size); } #[test] fn test_rollup_config_custom() { let config = RollupConfig { max_batch_size: 500, min_batch_size: 5, batch_timeout: Duration::from_secs(30), tree_depth: 16, bridge_address: Some("0x1234...".to_string()), }; assert_eq!(config.max_batch_size, 500); assert_eq!(config.min_batch_size, 5); assert_eq!(config.batch_timeout, Duration::from_secs(30)); assert_eq!(config.tree_depth, 16); assert!(config.bridge_address.is_some()); } #[test] fn test_rollup_config_clone() { let config = RollupConfig::default(); let cloned = config.clone(); assert_eq!(config.max_batch_size, cloned.max_batch_size); assert_eq!(config.min_batch_size, cloned.min_batch_size); } #[test] fn test_rollup_config_debug() { let config = RollupConfig::default(); let debug_str = format!("{:?}", config); assert!(debug_str.contains("RollupConfig")); assert!(debug_str.contains("max_batch_size")); } // ============================================================================ // BatchTransaction Tests // ============================================================================ #[test] fn test_batch_transaction_transfer() { let tx = BatchTransaction::transfer(0, 1, 100); match tx { BatchTransaction::Transfer { from, to, amount, nonce, signature, } => { assert_eq!(from, 0); assert_eq!(to, 1); assert_eq!(amount, 100); assert_eq!(nonce, 0); assert!(signature.is_empty()); } _ => panic!("Expected Transfer variant"), } } #[test] fn test_batch_transaction_deposit() { let l1_tx_hash = [0xab; 32]; let tx = BatchTransaction::deposit(5, 1000, l1_tx_hash); match tx { BatchTransaction::Deposit { to, amount, l1_tx_hash: hash, } => { assert_eq!(to, 5); assert_eq!(amount, 1000); assert_eq!(hash, l1_tx_hash); } _ => panic!("Expected Deposit variant"), } } #[test] fn test_batch_transaction_withdraw() { let l1_recipient = [0xcc; 20]; let tx = BatchTransaction::withdraw(10, l1_recipient, 500); match tx { BatchTransaction::Withdraw { from, l1_recipient: recipient, amount, nonce, signature, } => { assert_eq!(from, 10); assert_eq!(recipient, l1_recipient); assert_eq!(amount, 500); assert_eq!(nonce, 0); assert!(signature.is_empty()); } _ => panic!("Expected Withdraw variant"), } } #[test] fn test_batch_transaction_with_signature_transfer() { let sig = vec![1, 2, 3, 4, 5]; let tx = BatchTransaction::transfer(0, 1, 100).with_signature(sig.clone()); match tx { BatchTransaction::Transfer { signature, .. } => { assert_eq!(signature, sig); } _ => panic!("Expected Transfer"), } } #[test] fn test_batch_transaction_with_signature_withdraw() { let sig = vec![0xaa, 0xbb]; let tx = BatchTransaction::withdraw(0, [0u8; 20], 100).with_signature(sig.clone()); match tx { BatchTransaction::Withdraw { signature, .. } => { assert_eq!(signature, sig); } _ => panic!("Expected Withdraw"), } } #[test] fn test_batch_transaction_with_nonce_transfer() { let tx = BatchTransaction::transfer(0, 1, 100).with_nonce(42); match tx { BatchTransaction::Transfer { nonce, .. } => { assert_eq!(nonce, 42); } _ => panic!("Expected Transfer"), } } #[test] fn test_batch_transaction_with_nonce_withdraw() { let tx = BatchTransaction::withdraw(0, [0u8; 20], 100).with_nonce(99); match tx { BatchTransaction::Withdraw { nonce, .. } => { assert_eq!(nonce, 99); } _ => panic!("Expected Withdraw"), } } #[test] fn test_batch_transaction_hash_transfer() { let tx = BatchTransaction::transfer(0, 1, 100); let hash = tx.hash(); assert_ne!(hash, [0u8; 32]); } #[test] fn test_batch_transaction_hash_deposit() { let tx = BatchTransaction::deposit(0, 100, [0xab; 32]); let hash = tx.hash(); assert_ne!(hash, [0u8; 32]); } #[test] fn test_batch_transaction_hash_withdraw() { let tx = BatchTransaction::withdraw(0, [0xcc; 20], 100); let hash = tx.hash(); assert_ne!(hash, [0u8; 32]); } #[test] fn test_batch_transaction_hash_consistency() { let tx = BatchTransaction::transfer(0, 1, 100); let hash1 = tx.hash(); let hash2 = tx.hash(); assert_eq!(hash1, hash2); } #[test] fn test_batch_transaction_hash_different_amounts() { let tx1 = BatchTransaction::transfer(0, 1, 100); let tx2 = BatchTransaction::transfer(0, 1, 200); assert_ne!(tx1.hash(), tx2.hash()); } #[test] fn test_batch_transaction_hash_different_from_to() { let tx1 = BatchTransaction::transfer(0, 1, 100); let tx2 = BatchTransaction::transfer(1, 0, 100); assert_ne!(tx1.hash(), tx2.hash()); } #[test] fn test_batch_transaction_builder_chain() { let tx = BatchTransaction::transfer(0, 1, 100) .with_nonce(5) .with_signature(vec![1, 2, 3]); match tx { BatchTransaction::Transfer { nonce, signature, .. } => { assert_eq!(nonce, 5); assert_eq!(signature, vec![1, 2, 3]); } _ => panic!("Expected Transfer"), } } #[test] fn test_batch_transaction_clone() { let tx = BatchTransaction::transfer(0, 1, 100); let cloned = tx.clone(); assert_eq!(tx.hash(), cloned.hash()); } #[test] fn test_batch_transaction_large_amount() { let tx = BatchTransaction::transfer(0, 1, u128::MAX); let hash = tx.hash(); assert_ne!(hash, [0u8; 32]); } // ============================================================================ // RollupBatch Tests // ============================================================================ #[test] fn test_rollup_batch_new() { let batch = RollupBatch::new(0, StateRoot::zero()); assert_eq!(batch.batch_number, 0); assert!(batch.transactions.is_empty()); assert_eq!(batch.pre_state_root, StateRoot::zero()); assert_eq!(batch.post_state_root, StateRoot::zero()); assert!(batch.proof.is_none()); assert_eq!(batch.batch_hash, [0u8; 32]); assert!(batch.timestamp > 0); } #[test] fn test_rollup_batch_with_transactions() { let mut batch = RollupBatch::new(1, StateRoot([1u8; 32])); batch .transactions .push(BatchTransaction::transfer(0, 1, 100)); batch .transactions .push(BatchTransaction::transfer(1, 2, 50)); assert_eq!(batch.tx_count(), 2); } #[test] fn test_rollup_batch_compute_hash() { let mut batch = RollupBatch::new(0, StateRoot::zero()); batch .transactions .push(BatchTransaction::transfer(0, 1, 100)); batch.post_state_root = StateRoot([1u8; 32]); batch.compute_hash(); assert_ne!(batch.batch_hash, [0u8; 32]); } #[test] fn test_rollup_batch_hash_changes_with_transactions() { let mut batch1 = RollupBatch::new(0, StateRoot::zero()); batch1.compute_hash(); let hash1 = batch1.batch_hash; let mut batch2 = RollupBatch::new(0, StateRoot::zero()); batch2 .transactions .push(BatchTransaction::transfer(0, 1, 100)); batch2.compute_hash(); let hash2 = batch2.batch_hash; assert_ne!(hash1, hash2); } #[test] fn test_rollup_batch_tx_count() { let mut batch = RollupBatch::new(0, StateRoot::zero()); for i in 0..10 { assert_eq!(batch.tx_count(), i); batch .transactions .push(BatchTransaction::transfer(0, 1, 100)); } assert_eq!(batch.tx_count(), 10); } #[test] fn test_rollup_batch_is_proven() { let mut batch = RollupBatch::new(0, StateRoot::zero()); assert!(!batch.is_proven()); use crate::proof::{Proof, ProofSystemBackend}; batch.proof = Some(Proof { backend: ProofSystemBackend::Groth16, data: vec![1, 2, 3], public_inputs: vec![], }); assert!(batch.is_proven()); } #[test] fn test_rollup_batch_clone() { let mut batch = RollupBatch::new(5, StateRoot([0xaa; 32])); batch .transactions .push(BatchTransaction::transfer(0, 1, 100)); batch.post_state_root = StateRoot([0xbb; 32]); batch.compute_hash(); let cloned = batch.clone(); assert_eq!(batch.batch_number, cloned.batch_number); assert_eq!(batch.pre_state_root, cloned.pre_state_root); assert_eq!(batch.post_state_root, cloned.post_state_root); assert_eq!(batch.batch_hash, cloned.batch_hash); assert_eq!(batch.tx_count(), cloned.tx_count()); } #[test] fn test_rollup_batch_debug() { let batch = RollupBatch::new(0, StateRoot::zero()); let debug_str = format!("{:?}", batch); assert!(debug_str.contains("RollupBatch")); assert!(debug_str.contains("batch_number")); } // ============================================================================ // RollupState Tests // ============================================================================ #[test] fn test_rollup_state_variants() { assert_eq!(RollupState::Accepting, RollupState::Accepting); assert_ne!(RollupState::Accepting, RollupState::Building); assert_ne!(RollupState::Building, RollupState::Proving); assert_ne!(RollupState::Proving, RollupState::Ready); assert_ne!(RollupState::Ready, RollupState::Paused); } #[test] fn test_rollup_state_clone() { let state = RollupState::Accepting; let cloned = state; assert_eq!(state, cloned); } #[test] fn test_rollup_state_debug() { let state = RollupState::Proving; let debug_str = format!("{:?}", state); assert!(debug_str.contains("Proving")); } // ============================================================================ // RollupManager Tests // ============================================================================ #[test] fn test_rollup_manager_creation() { let manager = RollupManager::new(); assert_eq!(manager.state(), RollupState::Accepting); assert_eq!(manager.pending_count(), 0); assert_eq!(manager.batch_count(), 0); } #[test] fn test_rollup_manager_with_config() { let config = RollupConfig { max_batch_size: 100, min_batch_size: 2, batch_timeout: Duration::from_secs(10), tree_depth: 16, bridge_address: None, }; let manager = RollupManager::with_config(config.clone()); assert_eq!(manager.config().max_batch_size, 100); assert_eq!(manager.config().min_batch_size, 2); } #[test] fn test_rollup_manager_default() { let manager = RollupManager::default(); assert_eq!(manager.state(), RollupState::Accepting); } #[test] fn test_rollup_manager_state_root() { let manager = RollupManager::new(); assert_eq!(manager.state_root(), StateRoot::zero()); } #[test] fn test_rollup_manager_setup() { let mut manager = RollupManager::new(); let result = manager.setup(); assert!(result.is_ok()); assert!(manager.verification_key().is_some()); } #[test] fn test_rollup_manager_register_account() { let manager = RollupManager::new(); let pubkey_hash = [0xab; 32]; let root = manager.register_account(0, pubkey_hash).unwrap(); assert_ne!(root, StateRoot::zero()); let account = manager.get_account(0).unwrap(); assert_eq!(account.pubkey_hash, pubkey_hash); } #[test] fn test_rollup_manager_register_multiple_accounts() { let manager = RollupManager::new(); for i in 0..10 { let root = manager.register_account(i, [i as u8; 32]).unwrap(); assert_ne!(root, StateRoot::zero()); } for i in 0..10 { let account = manager.get_account(i).unwrap(); assert_eq!(account.pubkey_hash, [i as u8; 32]); } } #[test] fn test_rollup_manager_get_nonexistent_account() { let manager = RollupManager::new(); assert!(manager.get_account(999).is_none()); } #[test] fn test_rollup_manager_add_transaction() { let manager = RollupManager::new(); manager.register_account(0, [0xaa; 32]).unwrap(); manager.register_account(1, [0xbb; 32]).unwrap(); manager .state_tree .update_account(0, |acc| acc.balance = 1000) .unwrap(); let tx = BatchTransaction::transfer(0, 1, 100); manager.add_transaction(tx).unwrap(); assert_eq!(manager.pending_count(), 1); } #[test] fn test_rollup_manager_add_multiple_transactions() { let manager = RollupManager::new(); manager.register_account(0, [0xaa; 32]).unwrap(); manager.register_account(1, [0xbb; 32]).unwrap(); manager .state_tree .update_account(0, |acc| acc.balance = 10000) .unwrap(); for _ in 0..5 { let tx = BatchTransaction::transfer(0, 1, 100); manager.add_transaction(tx).unwrap(); } assert_eq!(manager.pending_count(), 5); } #[test] fn test_rollup_manager_add_transaction_insufficient_balance() { let manager = RollupManager::new(); manager.register_account(0, [0xaa; 32]).unwrap(); manager.register_account(1, [0xbb; 32]).unwrap(); manager .state_tree .update_account(0, |acc| acc.balance = 50) .unwrap(); let tx = BatchTransaction::transfer(0, 1, 100); let result = manager.add_transaction(tx); assert!(matches!(result, Err(RollupError::InsufficientBalance))); } #[test] fn test_rollup_manager_add_transaction_account_not_found() { let manager = RollupManager::new(); let tx = BatchTransaction::transfer(999, 1, 100); let result = manager.add_transaction(tx); assert!(matches!(result, Err(RollupError::AccountNotFound(999)))); } #[test] fn test_rollup_manager_add_deposit_transaction() { let manager = RollupManager::new(); manager.register_account(0, [0xaa; 32]).unwrap(); let tx = BatchTransaction::deposit(0, 1000, [0xab; 32]); manager.add_transaction(tx).unwrap(); assert_eq!(manager.pending_count(), 1); } #[test] fn test_rollup_manager_add_withdraw_transaction() { let manager = RollupManager::new(); manager.register_account(0, [0xaa; 32]).unwrap(); manager .state_tree .update_account(0, |acc| acc.balance = 1000) .unwrap(); let tx = BatchTransaction::withdraw(0, [0xcc; 20], 500); manager.add_transaction(tx).unwrap(); assert_eq!(manager.pending_count(), 1); } #[test] fn test_rollup_manager_pause_resume() { let manager = RollupManager::new(); assert_eq!(manager.state(), RollupState::Accepting); manager.pause(); assert_eq!(manager.state(), RollupState::Paused); manager.resume(); assert_eq!(manager.state(), RollupState::Accepting); } #[test] fn test_rollup_manager_add_transaction_when_paused() { let manager = RollupManager::new(); manager.register_account(0, [0xaa; 32]).unwrap(); manager .state_tree .update_account(0, |acc| acc.balance = 1000) .unwrap(); manager.pause(); let tx = BatchTransaction::transfer(0, 1, 100); let result = manager.add_transaction(tx); assert!(matches!(result, Err(RollupError::NotAccepting))); } #[test] fn test_rollup_manager_finalize_batch() { let manager = RollupManager::new(); manager.register_account(0, [0xaa; 32]).unwrap(); manager.register_account(1, [0xbb; 32]).unwrap(); manager .state_tree .update_account(0, |acc| acc.balance = 1000) .unwrap(); let tx = BatchTransaction::transfer(0, 1, 100); manager.add_transaction(tx).unwrap(); let batch = manager.finalize_batch().unwrap(); assert_eq!(batch.tx_count(), 1); assert_ne!(batch.pre_state_root, batch.post_state_root); assert_eq!(manager.batch_count(), 1); } #[test] fn test_rollup_manager_finalize_empty_batch() { let manager = RollupManager::new(); let result = manager.finalize_batch(); assert!(result.is_ok()); let batch = result.unwrap(); assert_eq!(batch.tx_count(), 0); } #[test] fn test_rollup_manager_batch_state_transitions() { let manager = RollupManager::new(); manager.register_account(0, [0xaa; 32]).unwrap(); manager.register_account(1, [0xbb; 32]).unwrap(); manager .state_tree .update_account(0, |acc| acc.balance = 1000) .unwrap(); let tx = BatchTransaction::transfer(0, 1, 300); manager.add_transaction(tx).unwrap(); let pre_balance_0 = manager.get_account(0).unwrap().balance; let pre_balance_1 = manager.get_account(1).unwrap().balance; manager.finalize_batch().unwrap(); let post_balance_0 = manager.get_account(0).unwrap().balance; let post_balance_1 = manager.get_account(1).unwrap().balance; assert_eq!(post_balance_0, pre_balance_0 - 300); assert_eq!(post_balance_1, pre_balance_1 + 300); } #[test] fn test_rollup_manager_multiple_batches() { let manager = RollupManager::new(); manager.register_account(0, [0xaa; 32]).unwrap(); manager.register_account(1, [0xbb; 32]).unwrap(); manager .state_tree .update_account(0, |acc| acc.balance = 10000) .unwrap(); for i in 0..3 { let tx = BatchTransaction::transfer(0, 1, 100); manager.add_transaction(tx).unwrap(); let batch = manager.finalize_batch().unwrap(); assert_eq!(batch.batch_number, i); manager.resume(); } assert_eq!(manager.batch_count(), 3); } #[test] fn test_rollup_manager_verify_batch_no_proof() { let manager = RollupManager::new(); let batch = RollupBatch::new(0, StateRoot::zero()); let result = manager.verify_batch(&batch); assert!(matches!(result, Err(RollupError::NoProof))); } #[test] fn test_rollup_manager_verify_batch_no_vk() { let manager = RollupManager::new(); use crate::proof::{Proof, ProofSystemBackend}; let mut batch = RollupBatch::new(0, StateRoot::zero()); batch.proof = Some(Proof { backend: ProofSystemBackend::Groth16, data: vec![1, 2, 3], public_inputs: vec![], }); let result = manager.verify_batch(&batch); assert!(matches!(result, Err(RollupError::NoVerificationKey))); } #[test] fn test_rollup_manager_check_timeout_no_pending() { let manager = RollupManager::new(); assert!(!manager.check_timeout()); } #[test] fn test_rollup_manager_config_accessor() { let manager = RollupManager::new(); let config = manager.config(); assert_eq!(config.max_batch_size, crate::constants::MAX_BATCH_SIZE); } #[test] fn test_rollup_manager_debug() { let manager = RollupManager::new(); let debug_str = format!("{:?}", manager); assert!(debug_str.contains("RollupManager")); assert!(debug_str.contains("state")); } // ============================================================================ // RollupError Tests // ============================================================================ #[test] fn test_rollup_error_not_accepting_display() { let error = RollupError::NotAccepting; let display = format!("{}", error); assert!(display.contains("not accepting")); } #[test] fn test_rollup_error_no_batch_display() { let error = RollupError::NoBatch; let display = format!("{}", error); assert!(display.contains("No batch")); } #[test] fn test_rollup_error_no_proof_display() { let error = RollupError::NoProof; let display = format!("{}", error); assert!(display.contains("No proof")); } #[test] fn test_rollup_error_no_verification_key_display() { let error = RollupError::NoVerificationKey; let display = format!("{}", error); assert!(display.contains("No verification key")); } #[test] fn test_rollup_error_account_not_found_display() { let error = RollupError::AccountNotFound(42); let display = format!("{}", error); assert!(display.contains("Account not found")); assert!(display.contains("42")); } #[test] fn test_rollup_error_insufficient_balance_display() { let error = RollupError::InsufficientBalance; let display = format!("{}", error); assert!(display.contains("Insufficient balance")); } #[test] fn test_rollup_error_invalid_transaction_display() { let error = RollupError::InvalidTransaction("bad tx".to_string()); let display = format!("{}", error); assert!(display.contains("Invalid transaction")); assert!(display.contains("bad tx")); } #[test] fn test_rollup_error_from_state_error() { let state_error = StateError::AccountNotFound(99); let rollup_error: RollupError = state_error.into(); let display = format!("{}", rollup_error); assert!(display.contains("State error")); } #[test] fn test_rollup_error_from_proof_error() { use crate::proof::ProofError; let proof_error = ProofError::ProvingError("test".to_string()); let rollup_error: RollupError = proof_error.into(); let display = format!("{}", rollup_error); assert!(display.contains("Proof error")); } #[test] fn test_rollup_error_debug() { let error = RollupError::NotAccepting; let debug_str = format!("{:?}", error); assert!(debug_str.contains("NotAccepting")); } // ============================================================================ // Integration Tests // ============================================================================ #[test] fn test_full_workflow() { let mut manager = RollupManager::new(); manager.setup().unwrap(); manager.register_account(0, [0xaa; 32]).unwrap(); manager.register_account(1, [0xbb; 32]).unwrap(); manager.register_account(2, [0xcc; 32]).unwrap(); manager .state_tree .update_account(0, |acc| acc.balance = 10000) .unwrap(); manager .add_transaction(BatchTransaction::transfer(0, 1, 500)) .unwrap(); manager .add_transaction(BatchTransaction::deposit(2, 1000, [0x11; 32])) .unwrap(); let batch = manager.finalize_batch().unwrap(); assert_eq!(batch.tx_count(), 2); assert!(batch.is_proven()); assert!(manager.verify_batch(&batch).is_ok()); } #[test] fn test_deposit_then_transfer() { let manager = RollupManager::new(); manager.register_account(0, [0xaa; 32]).unwrap(); manager.register_account(1, [0xbb; 32]).unwrap(); manager .add_transaction(BatchTransaction::deposit(0, 1000, [0x11; 32])) .unwrap(); manager.finalize_batch().unwrap(); manager.resume(); assert_eq!(manager.get_account(0).unwrap().balance, 1000); manager .add_transaction(BatchTransaction::transfer(0, 1, 500)) .unwrap(); manager.finalize_batch().unwrap(); assert_eq!(manager.get_account(0).unwrap().balance, 500); assert_eq!(manager.get_account(1).unwrap().balance, 500); } #[test] fn test_withdrawal_workflow() { let manager = RollupManager::new(); manager.register_account(0, [0xaa; 32]).unwrap(); manager .state_tree .update_account(0, |acc| acc.balance = 1000) .unwrap(); manager .add_transaction(BatchTransaction::withdraw(0, [0xbb; 20], 300)) .unwrap(); manager.finalize_batch().unwrap(); assert_eq!(manager.get_account(0).unwrap().balance, 700); } // ============================================================================ // Edge Cases // ============================================================================ #[test] fn test_batch_with_many_transactions() { let config = RollupConfig { max_batch_size: 100, min_batch_size: 1, batch_timeout: Duration::from_secs(60), tree_depth: 32, bridge_address: None, }; let manager = RollupManager::with_config(config); manager.register_account(0, [0xaa; 32]).unwrap(); manager.register_account(1, [0xbb; 32]).unwrap(); manager .state_tree .update_account(0, |acc| acc.balance = 1_000_000) .unwrap(); for _ in 0..50 { manager .add_transaction(BatchTransaction::transfer(0, 1, 10)) .unwrap(); } assert_eq!(manager.pending_count(), 50); let batch = manager.finalize_batch().unwrap(); assert_eq!(batch.tx_count(), 50); } #[test] fn test_zero_amount_transactions() { let manager = RollupManager::new(); manager.register_account(0, [0xaa; 32]).unwrap(); manager.register_account(1, [0xbb; 32]).unwrap(); manager .state_tree .update_account(0, |acc| acc.balance = 100) .unwrap(); manager .add_transaction(BatchTransaction::transfer(0, 1, 0)) .unwrap(); assert_eq!(manager.pending_count(), 1); } #[test] fn test_large_account_indices() { let manager = RollupManager::new(); let large_idx = u64::MAX - 1; manager.register_account(large_idx, [0xaa; 32]).unwrap(); let account = manager.get_account(large_idx).unwrap(); assert_eq!(account.pubkey_hash, [0xaa; 32]); } }