diff --git a/artifacts/program_methods/aggregator_circuit.bin b/artifacts/program_methods/aggregator_circuit.bin deleted file mode 100644 index a5be6ce1..00000000 Binary files a/artifacts/program_methods/aggregator_circuit.bin and /dev/null differ diff --git a/artifacts/program_methods/aggregator_circuit_strict.bin b/artifacts/program_methods/aggregator_circuit_strict.bin deleted file mode 100644 index 0aead6c3..00000000 Binary files a/artifacts/program_methods/aggregator_circuit_strict.bin and /dev/null differ diff --git a/bench_aggregator_cuda.sh b/bench_aggregator_cuda.sh deleted file mode 100755 index 75eaa4da..00000000 --- a/bench_aggregator_cuda.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env bash -# Benchmarks the aggregator circuit (core and strict variants) with CUDA acceleration. -# -# Fixtures must be generated first: -# cargo run --release -p ppe_test_data_gen -- --tx-output ppe_tx_fixtures.bin -# -# Usage: -# ./bench_aggregator_cuda.sh -# -# Environment: -# PPE_TX_FIXTURES — path to fixture file (default: ppe_tx_fixtures.bin) -# COUNTS — space-separated list of transaction counts (default: "1 3 5") - -set -euo pipefail - -# Point the build at CUDA 13.0 (required for Blackwell / compute_120). -export NVCC=/usr/local/cuda-13.0/bin/nvcc -export CUDA_HOME=/usr/local/cuda-13.0 -export PATH="/usr/local/cuda-13.0/bin:$PATH" - -FIXTURES="$(realpath "${PPE_TX_FIXTURES:-ppe_tx_fixtures.bin}")" -COUNTS="${COUNTS:-2 3 4 5 6 7 8 10 12 14 16}" -SEGMENT_LIMIT_PO2="${PPE_SEGMENT_LIMIT_PO2-19}" - -if [ ! -f "$FIXTURES" ]; then - echo "ERROR: fixture file '$FIXTURES' not found." - echo "Generate it first:" - echo " cargo run --release -p ppe_test_data_gen -- --tx-output $FIXTURES" - exit 1 -fi - -printf "\n%-6s %-8s %14s %20s\n" "n" "variant" "proving_ms" "proof_size_bytes" -printf "%-6s %-8s %14s %20s\n" "------" "--------" "--------------" "--------------------" - -run_bench() { - local count=$1 - local strict=$2 - local variant - variant=$([ "$strict" = "1" ] && echo "strict" || echo "core") - - local segment_limit_env=() - if [ -n "$SEGMENT_LIMIT_PO2" ]; then - segment_limit_env=(PPE_SEGMENT_LIMIT_PO2="$SEGMENT_LIMIT_PO2") - fi - - local line - line=$( - env \ - PPE_TX_FIXTURES="$FIXTURES" \ - AGGREGATOR_COUNT="$count" \ - AGGREGATOR_STRICT="$strict" \ - "${segment_limit_env[@]}" \ - cargo test -p lee --features cuda,prove bench_aggregator -- --nocapture 2>&1 \ - | grep "\[lee::analytics\] aggregator" || true - ) - - if [ -z "$line" ]; then - printf "%-6s %-8s %14s %20s\n" "$count" "$variant" "failed" "-" - return - fi - - local proving_ms proof_size - proving_ms=$(echo "$line" | grep -o 'proving_ms=[0-9]*' | cut -d= -f2) - proof_size=$(echo "$line" | grep -o 'proof_size_bytes=[0-9]*' | cut -d= -f2) - - printf "%-6s %-8s %14s %20s\n" "$count" "$variant" "$proving_ms" "$proof_size" -} - -for count in $COUNTS; do - run_bench "$count" "0" - run_bench "$count" "1" -done - -printf "\n" diff --git a/bench_sequencer_aggregator_cuda.sh b/bench_sequencer_aggregator_cuda.sh new file mode 100755 index 00000000..d8f18b40 --- /dev/null +++ b/bench_sequencer_aggregator_cuda.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Benchmarks the sequencer aggregator host/guest pair (sequencer_aggregator.rs) with CUDA +# acceleration. +# +# Test transactions are loaded from a cache (target/sequencer_aggregator_bench_transactions.bin, +# BENCH_MAX_TRANSACTIONS=8 by default); `AGGREGATOR_COUNT` truncates that cached set, so this +# script does NOT regenerate transactions. If the cache doesn't exist yet, generate it first +# (one-time cost, produces real, non-dev-mode PPE proofs): +# +# NVCC=/usr/local/cuda-13.0/bin/nvcc \ +# CUDA_HOME=/usr/local/cuda-13.0 \ +# PATH="/usr/local/cuda-13.0/bin:$PATH" \ +# PPE_SEGMENT_LIMIT_PO2=19 \ +# cargo test -p lee --features cuda,prove --lib \ +# sequencer_aggregator::tests::bench_sequencer_aggregator -- --nocapture +# +# Usage: +# ./bench_sequencer_aggregator_cuda.sh +# +# Environment: +# COUNTS — space-separated list of transaction counts (default: "2 4 8"); each +# must be <= BENCH_MAX_TRANSACTIONS in sequencer_aggregator.rs +# PPE_SEGMENT_LIMIT_PO2 — segment size limit (log2 cycles/segment) passed to the executor +# (default: 19) + +set -euo pipefail + +# Point the build at CUDA 13.0 (required for Blackwell / compute_120). +export NVCC=/usr/local/cuda-13.0/bin/nvcc +export CUDA_HOME=/usr/local/cuda-13.0 +export PATH="/usr/local/cuda-13.0/bin:$PATH" + +COUNTS="${COUNTS:-2 4 8}" +export PPE_SEGMENT_LIMIT_PO2="${PPE_SEGMENT_LIMIT_PO2-19}" + +printf "\n%-6s %14s %20s\n" "n" "proving_ms" "proof_size_bytes" +printf "%-6s %14s %20s\n" "------" "--------------" "--------------------" + +run_bench() { + local count=$1 + + local line + line=$( + AGGREGATOR_COUNT="$count" \ + cargo test -p lee --features cuda,prove --lib \ + sequencer_aggregator::tests::bench_sequencer_aggregator -- --nocapture 2>&1 \ + | grep "\[lee::analytics\] sequencer_aggregator" || true + ) + + if [ -z "$line" ]; then + printf "%-6s %14s %20s\n" "$count" "failed" "-" + return + fi + + local proving_ms proof_size + proving_ms=$(echo "$line" | grep -o 'proving_ms=[0-9]*' | cut -d= -f2) + proof_size=$(echo "$line" | grep -o 'proof_size_bytes=[0-9]*' | cut -d= -f2) + + printf "%-6s %14s %20s\n" "$count" "$proving_ms" "$proof_size" +} + +for count in $COUNTS; do + run_bench "$count" +done + +printf "\n" diff --git a/lee/state_machine/core/src/aggregator_circuit_io.rs b/lee/state_machine/core/src/aggregator_circuit_io.rs deleted file mode 100644 index a66ad67e..00000000 --- a/lee/state_machine/core/src/aggregator_circuit_io.rs +++ /dev/null @@ -1,35 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::{BlockId, PrivacyPreservingCircuitOutput, Timestamp, program::ProgramId}; - -/// Input to the aggregator circuit. -#[derive(Serialize, Deserialize)] -pub struct AggregatorCircuitInput { - /// Image ID of the privacy-preserving circuit. Passed as a runtime value so the - /// guest does not need a compile-time dependency on the image ID. - pub privacy_preserving_circuit_id: ProgramId, - pub block_id: BlockId, - pub timestamp: Timestamp, - pub circuit_outputs: Vec, -} - -/// Output committed to the journal by the aggregator circuit. -/// -/// Preserves the full `PrivacyPreservingCircuitOutput` for each transaction so observers -/// can perform state-dependent checks (nonces, commitment freshness, nullifier uniqueness) -/// independently. Only the individual proofs are dropped. -#[derive(Serialize, Deserialize)] -#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] -pub struct AggregatorCircuitOutput { - pub block_id: BlockId, - pub timestamp: Timestamp, - pub circuit_outputs: Vec, -} - -#[cfg(feature = "host")] -impl AggregatorCircuitOutput { - #[must_use] - pub fn to_bytes(&self) -> Vec { - bytemuck::cast_slice(&risc0_zkvm::serde::to_vec(self).unwrap()).to_vec() - } -} diff --git a/lee/state_machine/core/src/lib.rs b/lee/state_machine/core/src/lib.rs index ac9b64ac..336c991d 100644 --- a/lee/state_machine/core/src/lib.rs +++ b/lee/state_machine/core/src/lib.rs @@ -3,7 +3,6 @@ reason = "We prefer to group methods by functionality rather than by type for encoding" )] -pub use aggregator_circuit_io::{AggregatorCircuitInput, AggregatorCircuitOutput}; pub use circuit_io::{ InputAccountIdentity, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, }; @@ -14,15 +13,17 @@ pub use commitment::{ pub use encryption::{EncryptionScheme, SharedSecretKey}; pub use nullifier::{Identifier, Nullifier, NullifierPublicKey, NullifierSecretKey}; pub use program::PrivateAccountKind; +pub use sequencer_aggregator_io::SequencerAggregatorOutput; pub mod account; -mod aggregator_circuit_io; mod circuit_io; mod commitment; mod encoding; pub mod encryption; +pub mod message; mod nullifier; pub mod program; +mod sequencer_aggregator_io; #[cfg(feature = "host")] pub mod error; diff --git a/lee/state_machine/core/src/message.rs b/lee/state_machine/core/src/message.rs new file mode 100644 index 00000000..de900f79 --- /dev/null +++ b/lee/state_machine/core/src/message.rs @@ -0,0 +1,68 @@ +//! Guest-side mirror of `lee::privacy_preserving_transaction::message::{Message, +//! EncryptedAccountData}`. +//! +//! The aggregator guest cannot depend on the `lee` crate (it pulls in host-only +//! `risc0-zkvm`/`lee_core` features), so the host converts each transaction's `Message` into +//! this `lee_core`-resident mirror before writing it to the guest. The mirror omits `epk` +//! (the 1088-byte ML-KEM-768 ciphertext from `EphemeralPublicKey`): it isn't part of +//! [`PrivacyPreservingCircuitOutput`] and so plays no role in `env::verify`, and reading it +//! as `Vec` is costly enough to push the guest over a segment boundary. + +use serde::{Deserialize, Serialize}; + +use crate::{ + Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput, + account::{Account, AccountId, AccountWithMetadata, Nonce}, + encryption::Ciphertext, + program::{BlockValidityWindow, TimestampValidityWindow}, +}; + +/// Mirror of `lee::privacy_preserving_transaction::message::EncryptedAccountData`. +#[derive(Serialize, Deserialize)] +#[cfg_attr(any(feature = "host", test), derive(Debug, Clone, PartialEq, Eq))] +pub struct EncryptedAccountData { + pub ciphertext: Ciphertext, + pub view_tag: u8, +} + +/// Mirror of `lee::privacy_preserving_transaction::message::Message`. +#[derive(Serialize, Deserialize)] +#[cfg_attr(any(feature = "host", test), derive(Debug, Clone, PartialEq, Eq))] +pub struct Message { + pub public_account_ids: Vec, + pub nonces: Vec, + pub public_post_states: Vec, + pub encrypted_private_post_states: Vec, + pub new_commitments: Vec, + pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>, + pub block_validity_window: BlockValidityWindow, + pub timestamp_validity_window: TimestampValidityWindow, +} + +impl Message { + /// Reconstructs the `PrivacyPreservingCircuitOutput` this message corresponds to, given + /// the `public_pre_states` resolved for `public_account_ids` (same order). + /// + /// Mirrors `lee`'s `circuit_output_for_message`, minus the `public_pre_states` lookup + /// itself: the guest has no access to chain state, so the caller resolves pre-states and + /// passes them in directly. + #[must_use] + pub fn into_circuit_output( + self, + public_pre_states: Vec, + ) -> PrivacyPreservingCircuitOutput { + PrivacyPreservingCircuitOutput { + public_pre_states, + public_post_states: self.public_post_states, + ciphertexts: self + .encrypted_private_post_states + .into_iter() + .map(|data| data.ciphertext) + .collect(), + new_commitments: self.new_commitments, + new_nullifiers: self.new_nullifiers, + block_validity_window: self.block_validity_window, + timestamp_validity_window: self.timestamp_validity_window, + } + } +} diff --git a/lee/state_machine/core/src/sequencer_aggregator_io.rs b/lee/state_machine/core/src/sequencer_aggregator_io.rs new file mode 100644 index 00000000..2ffefb9c --- /dev/null +++ b/lee/state_machine/core/src/sequencer_aggregator_io.rs @@ -0,0 +1,12 @@ +use serde::{Deserialize, Serialize}; + +use crate::{BlockId, Timestamp, message::Message}; + +/// Output committed to the journal by the sequencer aggregator circuit. +#[derive(Serialize, Deserialize)] +#[cfg_attr(any(feature = "host", test), derive(Debug, Clone, PartialEq, Eq))] +pub struct SequencerAggregatorOutput { + pub block_id: BlockId, + pub timestamp: Timestamp, + pub messages: Vec, +} diff --git a/lee/state_machine/src/aggregator_circuit.rs b/lee/state_machine/src/aggregator_circuit.rs deleted file mode 100644 index 4478ca2d..00000000 --- a/lee/state_machine/src/aggregator_circuit.rs +++ /dev/null @@ -1,491 +0,0 @@ -//! Host-side aggregator circuit: batches multiple privacy-preserving circuit proofs into -//! a single aggregated proof. - -use borsh::{BorshDeserialize, BorshSerialize}; -use lee_core::{ - AggregatorCircuitInput, AggregatorCircuitOutput, BlockId, Commitment, Nullifier, - PrivacyPreservingCircuitOutput, Timestamp, account::AccountId, -}; -use risc0_zkvm::{ExecutorEnv, InnerReceipt, ProverOpts, Receipt, default_prover}; - -use crate::{ - PrivacyPreservingTransaction, V03State, - error::LeeError, - program_methods::PRIVACY_PRESERVING_CIRCUIT_ID, - validated_state_diff::circuit_output_for_message, -}; - -/// Proof produced by the aggregator circuit. -#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] -pub struct AggregatorProof(Vec); - -impl AggregatorProof { - #[must_use] - pub fn into_inner(self) -> Vec { - self.0 - } - - #[must_use] - pub const fn from_inner(inner: Vec) -> Self { - Self(inner) - } - - #[must_use] - pub fn is_valid_for(&self, output: &AggregatorCircuitOutput, circuit_id: [u32; 8]) -> bool { - verify_proof(&self.0, output, circuit_id) - } -} - -fn verify_proof( - proof_bytes: &[u8], - output: &AggregatorCircuitOutput, - circuit_id: [u32; 8], -) -> bool { - let Ok(inner) = borsh::from_slice::(proof_bytes) else { - return false; - }; - let receipt = Receipt::new(inner, output.to_bytes()); - receipt.verify(circuit_id).is_ok() -} - -/// Filters `input_txs` down to the subset that can be aggregated together in one batch. -/// -/// Each transaction is independently re-validated against `state` via -/// [`ValidatedStateDiff::from_privacy_preserving_transaction`] (signatures, nonces, validity -/// windows, proof, commitment/nullifier checks); transactions that fail this check are dropped. -/// -/// Surviving transactions are then checked against every other surviving transaction so far, -/// mirroring the cross-transaction `assert!`s in the aggregator guests -/// (`program_methods/guest/src/bin/aggregator_circuit{,_strict}/main.rs`): a transaction is -/// dropped if it: -/// - reuses a nullifier already spent by an earlier transaction in this batch, -/// - reuses a commitment already created by an earlier transaction in this batch, or -/// - updates a public account already updated by an earlier transaction in this batch. -/// -/// Returns the surviving transactions paired with the `PrivacyPreservingCircuitOutput` each -/// one's proof commits to, in input order. This filtering only depends on `state`, not on the -/// prover, so it can run anywhere `state` is available (e.g. ahead of batch construction). -#[must_use] -pub fn select_aggregatable_transactions( - input_txs: Vec, - state: &V03State, - _block_id: BlockId, - _timestamp: Timestamp, -) -> Vec<(PrivacyPreservingTransaction, PrivacyPreservingCircuitOutput)> { - let mut accepted = Vec::new(); - let mut seen_nullifiers: Vec = Vec::new(); - let mut seen_commitments: Vec = Vec::new(); - let mut seen_updated_account_ids: Vec = Vec::new(); - - for tx in input_txs { - /* - if let Err(e) = - ValidatedStateDiff::from_privacy_preserving_transaction(&tx, state, block_id, timestamp) - { - eprintln!("[DEBUG] tx dropped by from_privacy_preserving_transaction: {e}"); - continue; - }*/ - - let signer_account_ids = tx.signer_account_ids(); - let circuit_output = circuit_output_for_message(state, &tx.message, &signer_account_ids); - - let updated_account_ids = || { - circuit_output - .public_pre_states - .iter() - .zip(circuit_output.public_post_states.iter()) - .filter(|(pre_state, post_state)| pre_state.account != **post_state) - .map(|(pre_state, _)| pre_state.account_id) - }; - - let has_duplicate_nullifier = circuit_output - .new_nullifiers - .iter() - .any(|(nullifier, _)| seen_nullifiers.contains(nullifier)); - let has_duplicate_commitment = circuit_output - .new_commitments - .iter() - .any(|commitment| seen_commitments.contains(commitment)); - let has_duplicate_account_update = - updated_account_ids().any(|account_id| seen_updated_account_ids.contains(&account_id)); - - if has_duplicate_nullifier || has_duplicate_commitment || has_duplicate_account_update { - continue; - } - - seen_nullifiers.extend(circuit_output.new_nullifiers.iter().map(|(n, _)| *n)); - seen_commitments.extend(circuit_output.new_commitments.iter().cloned()); - seen_updated_account_ids.extend(updated_account_ids()); - - accepted.push((tx, circuit_output)); - } - - accepted -} - -/// Aggregates privacy-preserving circuit proofs into a single proof. -/// -/// `input_txs` is first filtered down via [`select_aggregatable_transactions`]; only the -/// surviving transactions are proven against. -/// -/// `elf` is the compiled aggregator circuit binary. Use -/// `lee::program_methods::AGGREGATOR_CIRCUIT_ELF` for the core circuit or -/// `AGGREGATOR_CIRCUIT_STRICT_ELF` for the strict variant. -pub fn aggregate( - block_id: BlockId, - timestamp: Timestamp, - input_txs: Vec, - state: &V03State, - elf: &[u8], - segment_limit_po2: Option, -) -> Result<(AggregatorCircuitOutput, AggregatorProof), LeeError> { - run_aggregator(block_id, timestamp, input_txs, state, elf, segment_limit_po2) -} - -fn run_aggregator( - block_id: BlockId, - timestamp: Timestamp, - input_txs: Vec, - state: &V03State, - elf: &[u8], - segment_limit_po2: Option, -) -> Result<(AggregatorCircuitOutput, AggregatorProof), LeeError> { - let mut env_builder = ExecutorEnv::builder(); - if let Some(po2) = segment_limit_po2 { - env_builder.segment_limit_po2(po2); - } - - let input_len = input_txs.len(); - let mut circuit_outputs = Vec::new(); - for (tx, circuit_output) in select_aggregatable_transactions(input_txs, state, block_id, timestamp) { - let inner = borsh::from_slice::(&tx.witness_set.proof.into_inner()) - .map_err(|e| LeeError::CircuitOutputDeserializationError(e.to_string()))?; - env_builder.add_assumption(Receipt::new(inner, circuit_output.to_bytes())); - circuit_outputs.push(circuit_output); - } - - eprintln!( - "[DEBUG] select_aggregatable_transactions: input_len={input_len} accepted={}", - circuit_outputs.len() - ); - - let input = AggregatorCircuitInput { - privacy_preserving_circuit_id: PRIVACY_PRESERVING_CIRCUIT_ID, - block_id, - timestamp, - circuit_outputs, - }; - - env_builder - .write(&input) - .map_err(|e| LeeError::CircuitProvingError(e.to_string()))?; - - let env = env_builder - .build() - .map_err(|e| LeeError::CircuitProvingError(e.to_string()))?; - - let prove_info = default_prover() - // TODO: succinct compresses all segments into one receipt via recursion — consider - // ProverOpts::composite() (no recursion, one receipt per segment) if proving speed - // matters more than proof size. - .prove_with_opts(env, elf, &ProverOpts::succinct()) - .map_err(|e| LeeError::CircuitProvingError(e.to_string()))?; - - let proof = AggregatorProof(borsh::to_vec(&prove_info.receipt.inner)?); - - let output: AggregatorCircuitOutput = prove_info - .receipt - .journal - .decode() - .map_err(|e| LeeError::CircuitOutputDeserializationError(e.to_string()))?; - - Ok((output, proof)) -} - -#[cfg(test)] -mod tests { - use lee_core::{BlockId, Timestamp}; - use test_program_methods::{PpeFixture, PpeTxFixtureBundle}; - - use super::aggregate; - use crate::{ - PrivacyPreservingTransaction, V03State, - program_methods::{ - AGGREGATOR_CIRCUIT_ELF, AGGREGATOR_CIRCUIT_ID, AGGREGATOR_CIRCUIT_STRICT_ELF, - AGGREGATOR_CIRCUIT_STRICT_ID, - }, - }; - - /// Benchmark: aggregate N pre-generated PPE transactions loaded from a fixture file. - /// - /// Generate fixtures first: - /// - /// ```sh - /// cargo run --release -p ppe_test_data_gen -- --tx-output ppe_tx_fixtures.bin - /// ``` - /// - /// Control via env vars: - /// - `PPE_TX_FIXTURES`: path to fixture file (default: `ppe_tx_fixtures.bin`). - /// - `AGGREGATOR_COUNT`: number of fixtures to use (default: all). - /// - `AGGREGATOR_STRICT`: set to "1" for the strict variant (default: core). - /// - /// Skips gracefully when the fixture file is absent. - /// - /// Output line (captured by `bench_aggregator_cuda.sh`): - /// `[lee::analytics] aggregator n=… variant=… proving_ms=… proof_size_bytes=…`. - #[test] - fn bench_aggregator() { - let path = std::env::var("PPE_TX_FIXTURES") - .unwrap_or_else(|_| "ppe_tx_fixtures.bin".to_owned()); - let Some(bundle) = PpeTxFixtureBundle::load_bundle(&path) else { - return; - }; - - let state: V03State = - borsh::from_slice(&bundle.state_bytes).expect("fixture state_bytes invalid"); - let mut transactions: Vec = bundle - .tx_bytes - .iter() - .map(|bytes| borsh::from_slice(bytes).expect("fixture tx_bytes invalid")) - .collect(); - - if transactions.is_empty() { - return; - } - - if let Ok(s) = std::env::var("AGGREGATOR_COUNT") { - let count: usize = s.parse().expect("AGGREGATOR_COUNT must be a number"); - transactions.truncate(count); - } - - let strict: bool = std::env::var("AGGREGATOR_STRICT") - .map(|s| s == "1" || s == "true") - .unwrap_or(false); - - let (elf, circuit_id) = if strict { - (AGGREGATOR_CIRCUIT_STRICT_ELF, AGGREGATOR_CIRCUIT_STRICT_ID) - } else { - (AGGREGATOR_CIRCUIT_ELF, AGGREGATOR_CIRCUIT_ID) - }; - - let block_id: BlockId = bundle.block_id; - let timestamp: Timestamp = bundle.timestamp; - let segment_limit_po2: Option = std::env::var("PPE_SEGMENT_LIMIT_PO2") - .ok() - .map(|s| s.parse().expect("PPE_SEGMENT_LIMIT_PO2 must be a number")); - - let n = transactions.len(); - let t0 = std::time::Instant::now(); - let (_, agg_proof) = aggregate( - block_id, - timestamp, - transactions, - &state, - elf, - segment_limit_po2, - ) - .expect("aggregation should succeed"); - let proving_ms = t0.elapsed().as_millis(); - - let variant = if strict { "strict" } else { "core" }; - let proof_size = agg_proof.into_inner().len(); - #[expect(clippy::print_stderr, reason = "benchmark result line consumed by tooling")] - { - eprintln!( - "[lee::analytics] aggregator n={n} variant={variant} proving_ms={proving_ms} proof_size_bytes={proof_size}", - ); - } - - let _ = circuit_id; - } - - /// Diagnostic: does `circuit_output_for_message`'s reconstruction of `public_pre_states[0]` - /// from a fresh `V03State::new_with_genesis_accounts` genesis state match the - /// `AccountWithMetadata` that `ppe_test_data_gen` originally fed to `execute_and_prove`? - /// No proving involved — pure host-side construction, to isolate whether the - /// "Invalid privacy preserving execution circuit proof" failure comes from this - /// reconstruction step. - #[test] - fn debug_circuit_output_for_message_pre_state_reconstruction() { - use lee_core::account::{Account, AccountId, AccountWithMetadata}; - - use crate::{PrivateKey, PublicKey, program::Program}; - - let program = Program::authenticated_transfer_program(); - let signing_key = PrivateKey::try_new([50_u8; 32]).expect("valid seed"); - let sender_account_id = AccountId::from(&PublicKey::new_from_private_key(&signing_key)); - - let original = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 110, - ..Account::default() - }, - true, - sender_account_id, - ); - - let state = V03State::new_with_genesis_accounts( - &[(sender_account_id, 110)], - vec![], - 1_700_000_000, - ); - let signer_account_ids = vec![sender_account_id]; - let reconstructed = AccountWithMetadata::new( - state.get_account_by_id(sender_account_id), - signer_account_ids.contains(&sender_account_id), - sender_account_id, - ); - - assert_eq!( - original, reconstructed, - "public_pre_states[0] reconstruction mismatch" - ); - } - - /// Diagnostic: for fixture index 0, does `circuit_output_for_message`'s reconstruction - /// (from `ppe_tx_fixtures.bin`'s state + transaction) match the actual - /// `PrivacyPreservingCircuitOutput` that was proven (from `ppe_fixtures.bin`)? - /// Field-by-field, to localize a journal mismatch if `proof.is_valid_for(...)` fails. - #[test] - fn debug_circuit_output_for_message_matches_proven_output() { - use lee_core::PrivacyPreservingCircuitOutput; - - use super::circuit_output_for_message; - - let fixtures_path = - std::env::var("PPE_FIXTURES").unwrap_or_else(|_| "ppe_fixtures.bin".to_owned()); - let fixtures = PpeFixture::load_bundle(&fixtures_path); - let Some(fixture) = fixtures.first() else { - return; - }; - let words: &[u32] = bytemuck::cast_slice(&fixture.output_bytes); - let original: PrivacyPreservingCircuitOutput = - risc0_zkvm::serde::from_slice(words).expect("output_bytes should decode"); - - let tx_path = std::env::var("PPE_TX_FIXTURES") - .unwrap_or_else(|_| "ppe_tx_fixtures.bin".to_owned()); - let Some(bundle) = PpeTxFixtureBundle::load_bundle(&tx_path) else { - return; - }; - let state: V03State = - borsh::from_slice(&bundle.state_bytes).expect("fixture state_bytes invalid"); - let tx: PrivacyPreservingTransaction = - borsh::from_slice(&bundle.tx_bytes[0]).expect("fixture tx_bytes invalid"); - let signer_account_ids = tx.signer_account_ids(); - let reconstructed = circuit_output_for_message(&state, &tx.message, &signer_account_ids); - - assert_eq!( - original.public_pre_states, reconstructed.public_pre_states, - "public_pre_states mismatch" - ); - assert_eq!( - original.public_post_states, reconstructed.public_post_states, - "public_post_states mismatch" - ); - assert_eq!( - original.ciphertexts, reconstructed.ciphertexts, - "ciphertexts mismatch" - ); - assert_eq!( - original.new_commitments, reconstructed.new_commitments, - "new_commitments mismatch" - ); - assert_eq!( - original.new_nullifiers, reconstructed.new_nullifiers, - "new_nullifiers mismatch" - ); - assert_eq!( - original.block_validity_window, reconstructed.block_validity_window, - "block_validity_window mismatch" - ); - assert_eq!( - original.timestamp_validity_window, reconstructed.timestamp_validity_window, - "timestamp_validity_window mismatch" - ); - assert_eq!(original, reconstructed, "full PrivacyPreservingCircuitOutput mismatch"); - } - - /// Diagnostic: run the real `from_privacy_preserving_transaction` check (signatures, - /// nonces, validity windows, proof verification, commitment/nullifier freshness) against - /// fixture index 0 of `ppe_tx_fixtures.bin`, in isolation, to see exactly which sub-check - /// fails (if any) for the current fixtures. - #[test] - fn debug_from_privacy_preserving_transaction_fixture0() { - use crate::validated_state_diff::ValidatedStateDiff; - - let tx_path = std::env::var("PPE_TX_FIXTURES") - .unwrap_or_else(|_| "ppe_tx_fixtures.bin".to_owned()); - let Some(bundle) = PpeTxFixtureBundle::load_bundle(&tx_path) else { - return; - }; - let state: V03State = - borsh::from_slice(&bundle.state_bytes).expect("fixture state_bytes invalid"); - let tx: PrivacyPreservingTransaction = - borsh::from_slice(&bundle.tx_bytes[0]).expect("fixture tx_bytes invalid"); - - let result = ValidatedStateDiff::from_privacy_preserving_transaction( - &tx, - &state, - bundle.block_id, - bundle.timestamp, - ); - match &result { - Ok(_) => eprintln!("[DEBUG] fixture 0: from_privacy_preserving_transaction Ok"), - Err(e) => eprintln!("[DEBUG] fixture 0: from_privacy_preserving_transaction Err: {e}"), - } - assert!(result.is_ok()); - } - - /// Diagnostic: drill into `Proof::is_valid_for` for fixture index 0 — does the proof - /// bytes blob even decode as `InnerReceipt`, does `receipt.verify(...)` succeed against - /// the reconstructed journal, and do the raw proof bytes match `ppe_fixtures.bin`'s - /// `proof_bytes` for the same index? - #[test] - fn debug_proof_is_valid_for_fixture0() { - use super::circuit_output_for_message; - - let tx_path = std::env::var("PPE_TX_FIXTURES") - .unwrap_or_else(|_| "ppe_tx_fixtures.bin".to_owned()); - let Some(bundle) = PpeTxFixtureBundle::load_bundle(&tx_path) else { - return; - }; - let state: V03State = - borsh::from_slice(&bundle.state_bytes).expect("fixture state_bytes invalid"); - let tx: PrivacyPreservingTransaction = - borsh::from_slice(&bundle.tx_bytes[0]).expect("fixture tx_bytes invalid"); - let signer_account_ids = tx.signer_account_ids(); - let reconstructed = circuit_output_for_message(&state, &tx.message, &signer_account_ids); - - let proof_bytes = tx.witness_set.proof.0.clone(); - eprintln!("[DEBUG] tx.witness_set.proof bytes len = {}", proof_bytes.len()); - - match borsh::from_slice::(&proof_bytes) { - Ok(inner) => { - eprintln!("[DEBUG] proof bytes decode as InnerReceipt: Ok"); - let receipt = risc0_zkvm::Receipt::new(inner, reconstructed.to_bytes()); - match receipt.verify(crate::program_methods::PRIVACY_PRESERVING_CIRCUIT_ID) { - Ok(()) => eprintln!("[DEBUG] receipt.verify: Ok"), - Err(e) => eprintln!("[DEBUG] receipt.verify: Err: {e}"), - } - } - Err(e) => eprintln!("[DEBUG] proof bytes decode as InnerReceipt: Err: {e}"), - } - - let fixtures_path = - std::env::var("PPE_FIXTURES").unwrap_or_else(|_| "ppe_fixtures.bin".to_owned()); - let fixtures = PpeFixture::load_bundle(&fixtures_path); - if let Some(fixture) = fixtures.first() { - eprintln!( - "[DEBUG] fixture.proof_bytes len = {}, tx proof bytes == fixture.proof_bytes: {}", - fixture.proof_bytes.len(), - proof_bytes == fixture.proof_bytes - ); - eprintln!( - "[DEBUG] fixture.output_bytes len = {}, reconstructed.to_bytes() == fixture.output_bytes: {}", - fixture.output_bytes.len(), - reconstructed.to_bytes() == fixture.output_bytes - ); - } - } -} diff --git a/lee/state_machine/src/lib.rs b/lee/state_machine/src/lib.rs index 8fc4768c..2a74ec9b 100644 --- a/lee/state_machine/src/lib.rs +++ b/lee/state_machine/src/lib.rs @@ -22,7 +22,6 @@ pub use state::{ }; pub use validated_state_diff::ValidatedStateDiff; -pub mod aggregator_circuit; pub mod encoding; pub mod error; mod merkle_tree; @@ -30,6 +29,7 @@ pub mod privacy_preserving_transaction; pub mod program; pub mod program_deployment_transaction; pub mod public_transaction; +pub mod sequencer_aggregator; mod signature; mod state; mod validated_state_diff; diff --git a/lee/state_machine/src/privacy_preserving_transaction/circuit.rs b/lee/state_machine/src/privacy_preserving_transaction/circuit.rs index e68c3424..e206193a 100644 --- a/lee/state_machine/src/privacy_preserving_transaction/circuit.rs +++ b/lee/state_machine/src/privacy_preserving_transaction/circuit.rs @@ -985,8 +985,7 @@ mod tests { env_builder.write(&PRIVACY_PRESERVING_CIRCUIT_ID).unwrap(); // Outputs are written once as a word-native `Vec<&PrivacyPreservingCircuitOutput>` - // (matching `aggregator_circuit`'s `AggregatorCircuitInput`) instead of N raw - // `Vec` journal buffers — see the ppe_aggregation guest for why. + // instead of N raw `Vec` journal buffers — see the ppe_aggregation guest for why. let outputs: Vec<&PrivacyPreservingCircuitOutput> = proofs.iter().map(|(o, _)| o).collect(); env_builder.write(&outputs).unwrap(); @@ -1062,8 +1061,7 @@ mod tests { env_builder.write(&PRIVACY_PRESERVING_CIRCUIT_ID).unwrap(); // Outputs are written once as a word-native `Vec` - // (matching `aggregator_circuit`'s `AggregatorCircuitInput`) instead of N raw - // `Vec` journal buffers — see the ppe_aggregation guest for why. + // instead of N raw `Vec` journal buffers — see the ppe_aggregation guest for why. let outputs: Vec = fixtures .iter() .map(|f| { diff --git a/lee/state_machine/src/sequencer_aggregator.rs b/lee/state_machine/src/sequencer_aggregator.rs new file mode 100644 index 00000000..40b1462c --- /dev/null +++ b/lee/state_machine/src/sequencer_aggregator.rs @@ -0,0 +1,474 @@ +//! TODO: redo-description (Marvin-pq) +//! Clean-slate host/guest pair for the sequencer's transaction aggregator. +//! +//! The guest cannot depend on the `lee` crate, so the host converts each transaction's +//! [`Message`] into its `lee_core`-resident mirror ([`lee_core::message::Message`]) and +//! writes it to the guest alongside the `public_pre_states` resolved for its +//! `public_account_ids`. The guest reconstructs the `PrivacyPreservingCircuitOutput` each +//! message corresponds to (`Message::into_circuit_output`), verifies the transaction's PPE +//! proof against it (as a recursive assumption added by the host), checks nullifier/ +//! commitment/account-update uniqueness and block/timestamp validity windows across the +//! batch, and commits the verified messages alongside `block_id`/`timestamp` as its journal. + +use lee_core::{ + BlockId, Commitment, Nullifier, PrivacyPreservingCircuitOutput, SequencerAggregatorOutput, + Timestamp, + account::{AccountId, AccountWithMetadata}, +}; +use risc0_zkvm::{ExecutorEnv, InnerReceipt, ProverOpts, Receipt, default_prover}; + +use crate::{ + PRIVACY_PRESERVING_CIRCUIT_ID, PrivacyPreservingTransaction, error::LeeError, + privacy_preserving_transaction::message::Message, +}; + +/// Converts `message` into its `lee_core`-resident mirror for the guest. `epk` is dropped +/// (see [`lee_core::message`]'s module docs). +fn to_aggregator_message(message: &Message) -> lee_core::message::Message { + lee_core::message::Message { + public_account_ids: message.public_account_ids.clone(), + nonces: message.nonces.clone(), + public_post_states: message.public_post_states.clone(), + encrypted_private_post_states: message + .encrypted_private_post_states + .iter() + .map(|data| lee_core::message::EncryptedAccountData { + ciphertext: data.ciphertext.clone(), + view_tag: data.view_tag, + }) + .collect(), + new_commitments: message.new_commitments.clone(), + new_nullifiers: message.new_nullifiers.clone(), + block_validity_window: message.block_validity_window, + timestamp_validity_window: message.timestamp_validity_window, + } +} + +/// Filters `input_txs` down to the subset that can be aggregated together in one batch. +/// +/// `public_pre_states[i]` must be the resolved `AccountWithMetadata` for each of +/// `input_txs[i].message().public_account_ids`, in the same order (i.e. the +/// `public_pre_states` of the `PrivacyPreservingCircuitOutput` `input_txs[i]`'s proof was +/// generated for). +/// +/// A transaction is dropped if it reuses a nullifier or commitment already claimed earlier in +/// the batch, or updates a public account already updated earlier in the batch. The guest +/// re-asserts these same checks as defense-in-depth. +/// +/// Returns the surviving transactions, each paired with its `lee_core`-resident message and +/// the `PrivacyPreservingCircuitOutput` its proof commits to, in input order. +fn select_aggregatable_transactions( + input_txs: Vec, + public_pre_states: Vec>, +) -> Vec<( + PrivacyPreservingTransaction, + lee_core::message::Message, + PrivacyPreservingCircuitOutput, +)> { + let mut accepted = Vec::new(); + let mut seen_nullifiers: Vec = Vec::new(); + let mut seen_commitments: Vec = Vec::new(); + let mut seen_updated_account_ids: Vec = Vec::new(); + + for (tx, pre_states) in input_txs.into_iter().zip(public_pre_states) { + let message = to_aggregator_message(tx.message()); + let circuit_output = message.clone().into_circuit_output(pre_states); + + let updated_account_ids: Vec = circuit_output + .public_pre_states + .iter() + .zip(&circuit_output.public_post_states) + .filter(|(pre_state, post_state)| pre_state.account != **post_state) + .map(|(pre_state, _)| pre_state.account_id) + .collect(); + + let has_duplicate_nullifier = circuit_output + .new_nullifiers + .iter() + .any(|(nullifier, _)| seen_nullifiers.contains(nullifier)); + let has_duplicate_commitment = circuit_output + .new_commitments + .iter() + .any(|commitment| seen_commitments.contains(commitment)); + let has_duplicate_account_update = updated_account_ids + .iter() + .any(|account_id| seen_updated_account_ids.contains(account_id)); + + if has_duplicate_nullifier || has_duplicate_commitment || has_duplicate_account_update { + continue; + } + + seen_nullifiers.extend(circuit_output.new_nullifiers.iter().map(|(n, _)| *n)); + seen_commitments.extend(circuit_output.new_commitments.iter().cloned()); + seen_updated_account_ids.extend(updated_account_ids); + + accepted.push((tx, message, circuit_output)); + } + + accepted +} + +/// Runs the sequencer aggregator guest over `input_txs`. +/// +/// `public_pre_states[i]` must be the resolved `AccountWithMetadata` for each of +/// `input_txs[i].message().public_account_ids`, in the same order (see +/// [`select_aggregatable_transactions`]). +/// +/// `input_txs` is first filtered down via [`select_aggregatable_transactions`]; only the +/// surviving transactions are proven against. For each surviving transaction, the host adds +/// its proof as a recursive assumption against the `PrivacyPreservingCircuitOutput` it +/// commits to. The guest redoes the same reconstruction from the mirrored message and +/// `public_pre_states`, verifies each proof via `env::verify`, checks nullifier/commitment/ +/// account-update uniqueness and `block_id`/`timestamp` validity windows across the batch, +/// and commits the verified messages alongside `block_id`/`timestamp` as the journal. +/// +/// `segment_limit_po2`, if set, caps each segment to `2^segment_limit_po2` cycles (see +/// [`risc0_zkvm::ExecutorEnvBuilder::segment_limit_po2`]). +/// +/// Returns the aggregated journal alongside the Borsh-encoded aggregation proof +/// (`prove_info.receipt.inner`). +pub fn aggregate( + input_txs: Vec, + public_pre_states: Vec>, + block_id: BlockId, + timestamp: Timestamp, + segment_limit_po2: Option, + elf: &[u8], +) -> Result<(SequencerAggregatorOutput, Vec), LeeError> { + crate::ensure!( + input_txs.len() == public_pre_states.len(), + LeeError::InvalidInput("transactions and public pre-states length mismatch".into()) + ); + + let accepted = select_aggregatable_transactions(input_txs, public_pre_states); + + let mut env_builder = ExecutorEnv::builder(); + if let Some(po2) = segment_limit_po2 { + env_builder.segment_limit_po2(po2); + } + + let mut messages = Vec::with_capacity(accepted.len()); + let mut verified_pre_states = Vec::with_capacity(accepted.len()); + for (tx, message, circuit_output) in accepted { + let proof_bytes = tx.witness_set().proof().clone().into_inner(); + let inner: InnerReceipt = borsh::from_slice(&proof_bytes)?; + + env_builder.add_assumption(Receipt::new(inner, circuit_output.to_bytes())); + messages.push(message); + verified_pre_states.push(circuit_output.public_pre_states); + } + + env_builder + .write(&PRIVACY_PRESERVING_CIRCUIT_ID) + .map_err(|e| LeeError::ProgramWriteInputFailed(e.to_string()))?; + env_builder + .write(&block_id) + .map_err(|e| LeeError::ProgramWriteInputFailed(e.to_string()))?; + env_builder + .write(×tamp) + .map_err(|e| LeeError::ProgramWriteInputFailed(e.to_string()))?; + env_builder + .write(&messages) + .map_err(|e| LeeError::ProgramWriteInputFailed(e.to_string()))?; + env_builder + .write(&verified_pre_states) + .map_err(|e| LeeError::ProgramWriteInputFailed(e.to_string()))?; + let env = env_builder.build().unwrap(); + + let prove_info = default_prover() + .prove_with_opts(env, elf, &ProverOpts::succinct()) + .map_err(|e| LeeError::CircuitProvingError(e.to_string()))?; + + let output = prove_info + .receipt + .journal + .decode() + .map_err(|e| LeeError::ProgramExecutionFailed(e.to_string()))?; + let proof = borsh::to_vec(&prove_info.receipt.inner)?; + + Ok((output, proof)) +} + +#[cfg(test)] +pub(crate) mod tests { + #![expect( + clippy::arithmetic_side_effects, + reason = "test transaction generator — deterministic index arithmetic and balance math" + )] + #![expect( + clippy::print_stderr, + reason = "test transaction cache reports hits/misses for local iteration" + )] + + use authenticated_transfer_core::Instruction; + use lee_core::{ + InputAccountIdentity, NullifierPublicKey, PrivacyPreservingCircuitOutput, + SharedSecretKey, + account::{Account, AccountId, AccountWithMetadata, Nonce}, + encryption::ViewingPublicKey, + }; + use risc0_zkvm::serde::from_slice; + use test_program_methods::SEQUENCER_AGGREGATOR_ELF; + + use super::*; + use crate::{ + PrivateKey, PublicKey, V03State, execute_and_prove, + privacy_preserving_transaction::{ + circuit::ProgramWithDependencies, message::Message, witness_set::WitnessSet, + }, + program::Program, + }; + + /// Block id and timestamp the generated transactions' validity windows and nonces are + /// proven/checked against. + const TIMESTAMP: u64 = 1_700_000_000; + + /// Derives a deterministic, valid `PrivateKey` for transaction index `i`. + /// + /// Only `seed[0]` and `seed[1]` vary; the remaining bytes are fixed at `50`, which keeps + /// the resulting 256-bit big-endian value comfortably below the secp256k1 curve order + /// for any `seed[0]`/`seed[1]`, so the key is always valid. + fn sender_signing_key(i: usize) -> PrivateKey { + let (lo, hi) = index_bytes(i); + let mut seed = [50_u8; 32]; + seed[0] = lo; + seed[1] = hi; + PrivateKey::try_new(seed).expect("deterministic seed should be a valid private key") + } + + /// Splits `i` into its low and high byte, for use as deterministic seed variation. + fn index_bytes(i: usize) -> (u8, u8) { + let lo = u8::try_from(i & 0xFF).expect("masked value fits in u8"); + let hi = u8::try_from((i >> 8) & 0xFF).expect("masked value fits in u8"); + (lo, hi) + } + + /// Generates `count` independent "public sender -> private recipient" transfers, each + /// proven through the privacy-preserving execution circuit, plus the genesis + /// `V03State` their sender accounts were proven against. + pub(crate) fn generate_test_transactions( + count: usize, + ) -> ( + V03State, + Vec, + Vec, + ) { + let program = Program::authenticated_transfer_program(); + let mut transactions = Vec::with_capacity(count); + let mut circuit_outputs = Vec::with_capacity(count); + let mut genesis_accounts = Vec::with_capacity(count); + + for i in 0..count { + let (lo, hi) = index_bytes(i); + + // ViewingPublicKey requires two independent 32-byte seed halves (d, z). + let mut d = [42_u8; 32]; + d[0] = lo; + d[1] = hi; + let mut z = [43_u8; 32]; + z[0] = lo; + z[1] = hi; + + // The message hash used for deterministic encapsulation; vary it per index. + let mut msg = [44_u8; 32]; + msg[0] = lo; + msg[1] = hi; + + let amount: u128 = 100; + let vpk = ViewingPublicKey::from_seed(&d, &z); + + // Recipient: fresh private account derived from this transaction's index. + let mut nsk = [41_u8; 32]; + nsk[0] = lo; + nsk[1] = hi; + let npk = NullifierPublicKey::from(&nsk); + let (ssk, epk) = SharedSecretKey::encapsulate_deterministic(&vpk, &msg, 0); + + // Sender: public account whose id is derived from a real signing key, so the + // transaction's signature matches `message.public_account_ids`. + let signing_key = sender_signing_key(i); + let sender_account_id = + AccountId::from(&PublicKey::new_from_private_key(&signing_key)); + + let sender = AccountWithMetadata::new( + Account { + program_owner: program.id(), + balance: amount + 10, + ..Account::default() + }, + true, + sender_account_id, + ); + let recipient = AccountWithMetadata::new( + Account::default(), + false, + AccountId::for_regular_private_account(&npk, 0), + ); + + let instruction = Program::serialize_instruction(Instruction::Transfer { amount }) + .expect("serialize instruction"); + + let (output, proof) = execute_and_prove( + vec![sender, recipient], + instruction, + vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivateUnauthorized { + npk, + ssk, + identifier: 0, + }, + ], + &ProgramWithDependencies::from(program.clone()), + ) + .expect("execute_and_prove"); + + // `output` is consumed by `try_from_circuit_output` below; keep its bytes so + // we can hand the same circuit output to `aggregate` alongside the tx. + let output_bytes = output.to_bytes(); + + let message = Message::try_from_circuit_output( + vec![sender_account_id], + vec![Nonce(0)], + vec![(npk, vpk, epk)], + output, + ) + .expect("build message from circuit output"); + let witness_set = WitnessSet::for_message(&message, proof, &[&signing_key]); + + transactions.push(PrivacyPreservingTransaction::new(message, witness_set)); + let output_words: &[u32] = bytemuck::cast_slice(&output_bytes); + circuit_outputs + .push(from_slice(output_words).expect("decode circuit output bytes")); + genesis_accounts.push((sender_account_id, amount + 10)); + } + + let state = V03State::new_with_genesis_accounts(&genesis_accounts, vec![], TIMESTAMP); + (state, transactions, circuit_outputs) + } + + /// Maximum number of transactions [`bench_sequencer_aggregator`] loads/generates. + /// + /// `AGGREGATOR_COUNT` truncates down from this, so bumping it regenerates + /// [`BENCH_TRANSACTIONS_CACHE_PATH`] (a one-time cost — see + /// [`load_or_generate_test_transactions`]). + const BENCH_MAX_TRANSACTIONS: usize = 8; + + /// Path to a Borsh-encoded cache of [`generate_test_transactions`]'s output, used by + /// [`bench_sequencer_aggregator`]. + /// + /// TEMPORARY: avoids re-running `execute_and_prove` on every test invocation while + /// this circuit is under active development. Remove this caching (and the cache + /// file) once the host/guest design has stabilized. + const BENCH_TRANSACTIONS_CACHE_PATH: &str = concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../target/sequencer_aggregator_bench_transactions.bin" + ); + + /// Like [`generate_test_transactions`], but reuses a Borsh-encoded cache from disk + /// when one matching `count` exists, instead of re-proving every transaction. + /// + /// `PrivacyPreservingCircuitOutput` doesn't derive Borsh, so it's cached via its + /// risc0-serde `to_bytes()` representation (one `Vec` per output) and decoded + /// back with `from_slice` on load, same as [`generate_test_transactions`] does for + /// the freshly-generated case. + pub(crate) fn load_or_generate_test_transactions( + count: usize, + cache_path: &str, + ) -> ( + V03State, + Vec, + Vec, + ) { + if let Ok(bytes) = std::fs::read(cache_path) { + if let Ok((state, transactions, output_bytes)) = + borsh::from_slice::<(V03State, Vec, Vec>)>( + &bytes, + ) + { + if transactions.len() == count { + eprintln!( + "[sequencer_aggregator] loaded {count} cached test transaction(s) from {cache_path}" + ); + let circuit_outputs = output_bytes + .iter() + .map(|bytes| { + let words: &[u32] = bytemuck::cast_slice(bytes); + from_slice(words).expect("decode cached circuit output") + }) + .collect(); + return (state, transactions, circuit_outputs); + } + } + } + + eprintln!( + "[sequencer_aggregator] generating {count} test transaction(s) (cache miss at {cache_path})" + ); + let (state, transactions, circuit_outputs) = generate_test_transactions(count); + let output_bytes: Vec> = circuit_outputs + .iter() + .map(PrivacyPreservingCircuitOutput::to_bytes) + .collect(); + let bytes = borsh::to_vec(&(&state, &transactions, &output_bytes)) + .expect("serialize test transactions"); + std::fs::write(cache_path, bytes).expect("write test transactions cache"); + (state, transactions, circuit_outputs) + } + + /// Benchmark: aggregate `AGGREGATOR_COUNT` (default: [`BENCH_MAX_TRANSACTIONS`]) PPE + /// transactions from the cached test transaction set. + /// + /// The cache is generated once for [`BENCH_MAX_TRANSACTIONS`] transactions; lower + /// `AGGREGATOR_COUNT` values truncate that set rather than regenerating it, so repeated + /// runs across different counts don't re-prove the underlying PPE transactions. + /// + /// Control via env vars: + /// - `AGGREGATOR_COUNT`: number of transactions to aggregate (default: + /// [`BENCH_MAX_TRANSACTIONS`]). + /// - `PPE_SEGMENT_LIMIT_PO2`: segment size limit (`log2` of cycles per segment) passed to + /// the executor. + /// + /// Output line (captured by `bench_sequencer_aggregator_cuda.sh`): + /// `[lee::analytics] sequencer_aggregator n=… proving_ms=… proof_size_bytes=…`. + #[test] + fn bench_sequencer_aggregator() { + let (_state, mut transactions, mut circuit_outputs) = load_or_generate_test_transactions( + BENCH_MAX_TRANSACTIONS, + BENCH_TRANSACTIONS_CACHE_PATH, + ); + + if let Ok(s) = std::env::var("AGGREGATOR_COUNT") { + let count: usize = s.parse().expect("AGGREGATOR_COUNT must be a number"); + transactions.truncate(count); + circuit_outputs.truncate(count); + } + + let public_pre_states: Vec> = circuit_outputs + .into_iter() + .map(|output| output.public_pre_states) + .collect(); + + let segment_limit_po2: Option = std::env::var("PPE_SEGMENT_LIMIT_PO2") + .ok() + .map(|s| s.parse().expect("PPE_SEGMENT_LIMIT_PO2 must be a number")); + + let n = transactions.len(); + let t0 = std::time::Instant::now(); + let (_output, proof) = aggregate( + transactions, + public_pre_states, + lee_core::GENESIS_BLOCK_ID, + TIMESTAMP, + segment_limit_po2, + SEQUENCER_AGGREGATOR_ELF, + ) + .expect("aggregate should succeed"); + let proving_ms = t0.elapsed().as_millis(); + let proof_size = proof.len(); + + eprintln!( + "[lee::analytics] sequencer_aggregator n={n} proving_ms={proving_ms} proof_size_bytes={proof_size}" + ); + } +} diff --git a/program_methods/guest/src/bin/aggregator_circuit/main.rs b/program_methods/guest/src/bin/aggregator_circuit/main.rs deleted file mode 100644 index 34e3d7bb..00000000 --- a/program_methods/guest/src/bin/aggregator_circuit/main.rs +++ /dev/null @@ -1,80 +0,0 @@ -//! Aggregator Circuit. -//! -//! Verifies N privacy-preserving circuit proofs and enforces: -//! - Intra-batch uniqueness of nullifiers and commitments. -//! - No public account is updated by more than one transaction in the batch. -//! -//! The full `PrivacyPreservingCircuitOutput` for each transaction is committed to the -//! journal so observers can perform state-dependent checks independently. - -use std::convert::Infallible; - -use lee_core::{AggregatorCircuitInput, AggregatorCircuitOutput}; -use risc0_zkvm::{guest::env, serde::to_vec}; - -fn main() { - let AggregatorCircuitInput { - privacy_preserving_circuit_id, - block_id, - timestamp, - circuit_outputs, - } = env::read(); - - for output in &circuit_outputs { - let output_words = - to_vec(output).expect("PrivacyPreservingCircuitOutput serialization should not fail"); - env::verify(privacy_preserving_circuit_id, &output_words) - .unwrap_or_else(|_: Infallible| unreachable!("Infallible error is never constructed")); - } - - // TEMPORARY: dedup checks (items 1-3) disabled for debugging — isolating proof - // verification (item 4) to narrow down a bench_aggregator failure at n=2. - /* - // Linear-scan dedup: batches are small (n is bounded), so a `Vec` + `contains` check - // avoids the per-element hashing cost of `HashSet` in the zkVM. - let mut seen_nullifiers: Vec = Vec::new(); - for output in &circuit_outputs { - for (nullifier, _) in &output.new_nullifiers { - assert!( - !seen_nullifiers.contains(nullifier), - "Duplicate nullifier across transactions in batch" - ); - seen_nullifiers.push(*nullifier); - } - } - - let mut seen_commitments: Vec = Vec::new(); - for output in &circuit_outputs { - for commitment in &output.new_commitments { - assert!( - !seen_commitments.contains(commitment), - "Duplicate commitment across transactions in batch" - ); - seen_commitments.push(commitment.clone()); - } - } - - let mut seen_updated_account_ids: Vec = Vec::new(); - for output in &circuit_outputs { - for (pre_state, post_state) in output - .public_pre_states - .iter() - .zip(output.public_post_states.iter()) - { - if pre_state.account != *post_state { - assert!( - !seen_updated_account_ids.contains(&pre_state.account_id), - "Public account updated by multiple transactions in batch" - ); - seen_updated_account_ids.push(pre_state.account_id); - } - } - } - */ - - env::commit(&AggregatorCircuitOutput { - block_id, - timestamp, - circuit_outputs, - }); -} diff --git a/program_methods/guest/src/bin/aggregator_circuit_strict/main.rs b/program_methods/guest/src/bin/aggregator_circuit_strict/main.rs deleted file mode 100644 index 8ded202e..00000000 --- a/program_methods/guest/src/bin/aggregator_circuit_strict/main.rs +++ /dev/null @@ -1,85 +0,0 @@ -//! Aggregator Circuit (Strict). -//! -//! Extends the core aggregator circuit with one additional check proven inside RISC0: -//! - Each transaction's validity window contains the provided `block_id` and `timestamp`. - -use std::convert::Infallible; - -use lee_core::{ - AggregatorCircuitInput, AggregatorCircuitOutput, Commitment, Nullifier, account::AccountId, -}; -use risc0_zkvm::{guest::env, serde::to_vec}; - -fn main() { - let AggregatorCircuitInput { - privacy_preserving_circuit_id, - block_id, - timestamp, - circuit_outputs, - } = env::read(); - - for output in &circuit_outputs { - let output_words = - to_vec(output).expect("PrivacyPreservingCircuitOutput serialization should not fail"); - env::verify(privacy_preserving_circuit_id, &output_words) - .unwrap_or_else(|_: Infallible| unreachable!("Infallible error is never constructed")); - } - - // Linear-scan dedup: batches are small (n is bounded), so a `Vec` + `contains` check - // avoids the per-element hashing cost of `HashSet` in the zkVM. - let mut seen_nullifiers: Vec = Vec::new(); - for output in &circuit_outputs { - for (nullifier, _) in &output.new_nullifiers { - assert!( - !seen_nullifiers.contains(nullifier), - "Duplicate nullifier across transactions in batch" - ); - seen_nullifiers.push(*nullifier); - } - } - - let mut seen_commitments: Vec = Vec::new(); - for output in &circuit_outputs { - for commitment in &output.new_commitments { - assert!( - !seen_commitments.contains(commitment), - "Duplicate commitment across transactions in batch" - ); - seen_commitments.push(commitment.clone()); - } - } - - for output in &circuit_outputs { - assert!( - output.block_validity_window.is_valid_for(block_id), - "Transaction block validity window does not include the block id" - ); - assert!( - output.timestamp_validity_window.is_valid_for(timestamp), - "Transaction timestamp validity window does not include the timestamp" - ); - } - - let mut seen_updated_account_ids: Vec = Vec::new(); - for output in &circuit_outputs { - for (pre_state, post_state) in output - .public_pre_states - .iter() - .zip(output.public_post_states.iter()) - { - if pre_state.account != *post_state { - assert!( - !seen_updated_account_ids.contains(&pre_state.account_id), - "Public account updated by multiple transactions in batch" - ); - seen_updated_account_ids.push(pre_state.account_id); - } - } - } - - env::commit(&AggregatorCircuitOutput { - block_id, - timestamp, - circuit_outputs, - }); -} diff --git a/test_program_methods/guest/src/bin/ppe_aggregation.rs b/test_program_methods/guest/src/bin/ppe_aggregation.rs index 5d8e887c..6dc4cf43 100644 --- a/test_program_methods/guest/src/bin/ppe_aggregation.rs +++ b/test_program_methods/guest/src/bin/ppe_aggregation.rs @@ -15,7 +15,7 @@ use risc0_zkvm::{guest::env, serde::to_vec}; /// circuit outputs from the single aggregated proof. /// /// Outputs are read once as a word-native `Vec<...>` and re-serialized per-output via -/// `to_vec()` for `env::verify`, mirroring `aggregator_circuit`. This replaced reading +/// `to_vec()` for `env::verify`. This replaced reading /// each journal as a raw `env::read::>()`: risc0's default serde deserializes /// `Vec` one byte at a time (each unpacked from a word), which costs more guest /// cycles than the word-native path. `to_vec(output)` and `output.to_bytes()` produce diff --git a/test_program_methods/guest/src/bin/sequencer_aggregator.rs b/test_program_methods/guest/src/bin/sequencer_aggregator.rs new file mode 100644 index 00000000..50ad7db0 --- /dev/null +++ b/test_program_methods/guest/src/bin/sequencer_aggregator.rs @@ -0,0 +1,106 @@ +use std::convert::Infallible; + +use lee_core::{ + BlockId, Commitment, Nullifier, SequencerAggregatorOutput, Timestamp, + account::{AccountId, AccountWithMetadata}, + message::Message, +}; +use risc0_zkvm::{guest::env, serde::to_vec}; + +/// Sequencer aggregator circuit. +/// +/// The host writes: +/// 1. The PPE circuit image ID (`[u32; 8]`) +/// 2. `block_id: BlockId` +/// 3. `timestamp: Timestamp` +/// 4. `Vec` — the `lee_core`-resident mirror of each transaction's `Message` +/// 5. `Vec>` — `public_pre_states` for each message's +/// `public_account_ids`, in the same order +/// +/// It also adds each transaction's PPE receipt as an assumption before running this guest. +/// +/// Checks: +/// 1. Nullifiers and commitments are unique across all messages in the batch. +/// 2. Each public account is updated by at most one transaction in the batch. +/// 3. `block_id`/`timestamp` fall within each message's validity window. +/// 4. Each message's PPE proof verifies (`Message::into_circuit_output` + `env::verify`). +/// The host filters out transactions that would fail any of these checks before +/// building this input, so failures here should never occur. +/// +/// Journal: [`SequencerAggregatorOutput`] — `block_id`, `timestamp`, and the verified +/// messages. +fn main() { + let ppe_image_id: [u32; 8] = env::read(); + let block_id: BlockId = env::read(); + let timestamp: Timestamp = env::read(); + let messages: Vec = env::read(); + let public_pre_states: Vec> = env::read(); + + assert_eq!( + messages.len(), + public_pre_states.len(), + "sequencer_aggregator: messages and public_pre_states length mismatch" + ); + + // 1. Nullifiers and commitments are unique across all messages in the batch. + let mut seen_nullifiers: Vec = Vec::new(); + let mut seen_commitments: Vec = Vec::new(); + for message in &messages { + for (nullifier, _) in &message.new_nullifiers { + assert!( + !seen_nullifiers.contains(nullifier), + "Duplicate nullifier across transactions in batch" + ); + seen_nullifiers.push(*nullifier); + } + for commitment in &message.new_commitments { + assert!( + !seen_commitments.contains(commitment), + "Duplicate commitment across transactions in batch" + ); + seen_commitments.push(commitment.clone()); + } + } + + // 2. Each public account is updated by at most one transaction in the batch. + let mut seen_updated_account_ids: Vec = Vec::new(); + for (message, pre_states) in messages.iter().zip(&public_pre_states) { + for (pre_state, post_state) in pre_states.iter().zip(&message.public_post_states) { + if pre_state.account != *post_state { + assert!( + !seen_updated_account_ids.contains(&pre_state.account_id), + "Public account updated by multiple transactions in batch" + ); + seen_updated_account_ids.push(pre_state.account_id); + } + } + } + + // 3. `block_id`/`timestamp` fall within each message's validity window. + for message in &messages { + assert!( + message.block_validity_window.is_valid_for(block_id), + "Transaction block validity window does not include the block id" + ); + assert!( + message.timestamp_validity_window.is_valid_for(timestamp), + "Transaction timestamp validity window does not include the timestamp" + ); + } + + let output = SequencerAggregatorOutput { + block_id, + timestamp, + messages, + }; + env::commit(&output); + + // 4. Each message's PPE proof verifies. + for (message, pre_states) in output.messages.into_iter().zip(public_pre_states) { + let circuit_output = message.into_circuit_output(pre_states); + let output_words = to_vec(&circuit_output) + .expect("PrivacyPreservingCircuitOutput serialization should not fail"); + env::verify(ppe_image_id, &output_words) + .unwrap_or_else(|_: Infallible| unreachable!("Infallible error is never constructed")); + } +} diff --git a/tools/cycle_bench/Cargo.toml b/tools/cycle_bench/Cargo.toml index 848c268d..398fa567 100644 --- a/tools/cycle_bench/Cargo.toml +++ b/tools/cycle_bench/Cargo.toml @@ -13,7 +13,6 @@ default = [] prove = ["lee/prove", "risc0-zkvm/prove"] cuda = ["lee/cuda", "risc0-zkvm/cuda"] ppe = ["prove"] -aggregator = ["ppe"] [dependencies] lee = { workspace = true } diff --git a/tools/cycle_bench/src/aggregator.rs b/tools/cycle_bench/src/aggregator.rs deleted file mode 100644 index 3619055d..00000000 --- a/tools/cycle_bench/src/aggregator.rs +++ /dev/null @@ -1,113 +0,0 @@ -//! Aggregator circuit bench module. -//! -//! Measures wall-clock time for batching N privacy-preserving circuit proofs into a -//! single aggregated proof, using both the core and strict aggregator variants. -//! -//! Reported metrics per (N, variant) pair: -//! - `pp_prove_ms`: time to generate the N pp-circuit proofs (context for total cost) -//! - `agg_prove_ms`: time to run `aggregate()` — the sequencer's batch proving step -//! - `agg_proof_bytes`: borsh-serialized `InnerReceipt` of the aggregated proof -//! - `pp_proof_bytes_per_tx`: same metric for one pp-proof, for size comparison -//! -//! Requires `--features aggregator` and a full build (aggregator ELFs must exist in -//! `artifacts/program_methods/`). - -#![allow( - dead_code, - reason = "Stubs are used when the `aggregator` feature is disabled." -)] - -use serde::Serialize; - -#[cfg(feature = "aggregator")] -mod agg_impl; - -#[derive(Debug, Serialize, Clone)] -pub struct AggregatorBenchResult { - pub label: String, - pub n_txs: usize, - pub strict: bool, - /// Total wall-clock time to generate all N pp-circuit proofs (ms). - pub pp_prove_ms: Option, - /// Wall-clock time for the `aggregate()` call alone (ms). - pub agg_prove_ms: Option, - /// borsh-serialized `InnerReceipt` length of the aggregated proof (bytes). - pub agg_proof_bytes: Option, - /// borsh-serialized `InnerReceipt` length of one pp-proof, for comparison (bytes). - pub pp_proof_bytes_per_tx: Option, - pub error: Option, -} - -#[cfg(not(feature = "aggregator"))] -#[must_use] -pub const fn run_all() -> Vec { - Vec::new() -} - -#[cfg(feature = "aggregator")] -#[must_use] -pub fn run_all() -> Vec { - let mut results = Vec::new(); - for n_txs in [1_usize, 3, 5] { - for strict in [false, true] { - let variant = if strict { "strict" } else { "core" }; - eprintln!("aggregator: {variant} n={n_txs}"); - results.push(agg_impl::run(n_txs, strict)); - } - } - results -} - -pub fn print_table(results: &[AggregatorBenchResult]) { - if results.is_empty() { - return; - } - let lw = results - .iter() - .map(|r| r.label.len()) - .max() - .unwrap_or(0) - .max("label".len()); - - println!( - "\n{:5} {:>22} {:>22} {:>12} {:>12} {}", - "label", - "n_txs", - "pp_prove_ms (s)", - "agg_prove_ms (s)", - "agg_bytes", - "pp_bytes/tx", - "error", - lw = lw, - ); - println!("{}", "-".repeat(lw + 85)); - for r in results { - let pp = fmt_ms(r.pp_prove_ms); - let ap = fmt_ms(r.agg_prove_ms); - let ab = r - .agg_proof_bytes - .map_or_else(|| "-".to_owned(), |n| n.to_string()); - let pb = r - .pp_proof_bytes_per_tx - .map_or_else(|| "-".to_owned(), |n| n.to_string()); - let e = r.error.as_deref().unwrap_or(""); - println!( - "{:5} {:>22} {:>22} {:>12} {:>12} {}", - r.label, - r.n_txs, - pp, - ap, - ab, - pb, - e, - lw = lw, - ); - } -} - -fn fmt_ms(ms: Option) -> String { - ms.map_or_else( - || "-".to_owned(), - |v| format!("{v:.1} ({:.1}s)", v / 1_000.0), - ) -} diff --git a/tools/cycle_bench/src/aggregator/agg_impl.rs b/tools/cycle_bench/src/aggregator/agg_impl.rs deleted file mode 100644 index 1961228c..00000000 --- a/tools/cycle_bench/src/aggregator/agg_impl.rs +++ /dev/null @@ -1,218 +0,0 @@ -//! Feature-gated implementation of aggregator circuit benches. -//! -//! ELFs are loaded at runtime from `artifacts/program_methods/` so this module -//! compiles even before a full RISC0 build. If the ELFs are not present, each -//! bench run reports an error rather than panicking. - -use std::{path::PathBuf, time::Instant}; - -use authenticated_transfer_core::Instruction; -use lee::{ - PrivacyPreservingTransaction, PrivateKey, PublicKey, V03State, - aggregator_circuit::aggregate, - execute_and_prove, - privacy_preserving_transaction::{ - circuit::ProgramWithDependencies, message::Message, witness_set::WitnessSet, - }, - program::Program, - program_methods::{AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID}, -}; -use lee_core::{ - BlockId, InputAccountIdentity, NullifierPublicKey, SharedSecretKey, Timestamp, - account::{Account, AccountId, AccountWithMetadata, Nonce}, - encryption::ViewingPublicKey, -}; -use risc0_zkvm::serde::to_vec; - -use super::AggregatorBenchResult; - -/// Loads an aggregator ELF from `artifacts/program_methods/{name}.bin` at runtime. -fn load_aggregator_elf(name: &str) -> anyhow::Result> { - let artifacts = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join("artifacts/program_methods"); - let path = artifacts.join(format!("{name}.bin")); - std::fs::read(&path).map_err(|e| { - anyhow::anyhow!( - "aggregator ELF not found at {}: {e}\n\ - Run a full RISC0 build (without RISC0_SKIP_BUILD=1) to generate it.", - path.display() - ) - }) -} - -/// Derives a deterministic, valid `PrivateKey` for sender `tag`. -/// -/// Only `seed[0]` varies; the remaining bytes are fixed at `50`, which keeps the -/// resulting 256-bit big-endian value comfortably below the secp256k1 curve order for -/// any `tag`, so the key is always valid. -fn sender_signing_key(tag: u8) -> PrivateKey { - let mut seed = [50_u8; 32]; - seed[0] = tag; - PrivateKey::try_new(seed).expect("deterministic seed should be a valid private key") -} - -/// Generates a public-to-private (shielded) auth-transfer pp-transaction. -/// -/// The sender is a public account whose id is derived from a real signing key, so the -/// resulting transaction's signature matches its `message.public_account_ids`; the -/// recipient is a fresh private account derived from `tag`. Distinct tags yield distinct -/// `npk` values → distinct commitments and nullifiers, so any number of these -/// transactions can be safely aggregated in one batch. -fn prove_shielded_transfer(tag: u8) -> anyhow::Result<(AccountId, PrivacyPreservingTransaction)> { - let nsk: [u8; 32] = [tag; 32]; - let d: [u8; 32] = [tag.wrapping_add(64); 32]; - let z: [u8; 32] = [tag.wrapping_add(128); 32]; - - let npk = NullifierPublicKey::from(&nsk); - let vpk = ViewingPublicKey::from_seed(&d, &z); - let (ssk, epk) = SharedSecretKey::encapsulate(&vpk); - - let recipient_account_id = AccountId::for_regular_private_account(&npk, 0); - - let signing_key = sender_signing_key(tag); - let sender_account_id = AccountId::from(&PublicKey::new_from_private_key(&signing_key)); - - let program = Program::new(AUTHENTICATED_TRANSFER_ELF.to_vec())?; - let pwd = ProgramWithDependencies::from(program); - - // Public sender with sufficient balance; unique account ID per tag so the - // strict aggregator's public-account-uniqueness check passes. - let sender = AccountWithMetadata { - account: Account { - program_owner: AUTHENTICATED_TRANSFER_ID, - balance: 1_000_000, - ..Account::default() - }, - is_authorized: true, - account_id: sender_account_id, - }; - - // Fresh private recipient account (zero balance, not yet authorized). - let recipient = AccountWithMetadata { - account: Account::default(), - is_authorized: false, - account_id: recipient_account_id, - }; - - let instruction_data = to_vec(&Instruction::Transfer { amount: 1_000 })?; - let identities = vec![ - InputAccountIdentity::Public, - InputAccountIdentity::PrivateUnauthorized { - npk, - ssk, - identifier: 0, - }, - ]; - - let (output, proof) = execute_and_prove(vec![sender, recipient], instruction_data, identities, &pwd)?; - - let message = Message::try_from_circuit_output( - vec![sender_account_id], - vec![Nonce(0)], - vec![(npk, vpk, epk)], - output, - )?; - let witness_set = WitnessSet::for_message(&message, proof, &[&signing_key]); - - Ok(( - sender_account_id, - PrivacyPreservingTransaction::new(message, witness_set), - )) -} - -pub fn run(n_txs: usize, strict: bool) -> AggregatorBenchResult { - let elf_name = if strict { - "aggregator_circuit_strict" - } else { - "aggregator_circuit" - }; - let label = format!( - "aggregator_{} n={n_txs}", - if strict { "strict" } else { "core" } - ); - - let elf = match load_aggregator_elf(elf_name) { - Ok(bytes) => bytes, - Err(e) => { - return AggregatorBenchResult { - label, - n_txs, - strict, - pp_prove_ms: None, - agg_prove_ms: None, - agg_proof_bytes: None, - pp_proof_bytes_per_tx: None, - error: Some(e.to_string()), - }; - } - }; - - // Generate N pp-transactions with distinct private recipients (tags 1..=N). - let pp_started = Instant::now(); - let txs: Result, anyhow::Error> = (0..n_txs) - .map(|i| prove_shielded_transfer(u8::try_from(i + 1).unwrap_or(u8::MAX))) - .collect(); - let pp_prove_ms = pp_started.elapsed().as_secs_f64() * 1_000.0; - - let txs = match txs { - Ok(t) => t, - Err(e) => { - return AggregatorBenchResult { - label, - n_txs, - strict, - pp_prove_ms: Some(pp_prove_ms), - agg_prove_ms: None, - agg_proof_bytes: None, - pp_proof_bytes_per_tx: None, - error: Some(e.to_string()), - }; - } - }; - - // Capture per-tx proof size before the vec is consumed by aggregate(). - let pp_proof_bytes_per_tx = txs - .first() - .map(|(_, tx)| tx.witness_set().proof().clone().into_inner().len()); - - let block_id: BlockId = 1; - let timestamp = Timestamp::from(1_700_000_000_u64); - - // Genesis state containing each sender's public account, matching the balance used - // when proving its transaction. - let genesis_accounts: Vec<(AccountId, u128)> = txs - .iter() - .map(|(account_id, _)| (*account_id, 1_000_000)) - .collect(); - let state = V03State::new_with_genesis_accounts(&genesis_accounts, vec![], timestamp); - let transactions: Vec = - txs.into_iter().map(|(_, tx)| tx).collect(); - - let agg_started = Instant::now(); - let result = aggregate(block_id, timestamp, transactions, &state, &elf, None); - let agg_prove_ms = agg_started.elapsed().as_secs_f64() * 1_000.0; - - match result { - Ok((_output, agg_proof)) => AggregatorBenchResult { - label, - n_txs, - strict, - pp_prove_ms: Some(pp_prove_ms), - agg_prove_ms: Some(agg_prove_ms), - agg_proof_bytes: Some(agg_proof.into_inner().len()), - pp_proof_bytes_per_tx, - error: None, - }, - Err(e) => AggregatorBenchResult { - label, - n_txs, - strict, - pp_prove_ms: Some(pp_prove_ms), - agg_prove_ms: Some(agg_prove_ms), - agg_proof_bytes: None, - pp_proof_bytes_per_tx, - error: Some(e.to_string()), - }, - } -} diff --git a/tools/cycle_bench/src/lib.rs b/tools/cycle_bench/src/lib.rs index f07148dd..97c93af8 100644 --- a/tools/cycle_bench/src/lib.rs +++ b/tools/cycle_bench/src/lib.rs @@ -18,13 +18,12 @@ ) )] #![cfg_attr( - any(feature = "ppe", feature = "aggregator"), + feature = "ppe", expect( clippy::print_stderr, - reason = "PPE/aggregator bench: eprintln progress messages" + reason = "PPE bench: eprintln progress messages" ) )] -pub mod aggregator; pub mod ppe; pub mod stats; diff --git a/tools/cycle_bench/src/main.rs b/tools/cycle_bench/src/main.rs index 7d509cc7..914d68c5 100644 --- a/tools/cycle_bench/src/main.rs +++ b/tools/cycle_bench/src/main.rs @@ -27,7 +27,7 @@ use clock_core::{ CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID, ClockAccountData, }; -use cycle_bench::{aggregator, ppe, stats::Stats}; +use cycle_bench::{ppe, stats::Stats}; use lee::program_methods::{ AMM_ELF, AMM_ID, ASSOCIATED_TOKEN_ACCOUNT_ELF, ASSOCIATED_TOKEN_ACCOUNT_ID, AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, CLOCK_ELF, CLOCK_ID, TOKEN_ELF, @@ -55,12 +55,6 @@ struct Cli { #[arg(long)] ppe: bool, - /// Also run aggregator circuit benches: batch N=1,3,5 pp-proofs through both the - /// core and strict aggregator circuits. Reports pp-prove time, agg-prove time, and - /// proof sizes. Requires --features aggregator at build time and a full RISC0 build. - #[arg(long)] - aggregator: bool, - /// Iterations for executor wall-time sampling per case. First iter is /// discarded as warmup, remaining N feed the stats. #[arg(long, default_value_t = 5)] @@ -521,25 +515,6 @@ fn main() -> Result<()> { ppe::print_table(&ppe_results); } - #[cfg(feature = "aggregator")] - let agg_results = if cli.aggregator { - aggregator::run_all() - } else { - Vec::new() - }; - #[cfg(not(feature = "aggregator"))] - let agg_results: Vec = { - if cli.aggregator { - eprintln!( - "cycle_bench: --aggregator requires --features aggregator at build time. Ignoring." - ); - } - Vec::new() - }; - if !agg_results.is_empty() { - aggregator::print_table(&agg_results); - } - let workspace_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("..") .join("..") @@ -551,7 +526,6 @@ fn main() -> Result<()> { let combined = serde_json::json!({ "standalone": results, "ppe": ppe_results, - "aggregator": agg_results, }); std::fs::write(&out_path, serde_json::to_string_pretty(&combined)?)?; println!("\nJSON written to {}", out_path.display());