1533 lines
46 KiB
Rust
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]);
|
|
}
|
|
}
|