1179 lines
37 KiB
Rust
1179 lines
37 KiB
Rust
//! ZK Circuit definitions for Synor rollups.
|
|
//!
|
|
//! Circuits define the constraints that must be satisfied for valid state transitions.
|
|
//! We use R1CS (Rank-1 Constraint System) representation for Groth16 compatibility.
|
|
//!
|
|
//! # Circuit Types
|
|
//!
|
|
//! - **TransferCircuit**: Validates token transfers between accounts
|
|
//! - **DepositCircuit**: Validates deposits from L1 to L2
|
|
//! - **WithdrawCircuit**: Validates withdrawals from L2 to L1
|
|
//! - **BatchCircuit**: Aggregates multiple transactions into single proof
|
|
|
|
use ark_bn254::Fr as ScalarField;
|
|
use ark_ff::PrimeField;
|
|
use ark_relations::r1cs::SynthesisError;
|
|
use serde::{Deserialize, Serialize};
|
|
use thiserror::Error;
|
|
|
|
use crate::state::StateRoot;
|
|
|
|
/// Circuit configuration parameters.
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
pub struct CircuitConfig {
|
|
/// Maximum number of transactions in a batch
|
|
pub max_batch_size: usize,
|
|
/// State tree depth
|
|
pub tree_depth: usize,
|
|
/// Enable signature verification in circuit
|
|
pub verify_signatures: bool,
|
|
}
|
|
|
|
impl Default for CircuitConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
max_batch_size: crate::constants::MAX_BATCH_SIZE,
|
|
tree_depth: crate::constants::STATE_TREE_DEPTH,
|
|
verify_signatures: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Constraint system interface for building circuits.
|
|
pub trait ConstraintSystem {
|
|
/// Allocates a public input variable.
|
|
fn alloc_input(&mut self, name: &str, value: ScalarField) -> Result<Variable, CircuitError>;
|
|
|
|
/// Allocates a private witness variable.
|
|
fn alloc_witness(&mut self, name: &str, value: ScalarField) -> Result<Variable, CircuitError>;
|
|
|
|
/// Adds a constraint: a * b = c
|
|
fn enforce_constraint(
|
|
&mut self,
|
|
a: LinearCombination,
|
|
b: LinearCombination,
|
|
c: LinearCombination,
|
|
) -> Result<(), CircuitError>;
|
|
|
|
/// Returns the number of constraints.
|
|
fn num_constraints(&self) -> usize;
|
|
|
|
/// Returns the number of public inputs.
|
|
fn num_inputs(&self) -> usize;
|
|
|
|
/// Returns the number of witness variables.
|
|
fn num_witnesses(&self) -> usize;
|
|
}
|
|
|
|
/// Variable in the constraint system.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
pub struct Variable(pub(crate) usize);
|
|
|
|
impl Variable {
|
|
/// Creates a new variable with the given index.
|
|
pub fn new(index: usize) -> Self {
|
|
Self(index)
|
|
}
|
|
|
|
/// Returns the variable index.
|
|
pub fn index(&self) -> usize {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
/// Linear combination of variables: sum(coeff_i * var_i).
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct LinearCombination {
|
|
terms: Vec<(ScalarField, Variable)>,
|
|
}
|
|
|
|
impl LinearCombination {
|
|
/// Creates an empty linear combination.
|
|
pub fn new() -> Self {
|
|
Self { terms: Vec::new() }
|
|
}
|
|
|
|
/// Creates a linear combination from a single variable.
|
|
pub fn from_variable(var: Variable) -> Self {
|
|
let mut lc = Self::new();
|
|
lc.add_term(ScalarField::from(1u64), var);
|
|
lc
|
|
}
|
|
|
|
/// Creates a linear combination from a constant.
|
|
pub fn from_constant(value: ScalarField) -> Self {
|
|
let mut lc = Self::new();
|
|
lc.add_term(value, Variable(0)); // Variable 0 is the constant 1
|
|
lc
|
|
}
|
|
|
|
/// Adds a term (coefficient * variable) to the linear combination.
|
|
pub fn add_term(&mut self, coeff: ScalarField, var: Variable) {
|
|
self.terms.push((coeff, var));
|
|
}
|
|
|
|
/// Returns the terms of the linear combination.
|
|
pub fn terms(&self) -> &[(ScalarField, Variable)] {
|
|
&self.terms
|
|
}
|
|
}
|
|
|
|
/// Base trait for all circuits.
|
|
pub trait Circuit: Clone + Send + Sync {
|
|
/// Returns the circuit name.
|
|
fn name(&self) -> &str;
|
|
|
|
/// Returns the circuit configuration.
|
|
fn config(&self) -> &CircuitConfig;
|
|
|
|
/// Synthesizes the circuit constraints.
|
|
fn synthesize<CS: ConstraintSystem>(&self, cs: &mut CS) -> Result<(), CircuitError>;
|
|
|
|
/// Returns the public inputs for this circuit instance.
|
|
fn public_inputs(&self) -> Vec<ScalarField>;
|
|
}
|
|
|
|
/// Transfer circuit: validates a token transfer between accounts.
|
|
#[derive(Clone, Debug)]
|
|
pub struct TransferCircuit {
|
|
config: CircuitConfig,
|
|
/// Previous state root
|
|
pub old_root: StateRoot,
|
|
/// New state root after transfer
|
|
pub new_root: StateRoot,
|
|
/// Sender account index
|
|
pub sender_idx: u64,
|
|
/// Recipient account index
|
|
pub recipient_idx: u64,
|
|
/// Transfer amount
|
|
pub amount: u64,
|
|
/// Sender's signature (for witness)
|
|
pub signature: Vec<u8>,
|
|
/// Merkle proof for sender account
|
|
pub sender_proof: Vec<[u8; 32]>,
|
|
/// Merkle proof for recipient account
|
|
pub recipient_proof: Vec<[u8; 32]>,
|
|
}
|
|
|
|
impl TransferCircuit {
|
|
/// Creates a new transfer circuit.
|
|
pub fn new(
|
|
old_root: StateRoot,
|
|
new_root: StateRoot,
|
|
sender_idx: u64,
|
|
recipient_idx: u64,
|
|
amount: u64,
|
|
) -> Self {
|
|
Self {
|
|
config: CircuitConfig::default(),
|
|
old_root,
|
|
new_root,
|
|
sender_idx,
|
|
recipient_idx,
|
|
amount,
|
|
signature: Vec::new(),
|
|
sender_proof: Vec::new(),
|
|
recipient_proof: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Sets the Merkle proofs for verification.
|
|
pub fn with_proofs(
|
|
mut self,
|
|
sender_proof: Vec<[u8; 32]>,
|
|
recipient_proof: Vec<[u8; 32]>,
|
|
) -> Self {
|
|
self.sender_proof = sender_proof;
|
|
self.recipient_proof = recipient_proof;
|
|
self
|
|
}
|
|
|
|
/// Sets the signature for verification.
|
|
pub fn with_signature(mut self, signature: Vec<u8>) -> Self {
|
|
self.signature = signature;
|
|
self
|
|
}
|
|
}
|
|
|
|
impl Circuit for TransferCircuit {
|
|
fn name(&self) -> &str {
|
|
"TransferCircuit"
|
|
}
|
|
|
|
fn config(&self) -> &CircuitConfig {
|
|
&self.config
|
|
}
|
|
|
|
fn synthesize<CS: ConstraintSystem>(&self, cs: &mut CS) -> Result<(), CircuitError> {
|
|
// Public inputs
|
|
let old_root_var = cs.alloc_input("old_root", field_from_hash(&self.old_root.0))?;
|
|
let new_root_var = cs.alloc_input("new_root", field_from_hash(&self.new_root.0))?;
|
|
|
|
// Private witnesses
|
|
let sender_idx_var = cs.alloc_witness("sender_idx", ScalarField::from(self.sender_idx))?;
|
|
let recipient_idx_var =
|
|
cs.alloc_witness("recipient_idx", ScalarField::from(self.recipient_idx))?;
|
|
let amount_var = cs.alloc_witness("amount", ScalarField::from(self.amount))?;
|
|
|
|
// Constraint 1: Amount > 0
|
|
// We don't have a direct "greater than" constraint in R1CS,
|
|
// but we can enforce that amount is non-zero by requiring amount * amount_inv = 1
|
|
// This is a simplified version; real implementation would use range proofs
|
|
|
|
// Constraint 2: Merkle proof verification for sender
|
|
// Verify that sender account exists at sender_idx in old_root
|
|
// (Merkle path verification constraints)
|
|
|
|
// Constraint 3: Merkle proof verification for recipient
|
|
// Verify that recipient account exists at recipient_idx in old_root
|
|
|
|
// Constraint 4: Balance check
|
|
// sender.balance >= amount
|
|
|
|
// Constraint 5: State transition
|
|
// new_sender_balance = old_sender_balance - amount
|
|
// new_recipient_balance = old_recipient_balance + amount
|
|
|
|
// Constraint 6: New root computation
|
|
// Verify that new_root is correct after state updates
|
|
|
|
// Note: Full implementation would include all Merkle proof verification
|
|
// and hash constraints. This is a structural placeholder.
|
|
|
|
// Dummy constraint to ensure variables are used
|
|
let _lc = LinearCombination::from_variable(old_root_var);
|
|
let _lc2 = LinearCombination::from_variable(new_root_var);
|
|
let _lc3 = LinearCombination::from_variable(sender_idx_var);
|
|
let _lc4 = LinearCombination::from_variable(recipient_idx_var);
|
|
let _lc5 = LinearCombination::from_variable(amount_var);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn public_inputs(&self) -> Vec<ScalarField> {
|
|
vec![
|
|
field_from_hash(&self.old_root.0),
|
|
field_from_hash(&self.new_root.0),
|
|
]
|
|
}
|
|
}
|
|
|
|
/// Batch circuit: aggregates multiple transfers into a single proof.
|
|
#[derive(Clone, Debug)]
|
|
pub struct BatchCircuit {
|
|
config: CircuitConfig,
|
|
/// Initial state root
|
|
pub initial_root: StateRoot,
|
|
/// Final state root after all transactions
|
|
pub final_root: StateRoot,
|
|
/// Individual transfer circuits
|
|
pub transfers: Vec<TransferCircuit>,
|
|
}
|
|
|
|
impl BatchCircuit {
|
|
/// Creates a new batch circuit.
|
|
pub fn new(initial_root: StateRoot, final_root: StateRoot) -> Self {
|
|
Self {
|
|
config: CircuitConfig::default(),
|
|
initial_root,
|
|
final_root,
|
|
transfers: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Adds a transfer to the batch.
|
|
pub fn add_transfer(&mut self, transfer: TransferCircuit) {
|
|
self.transfers.push(transfer);
|
|
}
|
|
|
|
/// Returns the number of transfers in the batch.
|
|
pub fn num_transfers(&self) -> usize {
|
|
self.transfers.len()
|
|
}
|
|
}
|
|
|
|
impl Circuit for BatchCircuit {
|
|
fn name(&self) -> &str {
|
|
"BatchCircuit"
|
|
}
|
|
|
|
fn config(&self) -> &CircuitConfig {
|
|
&self.config
|
|
}
|
|
|
|
fn synthesize<CS: ConstraintSystem>(&self, cs: &mut CS) -> Result<(), CircuitError> {
|
|
// Public inputs: initial_root, final_root, batch_hash
|
|
let initial_root_var =
|
|
cs.alloc_input("initial_root", field_from_hash(&self.initial_root.0))?;
|
|
let final_root_var = cs.alloc_input("final_root", field_from_hash(&self.final_root.0))?;
|
|
|
|
// For each transfer, verify:
|
|
// 1. The intermediate state roots chain correctly
|
|
// 2. Each individual transfer is valid
|
|
|
|
// In a real implementation, we would:
|
|
// - Synthesize each transfer circuit
|
|
// - Chain the state roots: root_i+1 = update(root_i, transfer_i)
|
|
// - Verify initial_root -> transfer_1 -> ... -> transfer_n -> final_root
|
|
|
|
let _lc = LinearCombination::from_variable(initial_root_var);
|
|
let _lc2 = LinearCombination::from_variable(final_root_var);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn public_inputs(&self) -> Vec<ScalarField> {
|
|
vec![
|
|
field_from_hash(&self.initial_root.0),
|
|
field_from_hash(&self.final_root.0),
|
|
]
|
|
}
|
|
}
|
|
|
|
/// Deposit circuit: validates an L1 -> L2 deposit.
|
|
#[derive(Clone, Debug)]
|
|
pub struct DepositCircuit {
|
|
config: CircuitConfig,
|
|
/// Previous state root
|
|
pub old_root: StateRoot,
|
|
/// New state root after deposit
|
|
pub new_root: StateRoot,
|
|
/// L1 deposit transaction hash
|
|
pub l1_tx_hash: [u8; 32],
|
|
/// Recipient account index on L2
|
|
pub recipient_idx: u64,
|
|
/// Deposit amount
|
|
pub amount: u64,
|
|
}
|
|
|
|
impl Circuit for DepositCircuit {
|
|
fn name(&self) -> &str {
|
|
"DepositCircuit"
|
|
}
|
|
|
|
fn config(&self) -> &CircuitConfig {
|
|
&self.config
|
|
}
|
|
|
|
fn synthesize<CS: ConstraintSystem>(&self, cs: &mut CS) -> Result<(), CircuitError> {
|
|
let old_root_var = cs.alloc_input("old_root", field_from_hash(&self.old_root.0))?;
|
|
let new_root_var = cs.alloc_input("new_root", field_from_hash(&self.new_root.0))?;
|
|
let l1_tx_hash_var = cs.alloc_input("l1_tx_hash", field_from_hash(&self.l1_tx_hash))?;
|
|
|
|
let _lc = LinearCombination::from_variable(old_root_var);
|
|
let _lc2 = LinearCombination::from_variable(new_root_var);
|
|
let _lc3 = LinearCombination::from_variable(l1_tx_hash_var);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn public_inputs(&self) -> Vec<ScalarField> {
|
|
vec![
|
|
field_from_hash(&self.old_root.0),
|
|
field_from_hash(&self.new_root.0),
|
|
field_from_hash(&self.l1_tx_hash),
|
|
]
|
|
}
|
|
}
|
|
|
|
/// Withdrawal circuit: validates an L2 -> L1 withdrawal.
|
|
#[derive(Clone, Debug)]
|
|
pub struct WithdrawCircuit {
|
|
config: CircuitConfig,
|
|
/// Previous state root
|
|
pub old_root: StateRoot,
|
|
/// New state root after withdrawal
|
|
pub new_root: StateRoot,
|
|
/// Sender account index on L2
|
|
pub sender_idx: u64,
|
|
/// L1 recipient address
|
|
pub l1_recipient: [u8; 20],
|
|
/// Withdrawal amount
|
|
pub amount: u64,
|
|
}
|
|
|
|
impl Circuit for WithdrawCircuit {
|
|
fn name(&self) -> &str {
|
|
"WithdrawCircuit"
|
|
}
|
|
|
|
fn config(&self) -> &CircuitConfig {
|
|
&self.config
|
|
}
|
|
|
|
fn synthesize<CS: ConstraintSystem>(&self, cs: &mut CS) -> Result<(), CircuitError> {
|
|
let old_root_var = cs.alloc_input("old_root", field_from_hash(&self.old_root.0))?;
|
|
let new_root_var = cs.alloc_input("new_root", field_from_hash(&self.new_root.0))?;
|
|
|
|
let _lc = LinearCombination::from_variable(old_root_var);
|
|
let _lc2 = LinearCombination::from_variable(new_root_var);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn public_inputs(&self) -> Vec<ScalarField> {
|
|
vec![
|
|
field_from_hash(&self.old_root.0),
|
|
field_from_hash(&self.new_root.0),
|
|
]
|
|
}
|
|
}
|
|
|
|
/// Converts a 32-byte hash to a scalar field element.
|
|
fn field_from_hash(hash: &[u8; 32]) -> ScalarField {
|
|
// Take first 31 bytes to ensure it fits in the field
|
|
let mut bytes = [0u8; 32];
|
|
bytes[..31].copy_from_slice(&hash[..31]);
|
|
ScalarField::from_le_bytes_mod_order(&bytes)
|
|
}
|
|
|
|
/// Circuit errors.
|
|
#[derive(Debug, Error)]
|
|
pub enum CircuitError {
|
|
#[error("Synthesis error: {0}")]
|
|
SynthesisError(String),
|
|
|
|
#[error("Invalid witness: {0}")]
|
|
InvalidWitness(String),
|
|
|
|
#[error("Constraint violation: {0}")]
|
|
ConstraintViolation(String),
|
|
|
|
#[error("Circuit configuration error: {0}")]
|
|
ConfigError(String),
|
|
}
|
|
|
|
impl From<SynthesisError> for CircuitError {
|
|
fn from(e: SynthesisError) -> Self {
|
|
CircuitError::SynthesisError(e.to_string())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
// ============================================================================
|
|
// Variable Tests
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_variable_new_creation() {
|
|
let var = Variable::new(0);
|
|
assert_eq!(var.index(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_variable_new_with_various_indices() {
|
|
for i in [0, 1, 100, 1000, usize::MAX] {
|
|
let var = Variable::new(i);
|
|
assert_eq!(var.index(), i);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_variable_equality() {
|
|
let var1 = Variable::new(5);
|
|
let var2 = Variable::new(5);
|
|
let var3 = Variable::new(10);
|
|
|
|
assert_eq!(var1, var2);
|
|
assert_ne!(var1, var3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_variable_clone() {
|
|
let var1 = Variable::new(42);
|
|
let var2 = var1;
|
|
assert_eq!(var1, var2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_variable_debug_format() {
|
|
let var = Variable::new(123);
|
|
let debug_str = format!("{:?}", var);
|
|
assert!(debug_str.contains("Variable"));
|
|
assert!(debug_str.contains("123"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_variable_hash() {
|
|
use std::collections::HashSet;
|
|
let mut set = HashSet::new();
|
|
set.insert(Variable::new(1));
|
|
set.insert(Variable::new(2));
|
|
set.insert(Variable::new(1)); // Duplicate
|
|
|
|
assert_eq!(set.len(), 2);
|
|
}
|
|
|
|
// ============================================================================
|
|
// LinearCombination Tests
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_linear_combination_new_empty() {
|
|
let lc = LinearCombination::new();
|
|
assert!(lc.terms().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_linear_combination_from_variable() {
|
|
let var = Variable::new(1);
|
|
let lc = LinearCombination::from_variable(var);
|
|
assert_eq!(lc.terms().len(), 1);
|
|
assert_eq!(lc.terms()[0].1, var);
|
|
assert_eq!(lc.terms()[0].0, ScalarField::from(1u64));
|
|
}
|
|
|
|
#[test]
|
|
fn test_linear_combination_from_constant() {
|
|
let value = ScalarField::from(42u64);
|
|
let lc = LinearCombination::from_constant(value);
|
|
assert_eq!(lc.terms().len(), 1);
|
|
assert_eq!(lc.terms()[0].0, value);
|
|
assert_eq!(lc.terms()[0].1, Variable(0)); // Constant is variable 0
|
|
}
|
|
|
|
#[test]
|
|
fn test_linear_combination_add_term() {
|
|
let mut lc = LinearCombination::new();
|
|
let var1 = Variable::new(1);
|
|
let var2 = Variable::new(2);
|
|
|
|
lc.add_term(ScalarField::from(3u64), var1);
|
|
assert_eq!(lc.terms().len(), 1);
|
|
|
|
lc.add_term(ScalarField::from(5u64), var2);
|
|
assert_eq!(lc.terms().len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_linear_combination_multiple_terms() {
|
|
let mut lc = LinearCombination::new();
|
|
for i in 0..10 {
|
|
lc.add_term(ScalarField::from(i as u64), Variable::new(i));
|
|
}
|
|
assert_eq!(lc.terms().len(), 10);
|
|
}
|
|
|
|
#[test]
|
|
fn test_linear_combination_terms_preserved_order() {
|
|
let mut lc = LinearCombination::new();
|
|
let vars: Vec<Variable> = (0..5).map(Variable::new).collect();
|
|
|
|
for (i, &var) in vars.iter().enumerate() {
|
|
lc.add_term(ScalarField::from((i + 1) as u64), var);
|
|
}
|
|
|
|
for (i, (coeff, var)) in lc.terms().iter().enumerate() {
|
|
assert_eq!(*coeff, ScalarField::from((i + 1) as u64));
|
|
assert_eq!(*var, vars[i]);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_linear_combination_default() {
|
|
let lc = LinearCombination::default();
|
|
assert!(lc.terms().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_linear_combination_clone() {
|
|
let mut lc1 = LinearCombination::new();
|
|
lc1.add_term(ScalarField::from(5u64), Variable::new(1));
|
|
let lc2 = lc1.clone();
|
|
|
|
assert_eq!(lc1.terms().len(), lc2.terms().len());
|
|
assert_eq!(lc1.terms()[0].0, lc2.terms()[0].0);
|
|
assert_eq!(lc1.terms()[0].1, lc2.terms()[0].1);
|
|
}
|
|
|
|
// ============================================================================
|
|
// CircuitConfig Tests
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_circuit_config_default() {
|
|
let config = CircuitConfig::default();
|
|
assert_eq!(config.max_batch_size, crate::constants::MAX_BATCH_SIZE);
|
|
assert_eq!(config.tree_depth, crate::constants::STATE_TREE_DEPTH);
|
|
assert!(config.verify_signatures);
|
|
}
|
|
|
|
#[test]
|
|
fn test_circuit_config_custom_values() {
|
|
let config = CircuitConfig {
|
|
max_batch_size: 500,
|
|
tree_depth: 16,
|
|
verify_signatures: false,
|
|
};
|
|
assert_eq!(config.max_batch_size, 500);
|
|
assert_eq!(config.tree_depth, 16);
|
|
assert!(!config.verify_signatures);
|
|
}
|
|
|
|
#[test]
|
|
fn test_circuit_config_clone() {
|
|
let config1 = CircuitConfig::default();
|
|
let config2 = config1.clone();
|
|
assert_eq!(config1.max_batch_size, config2.max_batch_size);
|
|
assert_eq!(config1.tree_depth, config2.tree_depth);
|
|
assert_eq!(config1.verify_signatures, config2.verify_signatures);
|
|
}
|
|
|
|
#[test]
|
|
fn test_circuit_config_debug() {
|
|
let config = CircuitConfig::default();
|
|
let debug_str = format!("{:?}", config);
|
|
assert!(debug_str.contains("CircuitConfig"));
|
|
assert!(debug_str.contains("max_batch_size"));
|
|
}
|
|
|
|
// ============================================================================
|
|
// TransferCircuit Tests
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_transfer_circuit_creation() {
|
|
let old_root = StateRoot([0u8; 32]);
|
|
let new_root = StateRoot([1u8; 32]);
|
|
|
|
let circuit = TransferCircuit::new(old_root, new_root, 0, 1, 100);
|
|
assert_eq!(circuit.name(), "TransferCircuit");
|
|
assert_eq!(circuit.sender_idx, 0);
|
|
assert_eq!(circuit.recipient_idx, 1);
|
|
assert_eq!(circuit.amount, 100);
|
|
}
|
|
|
|
#[test]
|
|
fn test_transfer_circuit_with_state_roots() {
|
|
let old_root = StateRoot([0xaa; 32]);
|
|
let new_root = StateRoot([0xbb; 32]);
|
|
|
|
let circuit = TransferCircuit::new(old_root, new_root, 10, 20, 500);
|
|
assert_eq!(circuit.old_root, old_root);
|
|
assert_eq!(circuit.new_root, new_root);
|
|
}
|
|
|
|
#[test]
|
|
fn test_transfer_circuit_with_proofs() {
|
|
let old_root = StateRoot([0u8; 32]);
|
|
let new_root = StateRoot([1u8; 32]);
|
|
|
|
let sender_proof = vec![[1u8; 32], [2u8; 32]];
|
|
let recipient_proof = vec![[3u8; 32], [4u8; 32]];
|
|
|
|
let circuit = TransferCircuit::new(old_root, new_root, 0, 1, 100)
|
|
.with_proofs(sender_proof.clone(), recipient_proof.clone());
|
|
|
|
assert_eq!(circuit.sender_proof, sender_proof);
|
|
assert_eq!(circuit.recipient_proof, recipient_proof);
|
|
}
|
|
|
|
#[test]
|
|
fn test_transfer_circuit_with_signature() {
|
|
let old_root = StateRoot([0u8; 32]);
|
|
let new_root = StateRoot([1u8; 32]);
|
|
let signature = vec![0xab, 0xcd, 0xef];
|
|
|
|
let circuit =
|
|
TransferCircuit::new(old_root, new_root, 0, 1, 100).with_signature(signature.clone());
|
|
|
|
assert_eq!(circuit.signature, signature);
|
|
}
|
|
|
|
#[test]
|
|
fn test_transfer_circuit_public_inputs_extraction() {
|
|
let old_root = StateRoot([0xaa; 32]);
|
|
let new_root = StateRoot([0xbb; 32]);
|
|
|
|
let circuit = TransferCircuit::new(old_root, new_root, 0, 1, 100);
|
|
let public_inputs = circuit.public_inputs();
|
|
|
|
assert_eq!(public_inputs.len(), 2);
|
|
assert_eq!(public_inputs[0], field_from_hash(&old_root.0));
|
|
assert_eq!(public_inputs[1], field_from_hash(&new_root.0));
|
|
}
|
|
|
|
#[test]
|
|
fn test_transfer_circuit_name() {
|
|
let circuit = TransferCircuit::new(StateRoot([0u8; 32]), StateRoot([1u8; 32]), 0, 1, 100);
|
|
assert_eq!(circuit.name(), "TransferCircuit");
|
|
}
|
|
|
|
#[test]
|
|
fn test_transfer_circuit_config_default() {
|
|
let circuit = TransferCircuit::new(StateRoot([0u8; 32]), StateRoot([1u8; 32]), 0, 1, 100);
|
|
let config = circuit.config();
|
|
assert_eq!(config.max_batch_size, crate::constants::MAX_BATCH_SIZE);
|
|
}
|
|
|
|
#[test]
|
|
fn test_transfer_circuit_builder_chain() {
|
|
let circuit = TransferCircuit::new(StateRoot([0u8; 32]), StateRoot([1u8; 32]), 0, 1, 100)
|
|
.with_proofs(vec![[1u8; 32]], vec![[2u8; 32]])
|
|
.with_signature(vec![1, 2, 3]);
|
|
|
|
assert_eq!(circuit.sender_proof.len(), 1);
|
|
assert_eq!(circuit.recipient_proof.len(), 1);
|
|
assert_eq!(circuit.signature.len(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_transfer_circuit_clone() {
|
|
let circuit =
|
|
TransferCircuit::new(StateRoot([0xaa; 32]), StateRoot([0xbb; 32]), 5, 10, 500);
|
|
let cloned = circuit.clone();
|
|
|
|
assert_eq!(circuit.old_root, cloned.old_root);
|
|
assert_eq!(circuit.new_root, cloned.new_root);
|
|
assert_eq!(circuit.sender_idx, cloned.sender_idx);
|
|
assert_eq!(circuit.recipient_idx, cloned.recipient_idx);
|
|
assert_eq!(circuit.amount, cloned.amount);
|
|
}
|
|
|
|
#[test]
|
|
fn test_transfer_circuit_large_amount() {
|
|
let circuit =
|
|
TransferCircuit::new(StateRoot([0u8; 32]), StateRoot([1u8; 32]), 0, 1, u64::MAX);
|
|
assert_eq!(circuit.amount, u64::MAX);
|
|
}
|
|
|
|
#[test]
|
|
fn test_transfer_circuit_zero_amount() {
|
|
let circuit = TransferCircuit::new(StateRoot([0u8; 32]), StateRoot([1u8; 32]), 0, 1, 0);
|
|
assert_eq!(circuit.amount, 0);
|
|
}
|
|
|
|
// ============================================================================
|
|
// BatchCircuit Tests
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_batch_circuit_empty_creation() {
|
|
let initial_root = StateRoot([0u8; 32]);
|
|
let final_root = StateRoot([2u8; 32]);
|
|
|
|
let batch = BatchCircuit::new(initial_root, final_root);
|
|
assert_eq!(batch.num_transfers(), 0);
|
|
assert_eq!(batch.initial_root, initial_root);
|
|
assert_eq!(batch.final_root, final_root);
|
|
}
|
|
|
|
#[test]
|
|
fn test_batch_circuit_add_transfer() {
|
|
let mut batch = BatchCircuit::new(StateRoot([0u8; 32]), StateRoot([2u8; 32]));
|
|
assert_eq!(batch.num_transfers(), 0);
|
|
|
|
let transfer = TransferCircuit::new(StateRoot([0u8; 32]), StateRoot([1u8; 32]), 0, 1, 100);
|
|
batch.add_transfer(transfer);
|
|
assert_eq!(batch.num_transfers(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_batch_circuit_add_multiple_transfers() {
|
|
let mut batch = BatchCircuit::new(StateRoot([0u8; 32]), StateRoot([10u8; 32]));
|
|
|
|
for i in 0..5 {
|
|
let transfer = TransferCircuit::new(
|
|
StateRoot([i as u8; 32]),
|
|
StateRoot([(i + 1) as u8; 32]),
|
|
i as u64,
|
|
(i + 1) as u64,
|
|
100 * (i as u64 + 1),
|
|
);
|
|
batch.add_transfer(transfer);
|
|
}
|
|
|
|
assert_eq!(batch.num_transfers(), 5);
|
|
}
|
|
|
|
#[test]
|
|
fn test_batch_circuit_num_transfers_accuracy() {
|
|
let mut batch = BatchCircuit::new(StateRoot([0u8; 32]), StateRoot([10u8; 32]));
|
|
|
|
for i in 0..100 {
|
|
batch.add_transfer(TransferCircuit::new(
|
|
StateRoot([0u8; 32]),
|
|
StateRoot([1u8; 32]),
|
|
0,
|
|
1,
|
|
i,
|
|
));
|
|
assert_eq!(batch.num_transfers(), (i + 1) as usize);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_batch_circuit_public_inputs() {
|
|
let initial_root = StateRoot([0xaa; 32]);
|
|
let final_root = StateRoot([0xbb; 32]);
|
|
|
|
let batch = BatchCircuit::new(initial_root, final_root);
|
|
let public_inputs = batch.public_inputs();
|
|
|
|
assert_eq!(public_inputs.len(), 2);
|
|
assert_eq!(public_inputs[0], field_from_hash(&initial_root.0));
|
|
assert_eq!(public_inputs[1], field_from_hash(&final_root.0));
|
|
}
|
|
|
|
#[test]
|
|
fn test_batch_circuit_name() {
|
|
let batch = BatchCircuit::new(StateRoot([0u8; 32]), StateRoot([1u8; 32]));
|
|
assert_eq!(batch.name(), "BatchCircuit");
|
|
}
|
|
|
|
#[test]
|
|
fn test_batch_circuit_config() {
|
|
let batch = BatchCircuit::new(StateRoot([0u8; 32]), StateRoot([1u8; 32]));
|
|
let config = batch.config();
|
|
assert_eq!(config.max_batch_size, crate::constants::MAX_BATCH_SIZE);
|
|
}
|
|
|
|
#[test]
|
|
fn test_batch_circuit_clone() {
|
|
let mut batch = BatchCircuit::new(StateRoot([0xaa; 32]), StateRoot([0xbb; 32]));
|
|
batch.add_transfer(TransferCircuit::new(
|
|
StateRoot([0u8; 32]),
|
|
StateRoot([1u8; 32]),
|
|
0,
|
|
1,
|
|
100,
|
|
));
|
|
|
|
let cloned = batch.clone();
|
|
assert_eq!(batch.initial_root, cloned.initial_root);
|
|
assert_eq!(batch.final_root, cloned.final_root);
|
|
assert_eq!(batch.num_transfers(), cloned.num_transfers());
|
|
}
|
|
|
|
// ============================================================================
|
|
// DepositCircuit Tests
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_deposit_circuit_structure() {
|
|
let circuit = DepositCircuit {
|
|
config: CircuitConfig::default(),
|
|
old_root: StateRoot([0xaa; 32]),
|
|
new_root: StateRoot([0xbb; 32]),
|
|
l1_tx_hash: [0xcc; 32],
|
|
recipient_idx: 42,
|
|
amount: 1000,
|
|
};
|
|
|
|
assert_eq!(circuit.old_root, StateRoot([0xaa; 32]));
|
|
assert_eq!(circuit.new_root, StateRoot([0xbb; 32]));
|
|
assert_eq!(circuit.l1_tx_hash, [0xcc; 32]);
|
|
assert_eq!(circuit.recipient_idx, 42);
|
|
assert_eq!(circuit.amount, 1000);
|
|
}
|
|
|
|
#[test]
|
|
fn test_deposit_circuit_name() {
|
|
let circuit = DepositCircuit {
|
|
config: CircuitConfig::default(),
|
|
old_root: StateRoot([0u8; 32]),
|
|
new_root: StateRoot([1u8; 32]),
|
|
l1_tx_hash: [0u8; 32],
|
|
recipient_idx: 0,
|
|
amount: 0,
|
|
};
|
|
assert_eq!(circuit.name(), "DepositCircuit");
|
|
}
|
|
|
|
#[test]
|
|
fn test_deposit_circuit_public_inputs() {
|
|
let circuit = DepositCircuit {
|
|
config: CircuitConfig::default(),
|
|
old_root: StateRoot([0xaa; 32]),
|
|
new_root: StateRoot([0xbb; 32]),
|
|
l1_tx_hash: [0xcc; 32],
|
|
recipient_idx: 0,
|
|
amount: 100,
|
|
};
|
|
|
|
let public_inputs = circuit.public_inputs();
|
|
assert_eq!(public_inputs.len(), 3);
|
|
assert_eq!(public_inputs[0], field_from_hash(&circuit.old_root.0));
|
|
assert_eq!(public_inputs[1], field_from_hash(&circuit.new_root.0));
|
|
assert_eq!(public_inputs[2], field_from_hash(&circuit.l1_tx_hash));
|
|
}
|
|
|
|
#[test]
|
|
fn test_deposit_circuit_clone() {
|
|
let circuit = DepositCircuit {
|
|
config: CircuitConfig::default(),
|
|
old_root: StateRoot([0xaa; 32]),
|
|
new_root: StateRoot([0xbb; 32]),
|
|
l1_tx_hash: [0xcc; 32],
|
|
recipient_idx: 42,
|
|
amount: 1000,
|
|
};
|
|
|
|
let cloned = circuit.clone();
|
|
assert_eq!(circuit.old_root, cloned.old_root);
|
|
assert_eq!(circuit.l1_tx_hash, cloned.l1_tx_hash);
|
|
assert_eq!(circuit.amount, cloned.amount);
|
|
}
|
|
|
|
#[test]
|
|
fn test_deposit_circuit_config() {
|
|
let circuit = DepositCircuit {
|
|
config: CircuitConfig::default(),
|
|
old_root: StateRoot([0u8; 32]),
|
|
new_root: StateRoot([1u8; 32]),
|
|
l1_tx_hash: [0u8; 32],
|
|
recipient_idx: 0,
|
|
amount: 0,
|
|
};
|
|
let config = circuit.config();
|
|
assert_eq!(config.max_batch_size, crate::constants::MAX_BATCH_SIZE);
|
|
}
|
|
|
|
// ============================================================================
|
|
// WithdrawCircuit Tests
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_withdraw_circuit_structure() {
|
|
let circuit = WithdrawCircuit {
|
|
config: CircuitConfig::default(),
|
|
old_root: StateRoot([0xaa; 32]),
|
|
new_root: StateRoot([0xbb; 32]),
|
|
sender_idx: 10,
|
|
l1_recipient: [0xcc; 20],
|
|
amount: 500,
|
|
};
|
|
|
|
assert_eq!(circuit.old_root, StateRoot([0xaa; 32]));
|
|
assert_eq!(circuit.new_root, StateRoot([0xbb; 32]));
|
|
assert_eq!(circuit.sender_idx, 10);
|
|
assert_eq!(circuit.l1_recipient, [0xcc; 20]);
|
|
assert_eq!(circuit.amount, 500);
|
|
}
|
|
|
|
#[test]
|
|
fn test_withdraw_circuit_name() {
|
|
let circuit = WithdrawCircuit {
|
|
config: CircuitConfig::default(),
|
|
old_root: StateRoot([0u8; 32]),
|
|
new_root: StateRoot([1u8; 32]),
|
|
sender_idx: 0,
|
|
l1_recipient: [0u8; 20],
|
|
amount: 0,
|
|
};
|
|
assert_eq!(circuit.name(), "WithdrawCircuit");
|
|
}
|
|
|
|
#[test]
|
|
fn test_withdraw_circuit_public_inputs() {
|
|
let circuit = WithdrawCircuit {
|
|
config: CircuitConfig::default(),
|
|
old_root: StateRoot([0xaa; 32]),
|
|
new_root: StateRoot([0xbb; 32]),
|
|
sender_idx: 0,
|
|
l1_recipient: [0u8; 20],
|
|
amount: 100,
|
|
};
|
|
|
|
let public_inputs = circuit.public_inputs();
|
|
assert_eq!(public_inputs.len(), 2);
|
|
assert_eq!(public_inputs[0], field_from_hash(&circuit.old_root.0));
|
|
assert_eq!(public_inputs[1], field_from_hash(&circuit.new_root.0));
|
|
}
|
|
|
|
#[test]
|
|
fn test_withdraw_circuit_clone() {
|
|
let circuit = WithdrawCircuit {
|
|
config: CircuitConfig::default(),
|
|
old_root: StateRoot([0xaa; 32]),
|
|
new_root: StateRoot([0xbb; 32]),
|
|
sender_idx: 10,
|
|
l1_recipient: [0xcc; 20],
|
|
amount: 500,
|
|
};
|
|
|
|
let cloned = circuit.clone();
|
|
assert_eq!(circuit.old_root, cloned.old_root);
|
|
assert_eq!(circuit.sender_idx, cloned.sender_idx);
|
|
assert_eq!(circuit.l1_recipient, cloned.l1_recipient);
|
|
assert_eq!(circuit.amount, cloned.amount);
|
|
}
|
|
|
|
#[test]
|
|
fn test_withdraw_circuit_config() {
|
|
let circuit = WithdrawCircuit {
|
|
config: CircuitConfig::default(),
|
|
old_root: StateRoot([0u8; 32]),
|
|
new_root: StateRoot([1u8; 32]),
|
|
sender_idx: 0,
|
|
l1_recipient: [0u8; 20],
|
|
amount: 0,
|
|
};
|
|
let config = circuit.config();
|
|
assert!(config.verify_signatures);
|
|
}
|
|
|
|
// ============================================================================
|
|
// CircuitError Tests
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_circuit_error_synthesis_display() {
|
|
let error = CircuitError::SynthesisError("test synthesis error".to_string());
|
|
let display = format!("{}", error);
|
|
assert!(display.contains("Synthesis error"));
|
|
assert!(display.contains("test synthesis error"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_circuit_error_invalid_witness_display() {
|
|
let error = CircuitError::InvalidWitness("bad witness data".to_string());
|
|
let display = format!("{}", error);
|
|
assert!(display.contains("Invalid witness"));
|
|
assert!(display.contains("bad witness data"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_circuit_error_constraint_violation_display() {
|
|
let error = CircuitError::ConstraintViolation("constraint failed".to_string());
|
|
let display = format!("{}", error);
|
|
assert!(display.contains("Constraint violation"));
|
|
assert!(display.contains("constraint failed"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_circuit_error_config_display() {
|
|
let error = CircuitError::ConfigError("invalid config".to_string());
|
|
let display = format!("{}", error);
|
|
assert!(display.contains("Circuit configuration error"));
|
|
assert!(display.contains("invalid config"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_circuit_error_debug_format() {
|
|
let error = CircuitError::SynthesisError("test".to_string());
|
|
let debug_str = format!("{:?}", error);
|
|
assert!(debug_str.contains("SynthesisError"));
|
|
}
|
|
|
|
// ============================================================================
|
|
// field_from_hash Tests
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_field_from_hash_zero() {
|
|
let hash = [0u8; 32];
|
|
let field = field_from_hash(&hash);
|
|
assert_eq!(field, ScalarField::from(0u64));
|
|
}
|
|
|
|
#[test]
|
|
fn test_field_from_hash_all_ones() {
|
|
let hash = [0xffu8; 32];
|
|
let field = field_from_hash(&hash);
|
|
let _ = field;
|
|
}
|
|
|
|
#[test]
|
|
fn test_field_from_hash_consistency() {
|
|
let hash = [0xab; 32];
|
|
let field1 = field_from_hash(&hash);
|
|
let field2 = field_from_hash(&hash);
|
|
assert_eq!(field1, field2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_field_from_hash_different_inputs() {
|
|
let hash1 = [0xaa; 32];
|
|
let hash2 = [0xbb; 32];
|
|
let field1 = field_from_hash(&hash1);
|
|
let field2 = field_from_hash(&hash2);
|
|
assert_ne!(field1, field2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_field_from_hash_truncation() {
|
|
let mut hash1 = [0xaa; 32];
|
|
let mut hash2 = [0xaa; 32];
|
|
hash1[31] = 0x00;
|
|
hash2[31] = 0xff;
|
|
let field1 = field_from_hash(&hash1);
|
|
let field2 = field_from_hash(&hash2);
|
|
assert_eq!(field1, field2);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Circuit Trait Implementation Tests
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_transfer_circuit_trait_clone() {
|
|
let circuit = TransferCircuit::new(StateRoot([0u8; 32]), StateRoot([1u8; 32]), 0, 1, 100);
|
|
|
|
fn assert_clone<T: Clone>(_: &T) {}
|
|
assert_clone(&circuit);
|
|
}
|
|
|
|
#[test]
|
|
fn test_batch_circuit_trait_send_sync() {
|
|
let batch = BatchCircuit::new(StateRoot([0u8; 32]), StateRoot([1u8; 32]));
|
|
|
|
fn assert_send_sync<T: Send + Sync>(_: &T) {}
|
|
assert_send_sync(&batch);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Edge Cases and Boundary Tests
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_transfer_circuit_max_indices() {
|
|
let circuit = TransferCircuit::new(
|
|
StateRoot([0u8; 32]),
|
|
StateRoot([1u8; 32]),
|
|
u64::MAX,
|
|
u64::MAX - 1,
|
|
u64::MAX,
|
|
);
|
|
assert_eq!(circuit.sender_idx, u64::MAX);
|
|
assert_eq!(circuit.recipient_idx, u64::MAX - 1);
|
|
assert_eq!(circuit.amount, u64::MAX);
|
|
}
|
|
|
|
#[test]
|
|
fn test_batch_circuit_with_zero_transfers() {
|
|
let batch = BatchCircuit::new(StateRoot([0u8; 32]), StateRoot([0u8; 32]));
|
|
assert_eq!(batch.num_transfers(), 0);
|
|
assert_eq!(batch.initial_root, batch.final_root);
|
|
}
|
|
|
|
#[test]
|
|
fn test_empty_merkle_proofs() {
|
|
let circuit = TransferCircuit::new(StateRoot([0u8; 32]), StateRoot([1u8; 32]), 0, 1, 100)
|
|
.with_proofs(vec![], vec![]);
|
|
|
|
assert!(circuit.sender_proof.is_empty());
|
|
assert!(circuit.recipient_proof.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_empty_signature() {
|
|
let circuit = TransferCircuit::new(StateRoot([0u8; 32]), StateRoot([1u8; 32]), 0, 1, 100)
|
|
.with_signature(vec![]);
|
|
|
|
assert!(circuit.signature.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_large_merkle_proof() {
|
|
let large_proof: Vec<[u8; 32]> = (0..32).map(|i| [i as u8; 32]).collect();
|
|
let circuit = TransferCircuit::new(StateRoot([0u8; 32]), StateRoot([1u8; 32]), 0, 1, 100)
|
|
.with_proofs(large_proof.clone(), large_proof.clone());
|
|
|
|
assert_eq!(circuit.sender_proof.len(), 32);
|
|
assert_eq!(circuit.recipient_proof.len(), 32);
|
|
}
|
|
}
|