synor/crates/synor-zk/src/rollup/mod.rs
2026-02-02 05:58:22 +05:30

1533 lines
46 KiB
Rust

//! 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<String>,
}
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<u8>,
},
/// 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<u8>,
},
}
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<u8>) -> 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<BatchTransaction>,
/// State root before batch
pub pre_state_root: StateRoot,
/// State root after batch
pub post_state_root: StateRoot,
/// Validity proof
pub proof: Option<Proof>,
/// 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<ProvingKey>,
verification_key: Option<VerificationKey>,
pending_txs: RwLock<VecDeque<BatchTransaction>>,
current_batch: RwLock<Option<RollupBatch>>,
committed_batches: RwLock<Vec<RollupBatch>>,
next_batch_number: RwLock<u64>,
state: RwLock<RollupState>,
batch_start_time: RwLock<Option<Instant>>,
}
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<RollupBatch, RollupError> {
// 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<bool, RollupError> {
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<StateRoot, RollupError> {
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<AccountState> {
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]);
}
}