diff --git a/Cargo.lock b/Cargo.lock index 12b97d12..d2c4ccaa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4979,6 +4979,7 @@ dependencies = [ "authenticated_transfer_core", "borsh", "bridge_core", + "bytemuck", "clock_core", "env_logger", "faucet_core", diff --git a/artifacts/program_methods/aggregator_circuit.bin b/artifacts/program_methods/aggregator_circuit.bin new file mode 100644 index 00000000..f4de2438 Binary files /dev/null and b/artifacts/program_methods/aggregator_circuit.bin differ diff --git a/artifacts/program_methods/aggregator_circuit_strict.bin b/artifacts/program_methods/aggregator_circuit_strict.bin new file mode 100644 index 00000000..01355931 Binary files /dev/null and b/artifacts/program_methods/aggregator_circuit_strict.bin differ diff --git a/artifacts/program_methods/amm.bin b/artifacts/program_methods/amm.bin index 046f21bb..e67b4320 100644 Binary files a/artifacts/program_methods/amm.bin and b/artifacts/program_methods/amm.bin differ diff --git a/artifacts/program_methods/associated_token_account.bin b/artifacts/program_methods/associated_token_account.bin index cae6ed4e..fb07bd9b 100644 Binary files a/artifacts/program_methods/associated_token_account.bin and b/artifacts/program_methods/associated_token_account.bin differ diff --git a/artifacts/program_methods/authenticated_transfer.bin b/artifacts/program_methods/authenticated_transfer.bin index cebe1042..ecb5f959 100644 Binary files a/artifacts/program_methods/authenticated_transfer.bin and b/artifacts/program_methods/authenticated_transfer.bin differ diff --git a/artifacts/program_methods/bridge.bin b/artifacts/program_methods/bridge.bin index 9810c6ac..65f49719 100644 Binary files a/artifacts/program_methods/bridge.bin and b/artifacts/program_methods/bridge.bin differ diff --git a/artifacts/program_methods/clock.bin b/artifacts/program_methods/clock.bin index 1124913f..8d4c4a2d 100644 Binary files a/artifacts/program_methods/clock.bin and b/artifacts/program_methods/clock.bin differ diff --git a/artifacts/program_methods/faucet.bin b/artifacts/program_methods/faucet.bin index ca45b686..97968c92 100644 Binary files a/artifacts/program_methods/faucet.bin and b/artifacts/program_methods/faucet.bin differ diff --git a/artifacts/program_methods/pinata.bin b/artifacts/program_methods/pinata.bin index 118f19d3..2162e12b 100644 Binary files a/artifacts/program_methods/pinata.bin and b/artifacts/program_methods/pinata.bin differ diff --git a/artifacts/program_methods/pinata_token.bin b/artifacts/program_methods/pinata_token.bin index f3ecb0e9..cd35ec2e 100644 Binary files a/artifacts/program_methods/pinata_token.bin and b/artifacts/program_methods/pinata_token.bin differ diff --git a/artifacts/program_methods/privacy_preserving_circuit.bin b/artifacts/program_methods/privacy_preserving_circuit.bin index 66f6d5b6..d425a615 100644 Binary files a/artifacts/program_methods/privacy_preserving_circuit.bin and b/artifacts/program_methods/privacy_preserving_circuit.bin differ diff --git a/artifacts/program_methods/token.bin b/artifacts/program_methods/token.bin index a36fbbc8..ff64472f 100644 Binary files a/artifacts/program_methods/token.bin and b/artifacts/program_methods/token.bin differ diff --git a/artifacts/program_methods/vault.bin b/artifacts/program_methods/vault.bin index 7628b459..34f4ea45 100644 Binary files a/artifacts/program_methods/vault.bin and b/artifacts/program_methods/vault.bin differ diff --git a/artifacts/test_program_methods/auth_asserting_noop.bin b/artifacts/test_program_methods/auth_asserting_noop.bin index 3884195d..2f34e63a 100644 Binary files a/artifacts/test_program_methods/auth_asserting_noop.bin and b/artifacts/test_program_methods/auth_asserting_noop.bin differ diff --git a/artifacts/test_program_methods/auth_transfer_proxy.bin b/artifacts/test_program_methods/auth_transfer_proxy.bin index 6afda3a5..95fd7895 100644 Binary files a/artifacts/test_program_methods/auth_transfer_proxy.bin and b/artifacts/test_program_methods/auth_transfer_proxy.bin differ diff --git a/artifacts/test_program_methods/burner.bin b/artifacts/test_program_methods/burner.bin index dd2db03f..30ac4a93 100644 Binary files a/artifacts/test_program_methods/burner.bin and b/artifacts/test_program_methods/burner.bin differ diff --git a/artifacts/test_program_methods/chain_caller.bin b/artifacts/test_program_methods/chain_caller.bin index 58291896..e0e0ae67 100644 Binary files a/artifacts/test_program_methods/chain_caller.bin and b/artifacts/test_program_methods/chain_caller.bin differ diff --git a/artifacts/test_program_methods/changer_claimer.bin b/artifacts/test_program_methods/changer_claimer.bin index bb2feabb..c94218ad 100644 Binary files a/artifacts/test_program_methods/changer_claimer.bin and b/artifacts/test_program_methods/changer_claimer.bin differ diff --git a/artifacts/test_program_methods/claimer.bin b/artifacts/test_program_methods/claimer.bin index b0f3a67c..2a5c268e 100644 Binary files a/artifacts/test_program_methods/claimer.bin and b/artifacts/test_program_methods/claimer.bin differ diff --git a/artifacts/test_program_methods/clock_chain_caller.bin b/artifacts/test_program_methods/clock_chain_caller.bin index 5cda6717..5e7de8b3 100644 Binary files a/artifacts/test_program_methods/clock_chain_caller.bin and b/artifacts/test_program_methods/clock_chain_caller.bin differ diff --git a/artifacts/test_program_methods/data_changer.bin b/artifacts/test_program_methods/data_changer.bin index 898cea24..ca07de4d 100644 Binary files a/artifacts/test_program_methods/data_changer.bin and b/artifacts/test_program_methods/data_changer.bin differ diff --git a/artifacts/test_program_methods/extra_output.bin b/artifacts/test_program_methods/extra_output.bin index a74d931d..f14ec53a 100644 Binary files a/artifacts/test_program_methods/extra_output.bin and b/artifacts/test_program_methods/extra_output.bin differ diff --git a/artifacts/test_program_methods/faucet_chain_caller.bin b/artifacts/test_program_methods/faucet_chain_caller.bin index 8effbf85..e2e838d0 100644 Binary files a/artifacts/test_program_methods/faucet_chain_caller.bin and b/artifacts/test_program_methods/faucet_chain_caller.bin differ diff --git a/artifacts/test_program_methods/flash_swap_callback.bin b/artifacts/test_program_methods/flash_swap_callback.bin index e4aa09dc..8aa5144d 100644 Binary files a/artifacts/test_program_methods/flash_swap_callback.bin and b/artifacts/test_program_methods/flash_swap_callback.bin differ diff --git a/artifacts/test_program_methods/flash_swap_initiator.bin b/artifacts/test_program_methods/flash_swap_initiator.bin index f0d031b8..72102049 100644 Binary files a/artifacts/test_program_methods/flash_swap_initiator.bin and b/artifacts/test_program_methods/flash_swap_initiator.bin differ diff --git a/artifacts/test_program_methods/malicious_authorization_changer.bin b/artifacts/test_program_methods/malicious_authorization_changer.bin index 589bc620..aa142150 100644 Binary files a/artifacts/test_program_methods/malicious_authorization_changer.bin and b/artifacts/test_program_methods/malicious_authorization_changer.bin differ diff --git a/artifacts/test_program_methods/malicious_caller_program_id.bin b/artifacts/test_program_methods/malicious_caller_program_id.bin index 8c0d1350..280e352d 100644 Binary files a/artifacts/test_program_methods/malicious_caller_program_id.bin and b/artifacts/test_program_methods/malicious_caller_program_id.bin differ diff --git a/artifacts/test_program_methods/malicious_injector.bin b/artifacts/test_program_methods/malicious_injector.bin index 8bcccb4e..4c9cdd90 100644 Binary files a/artifacts/test_program_methods/malicious_injector.bin and b/artifacts/test_program_methods/malicious_injector.bin differ diff --git a/artifacts/test_program_methods/malicious_launderer.bin b/artifacts/test_program_methods/malicious_launderer.bin index 70392d9c..52fb9e46 100644 Binary files a/artifacts/test_program_methods/malicious_launderer.bin and b/artifacts/test_program_methods/malicious_launderer.bin differ diff --git a/artifacts/test_program_methods/malicious_self_program_id.bin b/artifacts/test_program_methods/malicious_self_program_id.bin index 62b8af74..0c5b6c4f 100644 Binary files a/artifacts/test_program_methods/malicious_self_program_id.bin and b/artifacts/test_program_methods/malicious_self_program_id.bin differ diff --git a/artifacts/test_program_methods/minter.bin b/artifacts/test_program_methods/minter.bin index c4b115ab..d8a4c38e 100644 Binary files a/artifacts/test_program_methods/minter.bin and b/artifacts/test_program_methods/minter.bin differ diff --git a/artifacts/test_program_methods/missing_output.bin b/artifacts/test_program_methods/missing_output.bin index ae599f60..4ab42062 100644 Binary files a/artifacts/test_program_methods/missing_output.bin and b/artifacts/test_program_methods/missing_output.bin differ diff --git a/artifacts/test_program_methods/modified_transfer.bin b/artifacts/test_program_methods/modified_transfer.bin index 1435dea9..18f53f7a 100644 Binary files a/artifacts/test_program_methods/modified_transfer.bin and b/artifacts/test_program_methods/modified_transfer.bin differ diff --git a/artifacts/test_program_methods/nonce_changer.bin b/artifacts/test_program_methods/nonce_changer.bin index bc479a80..c961ff56 100644 Binary files a/artifacts/test_program_methods/nonce_changer.bin and b/artifacts/test_program_methods/nonce_changer.bin differ diff --git a/artifacts/test_program_methods/noop.bin b/artifacts/test_program_methods/noop.bin index e0d52639..a6d4e0c4 100644 Binary files a/artifacts/test_program_methods/noop.bin and b/artifacts/test_program_methods/noop.bin differ diff --git a/artifacts/test_program_methods/pda_claimer.bin b/artifacts/test_program_methods/pda_claimer.bin index f34373ff..fbbb3c47 100644 Binary files a/artifacts/test_program_methods/pda_claimer.bin and b/artifacts/test_program_methods/pda_claimer.bin differ diff --git a/artifacts/test_program_methods/pda_spend_proxy.bin b/artifacts/test_program_methods/pda_spend_proxy.bin index 33e38f00..63e67e20 100644 Binary files a/artifacts/test_program_methods/pda_spend_proxy.bin and b/artifacts/test_program_methods/pda_spend_proxy.bin differ diff --git a/artifacts/test_program_methods/pinata_cooldown.bin b/artifacts/test_program_methods/pinata_cooldown.bin index 65879893..b242d7f5 100644 Binary files a/artifacts/test_program_methods/pinata_cooldown.bin and b/artifacts/test_program_methods/pinata_cooldown.bin differ diff --git a/artifacts/test_program_methods/ppe_aggregation.bin b/artifacts/test_program_methods/ppe_aggregation.bin new file mode 100644 index 00000000..1e0c5a20 Binary files /dev/null and b/artifacts/test_program_methods/ppe_aggregation.bin differ diff --git a/artifacts/test_program_methods/private_pda_delegator.bin b/artifacts/test_program_methods/private_pda_delegator.bin index 7652eaa7..5365528f 100644 Binary files a/artifacts/test_program_methods/private_pda_delegator.bin and b/artifacts/test_program_methods/private_pda_delegator.bin differ diff --git a/artifacts/test_program_methods/program_owner_changer.bin b/artifacts/test_program_methods/program_owner_changer.bin index 81d9a7e1..046ded11 100644 Binary files a/artifacts/test_program_methods/program_owner_changer.bin and b/artifacts/test_program_methods/program_owner_changer.bin differ diff --git a/artifacts/test_program_methods/simple_balance_transfer.bin b/artifacts/test_program_methods/simple_balance_transfer.bin index ef9b3006..5a44f6de 100644 Binary files a/artifacts/test_program_methods/simple_balance_transfer.bin and b/artifacts/test_program_methods/simple_balance_transfer.bin differ diff --git a/artifacts/test_program_methods/time_locked_transfer.bin b/artifacts/test_program_methods/time_locked_transfer.bin index ba50eebd..0cbf79a4 100644 Binary files a/artifacts/test_program_methods/time_locked_transfer.bin and b/artifacts/test_program_methods/time_locked_transfer.bin differ diff --git a/artifacts/test_program_methods/two_pda_claimer.bin b/artifacts/test_program_methods/two_pda_claimer.bin index 32a8b581..a48e3964 100644 Binary files a/artifacts/test_program_methods/two_pda_claimer.bin and b/artifacts/test_program_methods/two_pda_claimer.bin differ diff --git a/artifacts/test_program_methods/validity_window.bin b/artifacts/test_program_methods/validity_window.bin index 85a38041..cc5f1a82 100644 Binary files a/artifacts/test_program_methods/validity_window.bin and b/artifacts/test_program_methods/validity_window.bin differ diff --git a/artifacts/test_program_methods/validity_window_chain_caller.bin b/artifacts/test_program_methods/validity_window_chain_caller.bin index 99f379c5..043a04bf 100644 Binary files a/artifacts/test_program_methods/validity_window_chain_caller.bin and b/artifacts/test_program_methods/validity_window_chain_caller.bin differ diff --git a/bench_aggregator_cuda.sh b/bench_aggregator_cuda.sh new file mode 100755 index 00000000..067fd95a --- /dev/null +++ b/bench_aggregator_cuda.sh @@ -0,0 +1,74 @@ +#!/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 -- --output ppe_fixtures.bin +# +# Usage: +# ./bench_aggregator_cuda.sh +# +# Environment: +# PPE_FIXTURES — path to fixture file (default: ppe_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_FIXTURES:-ppe_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 -- --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_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/lee/state_machine/Cargo.toml b/lee/state_machine/Cargo.toml index e1dc4084..2449f778 100644 --- a/lee/state_machine/Cargo.toml +++ b/lee/state_machine/Cargo.toml @@ -32,6 +32,7 @@ risc0-binfmt = "3.0.2" [dev-dependencies] lee_core = { workspace = true, features = ["test_utils"] } +bytemuck.workspace = true token_core.workspace = true authenticated_transfer_core.workspace = true test_program_methods.workspace = true diff --git a/lee/state_machine/core/src/aggregator_circuit_io.rs b/lee/state_machine/core/src/aggregator_circuit_io.rs new file mode 100644 index 00000000..a66ad67e --- /dev/null +++ b/lee/state_machine/core/src/aggregator_circuit_io.rs @@ -0,0 +1,35 @@ +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/commitment.rs b/lee/state_machine/core/src/commitment.rs index 7c81c12c..53df807a 100644 --- a/lee/state_machine/core/src/commitment.rs +++ b/lee/state_machine/core/src/commitment.rs @@ -29,10 +29,9 @@ pub const DUMMY_COMMITMENT_HASH: [u8; 32] = [ 129, 241, 118, 39, 41, 253, 141, 171, 184, 71, 8, 41, ]; -#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)] -#[cfg_attr( - any(feature = "host", test), - derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord) +#[derive( + Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, BorshSerialize, + BorshDeserialize, )] pub struct Commitment(pub(super) [u8; 32]); diff --git a/lee/state_machine/core/src/lib.rs b/lee/state_machine/core/src/lib.rs index 466e1f5d..ac9b64ac 100644 --- a/lee/state_machine/core/src/lib.rs +++ b/lee/state_machine/core/src/lib.rs @@ -3,6 +3,7 @@ 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, }; @@ -15,6 +16,7 @@ pub use nullifier::{Identifier, Nullifier, NullifierPublicKey, NullifierSecretKe pub use program::PrivateAccountKind; pub mod account; +mod aggregator_circuit_io; mod circuit_io; mod commitment; mod encoding; diff --git a/lee/state_machine/core/src/nullifier.rs b/lee/state_machine/core/src/nullifier.rs index d1fbae42..46212efc 100644 --- a/lee/state_machine/core/src/nullifier.rs +++ b/lee/state_machine/core/src/nullifier.rs @@ -65,10 +65,9 @@ impl From<&NullifierSecretKey> for NullifierPublicKey { pub type NullifierSecretKey = [u8; 32]; -#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)] -#[cfg_attr( - any(feature = "host", test), - derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash) +#[derive( + Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, BorshSerialize, + BorshDeserialize, )] pub struct Nullifier(pub(super) [u8; 32]); diff --git a/lee/state_machine/src/aggregator_circuit.rs b/lee/state_machine/src/aggregator_circuit.rs new file mode 100644 index 00000000..308e0000 --- /dev/null +++ b/lee/state_machine/src/aggregator_circuit.rs @@ -0,0 +1,205 @@ +//! 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, PrivacyPreservingCircuitOutput, + Timestamp, +}; +use risc0_zkvm::{ExecutorEnv, InnerReceipt, ProverOpts, Receipt, default_prover}; + +use crate::{ + error::LeeError, privacy_preserving_transaction::circuit::Proof, + program_methods::PRIVACY_PRESERVING_CIRCUIT_ID, +}; + +/// 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() +} + +/// Aggregates N privacy-preserving circuit proofs into a single proof. +/// +/// `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, + proofs: Vec<(PrivacyPreservingCircuitOutput, Proof)>, + elf: &[u8], + segment_limit_po2: Option, +) -> Result<(AggregatorCircuitOutput, AggregatorProof), LeeError> { + run_aggregator(block_id, timestamp, proofs, elf, segment_limit_po2) +} + +fn run_aggregator( + block_id: BlockId, + timestamp: Timestamp, + proofs: Vec<(PrivacyPreservingCircuitOutput, Proof)>, + elf: &[u8], + segment_limit_po2: Option, +) -> Result<(AggregatorCircuitOutput, AggregatorProof), LeeError> { + // TODO: add host-side pre-checks before invoking the prover (e.g. no duplicate + // nullifiers/commitments, validity windows, public account uniqueness) so obviously + // invalid batches are rejected cheaply without spending GPU time. + let mut env_builder = ExecutorEnv::builder(); + if let Some(po2) = segment_limit_po2 { + env_builder.segment_limit_po2(po2); + } + let mut circuit_outputs = Vec::with_capacity(proofs.len()); + + for (circuit_output, proof) in proofs { + let inner = borsh::from_slice::(&proof.into_inner()) + .map_err(|e| LeeError::CircuitOutputDeserializationError(e.to_string()))?; + let receipt = Receipt::new(inner, circuit_output.to_bytes()); + env_builder.add_assumption(receipt); + circuit_outputs.push(circuit_output); + } + + 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, PrivacyPreservingCircuitOutput, Timestamp}; + use test_program_methods::PpeFixture; + + use super::aggregate; + use crate::{ + privacy_preserving_transaction::circuit::Proof, + program_methods::{ + AGGREGATOR_CIRCUIT_ELF, AGGREGATOR_CIRCUIT_ID, AGGREGATOR_CIRCUIT_STRICT_ELF, + AGGREGATOR_CIRCUIT_STRICT_ID, + }, + }; + + /// Benchmark: aggregate N pre-generated PPE proofs loaded from a fixture file. + /// + /// Generate fixtures first: + /// cargo run --release -p ppe_test_data_gen -- --output ppe_fixtures.bin + /// + /// Control via env vars: + /// PPE_FIXTURES — path to fixture file (default: ppe_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_FIXTURES").unwrap_or_else(|_| "ppe_fixtures.bin".to_owned()); + let mut fixtures = PpeFixture::load_bundle(&path); + + if fixtures.is_empty() { + return; + } + + if let Ok(s) = std::env::var("AGGREGATOR_COUNT") { + let count: usize = s.parse().expect("AGGREGATOR_COUNT must be a number"); + fixtures.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 proofs: Vec<(PrivacyPreservingCircuitOutput, Proof)> = fixtures + .iter() + .map(|f| { + let words: &[u32] = bytemuck::cast_slice(&f.output_bytes); + let output: PrivacyPreservingCircuitOutput = + risc0_zkvm::serde::from_slice(words).expect("fixture output_bytes invalid"); + let proof = Proof::from_inner(f.proof_bytes.clone()); + (output, proof) + }) + .collect(); + + let block_id: BlockId = 1; + let timestamp = Timestamp::from(1_700_000_000_u64); + 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 t0 = std::time::Instant::now(); + let (_, agg_proof) = + aggregate(block_id, timestamp, proofs, 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(); + eprintln!( + "[lee::analytics] aggregator n={} variant={variant} proving_ms={proving_ms} proof_size_bytes={proof_size}", + fixtures.len(), + ); + + let _ = circuit_id; + } +} diff --git a/lee/state_machine/src/lib.rs b/lee/state_machine/src/lib.rs index 129821b5..8fc4768c 100644 --- a/lee/state_machine/src/lib.rs +++ b/lee/state_machine/src/lib.rs @@ -22,6 +22,7 @@ pub use state::{ }; pub use validated_state_diff::ValidatedStateDiff; +pub mod aggregator_circuit; pub mod encoding; pub mod error; mod merkle_tree; diff --git a/program_methods/guest/src/bin/aggregator_circuit/main.rs b/program_methods/guest/src/bin/aggregator_circuit/main.rs new file mode 100644 index 00000000..d9261b23 --- /dev/null +++ b/program_methods/guest/src/bin/aggregator_circuit/main.rs @@ -0,0 +1,69 @@ +//! 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::{collections::HashSet, 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")); + } + + let mut seen_nullifiers: HashSet = HashSet::new(); + for output in &circuit_outputs { + for (nullifier, _) in &output.new_nullifiers { + assert!( + seen_nullifiers.insert(*nullifier), + "Duplicate nullifier across transactions in batch" + ); + } + } + + let mut seen_commitments: HashSet = HashSet::new(); + for output in &circuit_outputs { + for commitment in &output.new_commitments { + assert!( + seen_commitments.insert(commitment.clone()), + "Duplicate commitment across transactions in batch" + ); + } + } + + let mut seen_updated_account_ids: HashSet = HashSet::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.insert(pre_state.account_id), + "Public account updated by multiple transactions in batch" + ); + } + } + } + + 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 new file mode 100644 index 00000000..3232336a --- /dev/null +++ b/program_methods/guest/src/bin/aggregator_circuit_strict/main.rs @@ -0,0 +1,78 @@ +//! 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::{collections::HashSet, 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")); + } + + let mut seen_nullifiers: HashSet = HashSet::new(); + for output in &circuit_outputs { + for (nullifier, _) in &output.new_nullifiers { + assert!( + seen_nullifiers.insert(*nullifier), + "Duplicate nullifier across transactions in batch" + ); + } + } + + let mut seen_commitments: HashSet = HashSet::new(); + for output in &circuit_outputs { + for commitment in &output.new_commitments { + assert!( + seen_commitments.insert(commitment.clone()), + "Duplicate commitment across transactions in batch" + ); + } + } + + 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: HashSet = HashSet::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.insert(pre_state.account_id), + "Public account updated by multiple transactions in batch" + ); + } + } + } + + env::commit(&AggregatorCircuitOutput { + block_id, + timestamp, + circuit_outputs, + }); +} diff --git a/programs/sequencer_aggregation_circuit/core/Cargo.toml b/programs/sequencer_aggregation_circuit/core/Cargo.toml new file mode 100644 index 00000000..cb9a16bf --- /dev/null +++ b/programs/sequencer_aggregation_circuit/core/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "sequencer_aggregation_circuit_core" +version = "0.1.0" +edition = "2024" +license = { workspace = true } + +[lints] +workspace = true + +[dependencies] +lee_core.workspace = true +borsh.workspace = true diff --git a/tools/cycle_bench/Cargo.toml b/tools/cycle_bench/Cargo.toml index 03491c98..848c268d 100644 --- a/tools/cycle_bench/Cargo.toml +++ b/tools/cycle_bench/Cargo.toml @@ -11,7 +11,9 @@ workspace = true [features] 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 new file mode 100644 index 00000000..fd54396b --- /dev/null +++ b/tools/cycle_bench/src/aggregator.rs @@ -0,0 +1,110 @@ +//! 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 new file mode 100644 index 00000000..eeadbfd8 --- /dev/null +++ b/tools/cycle_bench/src/aggregator/agg_impl.rs @@ -0,0 +1,177 @@ +//! 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::{ + aggregator_circuit::aggregate, + execute_and_prove, + privacy_preserving_transaction::circuit::{Proof, ProgramWithDependencies}, + program::Program, + program_methods::{AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID}, +}; +use lee_core::{ + BlockId, InputAccountIdentity, NullifierPublicKey, SharedSecretKey, Timestamp, + account::{Account, AccountId, AccountWithMetadata}, + 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() + ) + }) +} + +/// Generates a public-to-private (shielded) auth-transfer pp-proof. +/// +/// The sender is a public account; 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 proofs can be safely aggregated in one batch. +fn prove_shielded_transfer( + tag: u8, +) -> anyhow::Result<(lee_core::PrivacyPreservingCircuitOutput, Proof)> { + 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 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: AccountId::new([tag; 32]), + }; + + // 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 }, + ]; + + Ok(execute_and_prove( + vec![sender, recipient], + instruction_data, + identities, + &pwd, + )?) +} + +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-proofs with distinct private recipients (tags 1..=N). + let pp_started = Instant::now(); + let proofs: 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 proofs = match proofs { + Ok(p) => p, + 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 = proofs.first().map(|(_, p)| p.clone().into_inner().len()); + + let block_id: BlockId = 1; + let timestamp = Timestamp::from(1_700_000_000_u64); + + let agg_started = Instant::now(); + let result = aggregate(block_id, timestamp, proofs, &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 7091cf84..f07148dd 100644 --- a/tools/cycle_bench/src/lib.rs +++ b/tools/cycle_bench/src/lib.rs @@ -14,10 +14,17 @@ feature = "ppe", expect( clippy::arbitrary_source_item_ordering, + reason = "PPE module: re-export ordering trips strict lints" + ) +)] +#![cfg_attr( + any(feature = "ppe", feature = "aggregator"), + expect( clippy::print_stderr, - reason = "PPE module: re-export ordering and eprintln progress trip strict lints" + reason = "PPE/aggregator 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 914d68c5..7d509cc7 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::{ppe, stats::Stats}; +use cycle_bench::{aggregator, 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,6 +55,12 @@ 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)] @@ -515,6 +521,25 @@ 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("..") @@ -526,6 +551,7 @@ 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());