diff --git a/artifacts/program_methods/aggregator_circuit.bin b/artifacts/program_methods/aggregator_circuit.bin index 3d1512fd..a5be6ce1 100644 Binary files a/artifacts/program_methods/aggregator_circuit.bin 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 index 3b962055..0aead6c3 100644 Binary files a/artifacts/program_methods/aggregator_circuit_strict.bin 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 e67b4320..47739129 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 fb07bd9b..73ac27bc 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 ecb5f959..42860d61 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 65f49719..0f4df26a 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 8d4c4a2d..30cd344a 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 97968c92..01a47654 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 2162e12b..29d68668 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 cd35ec2e..677408c6 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 d425a615..d5958e22 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 ff64472f..60406f75 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 34f4ea45..e0f97642 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 2f34e63a..d0d7b9af 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 95fd7895..6300a084 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 30ac4a93..bbb15d7f 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 e0e0ae67..1b9b950f 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 c94218ad..4cdea6d2 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 2a5c268e..70150e45 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 5e7de8b3..25d368d1 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 ca07de4d..0f24475f 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 f14ec53a..d86b8c41 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 e2e838d0..7f83a185 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 8aa5144d..531bbcd4 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 72102049..fd0d9e57 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 aa142150..ce8d6f60 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 280e352d..139844e8 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 4c9cdd90..43048e73 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 52fb9e46..3417e9ea 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 0c5b6c4f..3d4317bc 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 d8a4c38e..8d5c8d3a 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 4ab42062..d9dd744f 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 18f53f7a..979c7d86 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 c961ff56..ff29fe1a 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 a6d4e0c4..b3c3e1df 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 fbbb3c47..436207b2 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 63e67e20..cc683fab 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 b242d7f5..c476d50c 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 index 2fd55a6e..9216e687 100644 Binary files a/artifacts/test_program_methods/ppe_aggregation.bin 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 5365528f..1e39b068 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 046ded11..3a4f865a 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 5a44f6de..3a365b86 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 0cbf79a4..7a0643d6 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 a48e3964..26858993 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 cc5f1a82..f135117d 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 043a04bf..9291881d 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 index 067fd95a..75eaa4da 100755 --- a/bench_aggregator_cuda.sh +++ b/bench_aggregator_cuda.sh @@ -2,14 +2,14 @@ # 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 +# cargo run --release -p ppe_test_data_gen -- --tx-output ppe_tx_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") +# 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 @@ -18,14 +18,14 @@ 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}")" +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 -- --output $FIXTURES" + echo " cargo run --release -p ppe_test_data_gen -- --tx-output $FIXTURES" exit 1 fi @@ -46,7 +46,7 @@ run_bench() { local line line=$( env \ - PPE_FIXTURES="$FIXTURES" \ + PPE_TX_FIXTURES="$FIXTURES" \ AGGREGATOR_COUNT="$count" \ AGGREGATOR_STRICT="$strict" \ "${segment_limit_env[@]}" \ diff --git a/lee/state_machine/core/src/commitment.rs b/lee/state_machine/core/src/commitment.rs index 53df807a..f287526e 100644 --- a/lee/state_machine/core/src/commitment.rs +++ b/lee/state_machine/core/src/commitment.rs @@ -30,7 +30,15 @@ pub const DUMMY_COMMITMENT_HASH: [u8; 32] = [ ]; #[derive( - Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, BorshSerialize, + 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/nullifier.rs b/lee/state_machine/core/src/nullifier.rs index 46212efc..7c38249d 100644 --- a/lee/state_machine/core/src/nullifier.rs +++ b/lee/state_machine/core/src/nullifier.rs @@ -66,7 +66,16 @@ impl From<&NullifierSecretKey> for NullifierPublicKey { pub type NullifierSecretKey = [u8; 32]; #[derive( - Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, BorshSerialize, + 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 index 308e0000..4478ca2d 100644 --- a/lee/state_machine/src/aggregator_circuit.rs +++ b/lee/state_machine/src/aggregator_circuit.rs @@ -3,14 +3,16 @@ use borsh::{BorshDeserialize, BorshSerialize}; use lee_core::{ - AggregatorCircuitInput, AggregatorCircuitOutput, BlockId, PrivacyPreservingCircuitOutput, - Timestamp, + AggregatorCircuitInput, AggregatorCircuitOutput, BlockId, Commitment, Nullifier, + PrivacyPreservingCircuitOutput, Timestamp, account::AccountId, }; use risc0_zkvm::{ExecutorEnv, InnerReceipt, ProverOpts, Receipt, default_prover}; use crate::{ - error::LeeError, privacy_preserving_transaction::circuit::Proof, + PrivacyPreservingTransaction, V03State, + error::LeeError, program_methods::PRIVACY_PRESERVING_CIRCUIT_ID, + validated_state_diff::circuit_output_for_message, }; /// Proof produced by the aggregator circuit. @@ -46,7 +48,85 @@ fn verify_proof( receipt.verify(circuit_id).is_ok() } -/// Aggregates N privacy-preserving circuit proofs into a single proof. +/// 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 @@ -54,37 +134,41 @@ fn verify_proof( pub fn aggregate( block_id: BlockId, timestamp: Timestamp, - proofs: Vec<(PrivacyPreservingCircuitOutput, Proof)>, + input_txs: Vec, + state: &V03State, elf: &[u8], segment_limit_po2: Option, ) -> Result<(AggregatorCircuitOutput, AggregatorProof), LeeError> { - run_aggregator(block_id, timestamp, proofs, elf, segment_limit_po2) + run_aggregator(block_id, timestamp, input_txs, state, elf, segment_limit_po2) } fn run_aggregator( block_id: BlockId, timestamp: Timestamp, - proofs: Vec<(PrivacyPreservingCircuitOutput, Proof)>, + input_txs: Vec, + state: &V03State, 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()) + 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()))?; - let receipt = Receipt::new(inner, circuit_output.to_bytes()); - env_builder.add_assumption(receipt); + 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, @@ -120,45 +204,58 @@ fn run_aggregator( #[cfg(test)] mod tests { - use lee_core::{BlockId, PrivacyPreservingCircuitOutput, Timestamp}; - use test_program_methods::PpeFixture; + use lee_core::{BlockId, Timestamp}; + use test_program_methods::{PpeFixture, PpeTxFixtureBundle}; use super::aggregate; use crate::{ - privacy_preserving_transaction::circuit::Proof, + PrivacyPreservingTransaction, V03State, 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. + /// Benchmark: aggregate N pre-generated PPE transactions loaded from a fixture file. /// /// Generate fixtures first: - /// cargo run --release -p ppe_test_data_gen -- --output ppe_fixtures.bin + /// + /// ```sh + /// cargo run --release -p ppe_test_data_gen -- --tx-output ppe_tx_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) + /// - `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=… + /// 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); + 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; + }; - if fixtures.is_empty() { + 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"); - fixtures.truncate(count); + transactions.truncate(count); } let strict: bool = std::env::var("AGGREGATOR_STRICT") @@ -171,35 +268,224 @@ mod tests { (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 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, proofs, elf, segment_limit_po2).expect("aggregation should succeed"); + 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(); - eprintln!( - "[lee::analytics] aggregator n={} variant={variant} proving_ms={proving_ms} proof_size_bytes={proof_size}", - fixtures.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/privacy_preserving_transaction/circuit.rs b/lee/state_machine/src/privacy_preserving_transaction/circuit.rs index 858df477..e68c3424 100644 --- a/lee/state_machine/src/privacy_preserving_transaction/circuit.rs +++ b/lee/state_machine/src/privacy_preserving_transaction/circuit.rs @@ -860,7 +860,11 @@ mod tests { let (output_0, proof_0) = execute_and_prove( vec![ AccountWithMetadata::new( - Account { program_owner: program.id(), balance: 100, ..Account::default() }, + Account { + program_owner: program.id(), + balance: 100, + ..Account::default() + }, true, AccountId::new([0x01; 32]), ), @@ -870,9 +874,10 @@ mod tests { AccountId::for_regular_private_account(&keys_0.npk(), 0), ), ], - Program::serialize_instruction( - authenticated_transfer_core::Instruction::Transfer { amount: 10 }, - ).unwrap(), + Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer { + amount: 10, + }) + .unwrap(), vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivateUnauthorized { @@ -882,7 +887,8 @@ mod tests { }, ], &program.clone().into(), - ).expect("proof 0 should succeed"); + ) + .expect("proof 0 should succeed"); // ── Proof 1: public sender → private recipient ──────────────────────────── let keys_1 = test_private_account_keys_2(); @@ -890,7 +896,11 @@ mod tests { let (output_1, proof_1) = execute_and_prove( vec![ AccountWithMetadata::new( - Account { program_owner: program.id(), balance: 200, ..Account::default() }, + Account { + program_owner: program.id(), + balance: 200, + ..Account::default() + }, true, AccountId::new([0x02; 32]), ), @@ -900,9 +910,10 @@ mod tests { AccountId::for_regular_private_account(&keys_1.npk(), 0), ), ], - Program::serialize_instruction( - authenticated_transfer_core::Instruction::Transfer { amount: 20 }, - ).unwrap(), + Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer { + amount: 20, + }) + .unwrap(), vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivateUnauthorized { @@ -912,14 +923,19 @@ mod tests { }, ], &program.clone().into(), - ).expect("proof 1 should succeed"); + ) + .expect("proof 1 should succeed"); // ── Proof 2: fully private transfer ────────────────────────────────────── let sender_keys_2 = test_private_account_keys_1(); let recipient_keys_2 = test_private_account_keys_2(); let sender_2_id = AccountId::for_regular_private_account(&sender_keys_2.npk(), 0); - let sender_2_account = - Account { program_owner: program.id(), balance: 50, nonce: Nonce(1), ..Account::default() }; + let sender_2_account = Account { + program_owner: program.id(), + balance: 50, + nonce: Nonce(1), + ..Account::default() + }; let sender_2_commitment = Commitment::new(&sender_2_id, &sender_2_account); let mut cs = CommitmentSet::with_capacity(1); cs.extend(std::slice::from_ref(&sender_2_commitment)); @@ -937,9 +953,10 @@ mod tests { AccountId::for_regular_private_account(&recipient_keys_2.npk(), 1), ), ], - Program::serialize_instruction( - authenticated_transfer_core::Instruction::Transfer { amount: 30 }, - ).unwrap(), + Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer { + amount: 30, + }) + .unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { ssk: ssk_2_sender, @@ -954,11 +971,15 @@ mod tests { }, ], &program.into(), - ).expect("proof 2 should succeed"); + ) + .expect("proof 2 should succeed"); // ── Aggregate all three ─────────────────────────────────────────────────── - let proofs: Vec<(PrivacyPreservingCircuitOutput, Proof)> = - vec![(output_0, proof_0), (output_1, proof_1), (output_2, proof_2)]; + let proofs: Vec<(PrivacyPreservingCircuitOutput, Proof)> = vec![ + (output_0, proof_0), + (output_1, proof_1), + (output_2, proof_2), + ]; let mut env_builder = ExecutorEnv::builder(); env_builder.write(&PRIVACY_PRESERVING_CIRCUIT_ID).unwrap(); @@ -966,8 +987,7 @@ mod tests { // 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. - let outputs: Vec<&PrivacyPreservingCircuitOutput> = - proofs.iter().map(|(o, _)| o).collect(); + let outputs: Vec<&PrivacyPreservingCircuitOutput> = proofs.iter().map(|(o, _)| o).collect(); env_builder.write(&outputs).unwrap(); let journals: Vec> = proofs.iter().map(|(o, _)| o.to_bytes()).collect(); @@ -1014,8 +1034,7 @@ mod tests { use risc0_zkvm::{ExecutorEnv, InnerReceipt, ProverOpts, Receipt, default_prover}; use test_program_methods::{PPE_AGGREGATION_ELF, PPE_AGGREGATION_ID, PpeFixture}; - let path = - std::env::var("PPE_FIXTURES").unwrap_or_else(|_| "ppe_fixtures.bin".to_owned()); + 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() { @@ -1023,7 +1042,9 @@ mod tests { } if let Ok(count_str) = std::env::var("PPE_FIXTURES_COUNT") { - let count: usize = count_str.parse().expect("PPE_FIXTURES_COUNT must be a number"); + let count: usize = count_str + .parse() + .expect("PPE_FIXTURES_COUNT must be a number"); fixtures.truncate(count); } @@ -1032,7 +1053,9 @@ mod tests { // Smaller segments lower peak prover memory (handy on memory-constrained // GPUs) at the cost of more segments and overall proving time. if let Ok(po2_str) = std::env::var("PPE_SEGMENT_LIMIT_PO2") { - let po2: u32 = po2_str.parse().expect("PPE_SEGMENT_LIMIT_PO2 must be a number"); + let po2: u32 = po2_str + .parse() + .expect("PPE_SEGMENT_LIMIT_PO2 must be a number"); env_builder.segment_limit_po2(po2); } @@ -1064,16 +1087,19 @@ mod tests { let proving_ms = t0.elapsed().as_millis(); let proof_size = borsh::to_vec(&prove_info.receipt.inner).unwrap().len(); - eprintln!( - "[lee::analytics] ppe_aggregation n={} proving_ms={} proof_size_bytes={} \ - segments={} total_cycles={} user_cycles={}", - fixtures.len(), - proving_ms, - proof_size, - prove_info.stats.segments, - prove_info.stats.total_cycles, - prove_info.stats.user_cycles, - ); + #[expect(clippy::print_stderr, reason = "benchmark result line consumed by tooling")] + { + eprintln!( + "[lee::analytics] ppe_aggregation n={} proving_ms={} proof_size_bytes={} \ + segments={} total_cycles={} user_cycles={}", + fixtures.len(), + proving_ms, + proof_size, + prove_info.stats.segments, + prove_info.stats.total_cycles, + prove_info.stats.user_cycles, + ); + } prove_info .receipt @@ -1082,7 +1108,11 @@ mod tests { let recovered: Vec = prove_info.receipt.journal.decode().unwrap(); - assert_eq!(recovered.len(), fixtures.len(), "recovered output count mismatch"); + assert_eq!( + recovered.len(), + fixtures.len(), + "recovered output count mismatch" + ); } #[test] diff --git a/lee/state_machine/src/validated_state_diff.rs b/lee/state_machine/src/validated_state_diff.rs index cfbd1703..af5b9bc8 100644 --- a/lee/state_machine/src/validated_state_diff.rs +++ b/lee/state_machine/src/validated_state_diff.rs @@ -404,25 +404,10 @@ impl ValidatedStateDiff { LeeError::OutOfValidityWindow ); - // Build pre_states for proof verification - let public_pre_states: Vec<_> = message - .public_account_ids - .iter() - .map(|account_id| { - AccountWithMetadata::new( - state.get_account_by_id(*account_id), - signer_account_ids.contains(account_id), - *account_id, - ) - }) - .collect(); + let circuit_output = circuit_output_for_message(state, message, &signer_account_ids); // 4. Proof verification - check_privacy_preserving_circuit_proof_is_valid( - &witness_set.proof, - &public_pre_states, - message, - )?; + check_privacy_preserving_circuit_proof_is_valid(&witness_set.proof, &circuit_output)?; // 5. Commitment freshness state.check_commitments_are_new(&message.new_commitments)?; @@ -484,13 +469,27 @@ impl ValidatedStateDiff { } } -fn check_privacy_preserving_circuit_proof_is_valid( - proof: &Proof, - public_pre_states: &[AccountWithMetadata], +/// Reconstructs the `PrivacyPreservingCircuitOutput` a `Message` corresponds to, using `state` +/// to fill in `public_pre_states`. Used to verify the transaction's proof here, and reused by +/// the aggregator circuit to rebuild the journal each proof verifies against. +pub fn circuit_output_for_message( + state: &V03State, message: &Message, -) -> Result<(), LeeError> { - let output = PrivacyPreservingCircuitOutput { - public_pre_states: public_pre_states.to_vec(), + signer_account_ids: &[AccountId], +) -> PrivacyPreservingCircuitOutput { + let public_pre_states = message + .public_account_ids + .iter() + .map(|account_id| { + AccountWithMetadata::new( + state.get_account_by_id(*account_id), + signer_account_ids.contains(account_id), + *account_id, + ) + }) + .collect(); + PrivacyPreservingCircuitOutput { + public_pre_states, public_post_states: message.public_post_states.clone(), ciphertexts: message .encrypted_private_post_states @@ -502,9 +501,15 @@ fn check_privacy_preserving_circuit_proof_is_valid( new_nullifiers: message.new_nullifiers.clone(), block_validity_window: message.block_validity_window, timestamp_validity_window: message.timestamp_validity_window, - }; + } +} + +fn check_privacy_preserving_circuit_proof_is_valid( + proof: &Proof, + output: &PrivacyPreservingCircuitOutput, +) -> Result<(), LeeError> { proof - .is_valid_for(&output) + .is_valid_for(output) .then_some(()) .ok_or(LeeError::InvalidPrivacyPreservingProof) } diff --git a/ppe_fixtures.bin b/ppe_fixtures.bin new file mode 100644 index 00000000..3c0126be Binary files /dev/null and b/ppe_fixtures.bin differ diff --git a/ppe_tx_fixtures.bin b/ppe_tx_fixtures.bin new file mode 100644 index 00000000..0daf5cf9 Binary files /dev/null and b/ppe_tx_fixtures.bin differ diff --git a/program_methods/guest/src/bin/aggregator_circuit/main.rs b/program_methods/guest/src/bin/aggregator_circuit/main.rs index 2a99b792..34e3d7bb 100644 --- a/program_methods/guest/src/bin/aggregator_circuit/main.rs +++ b/program_methods/guest/src/bin/aggregator_circuit/main.rs @@ -9,7 +9,7 @@ use std::convert::Infallible; -use lee_core::{AggregatorCircuitInput, AggregatorCircuitOutput, Commitment, Nullifier, account::AccountId}; +use lee_core::{AggregatorCircuitInput, AggregatorCircuitOutput}; use risc0_zkvm::{guest::env, serde::to_vec}; fn main() { @@ -27,6 +27,9 @@ fn main() { .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(); @@ -53,8 +56,10 @@ fn main() { 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()) + for (pre_state, post_state) in output + .public_pre_states + .iter() + .zip(output.public_post_states.iter()) { if pre_state.account != *post_state { assert!( @@ -65,6 +70,7 @@ fn main() { } } } + */ env::commit(&AggregatorCircuitOutput { block_id, diff --git a/program_methods/guest/src/bin/aggregator_circuit_strict/main.rs b/program_methods/guest/src/bin/aggregator_circuit_strict/main.rs index 2b6ff9e3..8ded202e 100644 --- a/program_methods/guest/src/bin/aggregator_circuit_strict/main.rs +++ b/program_methods/guest/src/bin/aggregator_circuit_strict/main.rs @@ -62,8 +62,10 @@ fn main() { 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()) + for (pre_state, post_state) in output + .public_pre_states + .iter() + .zip(output.public_post_states.iter()) { if pre_state.account != *post_state { assert!( diff --git a/test_program_methods/src/fixtures.rs b/test_program_methods/src/fixtures.rs index aad5cb10..8408f6b3 100644 --- a/test_program_methods/src/fixtures.rs +++ b/test_program_methods/src/fixtures.rs @@ -23,6 +23,7 @@ impl PpeFixture { /// Returns an empty `Vec` (and prints a skip notice) when the file does not exist, /// so that test suites skip gracefully when fixtures have not been generated yet. /// Any other I/O error or deserialisation failure panics with a diagnostic message. + #[must_use] pub fn load_bundle(path: &str) -> Vec { if !std::path::Path::new(path).exists() { eprintln!( @@ -36,3 +37,47 @@ impl PpeFixture { borsh::from_slice(&bytes).expect("PPE fixture bundle failed Borsh deserialisation") } } + +/// A bundle of pre-generated `PrivacyPreservingTransaction`s plus the genesis `V03State` +/// they were proven against. +/// +/// Produced by `ppe_test_data_gen` and consumed by the aggregator circuit benchmark, so +/// that transaction proving is fully decoupled from aggregation. `state_bytes` and +/// `tx_bytes` are Borsh-encoded `lee::V03State` and `lee::PrivacyPreservingTransaction` +/// values respectively, kept as raw bytes so this crate doesn't need to depend on `lee`. +/// +/// Load a bundle with [`PpeTxFixtureBundle::load_bundle`]. +#[derive(BorshSerialize, BorshDeserialize)] +pub struct PpeTxFixtureBundle { + /// Block id the transactions' validity windows and nonces were proven against. + pub block_id: u64, + /// Timestamp the transactions' validity windows were proven against. + pub timestamp: u64, + /// Human-readable labels identifying each transaction's scenario, in `tx_bytes` order. + pub labels: Vec, + /// Borsh-encoded `V03State` containing the genesis sender accounts. + pub state_bytes: Vec, + /// Borsh-encoded `PrivacyPreservingTransaction`s, one per `labels` entry. + pub tx_bytes: Vec>, +} + +impl PpeTxFixtureBundle { + /// Loads a Borsh-encoded `PpeTxFixtureBundle` from `path`. + /// + /// Returns `None` (and prints a skip notice) when the file does not exist, so that + /// test suites skip gracefully when fixtures have not been generated yet. Any other + /// I/O error or deserialisation failure panics with a diagnostic message. + #[must_use] + pub fn load_bundle(path: &str) -> Option { + if !std::path::Path::new(path).exists() { + eprintln!( + "[test_program_methods] PPE tx fixture file '{path}' not found — skipping. \ + Run `RISC0_DEV_MODE=1 cargo run --release -p ppe_test_data_gen` to generate it." + ); + return None; + } + let bytes = std::fs::read(path) + .unwrap_or_else(|e| panic!("failed to read PPE tx fixture file '{path}': {e}")); + Some(borsh::from_slice(&bytes).expect("PPE tx fixture bundle failed Borsh deserialisation")) + } +} diff --git a/test_program_methods/src/lib.rs b/test_program_methods/src/lib.rs index 1607ca28..60ee7669 100644 --- a/test_program_methods/src/lib.rs +++ b/test_program_methods/src/lib.rs @@ -1,4 +1,9 @@ +#![expect( + clippy::print_stderr, + reason = "fixture loaders print a skip notice when fixture files are absent" +)] + +pub use fixtures::{PpeFixture, PpeTxFixtureBundle}; pub mod fixtures; -pub use fixtures::PpeFixture; include!(concat!(env!("OUT_DIR"), "/methods.rs")); diff --git a/tools/cycle_bench/src/aggregator.rs b/tools/cycle_bench/src/aggregator.rs index fd54396b..3619055d 100644 --- a/tools/cycle_bench/src/aggregator.rs +++ b/tools/cycle_bench/src/aggregator.rs @@ -106,5 +106,8 @@ pub fn print_table(results: &[AggregatorBenchResult]) { } fn fmt_ms(ms: Option) -> String { - ms.map_or_else(|| "-".to_owned(), |v| format!("{v:.1} ({:.1}s)", v / 1_000.0)) + 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 index eeadbfd8..1961228c 100644 --- a/tools/cycle_bench/src/aggregator/agg_impl.rs +++ b/tools/cycle_bench/src/aggregator/agg_impl.rs @@ -8,15 +8,18 @@ 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::{Proof, ProgramWithDependencies}, + 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}, + account::{Account, AccountId, AccountWithMetadata, Nonce}, encryption::ViewingPublicKey, }; use risc0_zkvm::serde::to_vec; @@ -38,24 +41,38 @@ fn load_aggregator_elf(name: &str) -> anyhow::Result> { }) } -/// Generates a public-to-private (shielded) auth-transfer pp-proof. +/// Derives a deterministic, valid `PrivateKey` for sender `tag`. /// -/// 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)> { +/// 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 (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); @@ -68,7 +85,7 @@ fn prove_shielded_transfer( ..Account::default() }, is_authorized: true, - account_id: AccountId::new([tag; 32]), + account_id: sender_account_id, }; // Fresh private recipient account (zero balance, not yet authorized). @@ -81,15 +98,27 @@ fn prove_shielded_transfer( let instruction_data = to_vec(&Instruction::Transfer { amount: 1_000 })?; let identities = vec![ InputAccountIdentity::Public, - InputAccountIdentity::PrivateUnauthorized { npk, ssk, identifier: 0 }, + InputAccountIdentity::PrivateUnauthorized { + npk, + ssk, + identifier: 0, + }, ]; - Ok(execute_and_prove( - vec![sender, recipient], - instruction_data, - identities, - &pwd, - )?) + 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 { @@ -115,19 +144,19 @@ pub fn run(n_txs: usize, strict: bool) -> AggregatorBenchResult { 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). + // Generate N pp-transactions with distinct private recipients (tags 1..=N). let pp_started = Instant::now(); - let proofs: Result, anyhow::Error> = (0..n_txs) + 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 proofs = match proofs { - Ok(p) => p, + let txs = match txs { + Ok(t) => t, Err(e) => { return AggregatorBenchResult { label, @@ -138,18 +167,30 @@ pub fn run(n_txs: usize, strict: bool) -> AggregatorBenchResult { 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 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, proofs, &elf, None); + 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 { diff --git a/tools/ppe_test_data_gen/src/main.rs b/tools/ppe_test_data_gen/src/main.rs index 560cee9f..c063d5ea 100644 --- a/tools/ppe_test_data_gen/src/main.rs +++ b/tools/ppe_test_data_gen/src/main.rs @@ -4,6 +4,10 @@ //! `to_bytes()`) and the raw `InnerReceipt` bytes (Borsh-encoded, from `Proof::into_inner()`). //! The whole bundle is a Borsh-encoded `Vec`. //! +//! Each proof is also wrapped into a `PrivacyPreservingTransaction` (signed by a real key) +//! together with the genesis `V03State` its sender accounts were proven against, and written +//! as a `PpeTxFixtureBundle` for the aggregator circuit's host-side pre-checks. +//! //! Keys are derived deterministically from the proof index so the fixture file is //! reproducible. //! # Usage @@ -49,15 +53,25 @@ use authenticated_transfer_core::Instruction; use borsh::{BorshDeserialize, BorshSerialize}; use clap::Parser; use lee::{ - execute_and_prove, privacy_preserving_transaction::circuit::ProgramWithDependencies, + PrivacyPreservingTransaction, PrivateKey, PublicKey, V03State, execute_and_prove, + privacy_preserving_transaction::{ + circuit::ProgramWithDependencies, + message::Message, + witness_set::WitnessSet, + }, program::Program, }; use lee_core::{ InputAccountIdentity, NullifierPublicKey, SharedSecretKey, - account::{Account, AccountId, AccountWithMetadata}, + account::{Account, AccountId, AccountWithMetadata, Nonce}, encryption::ViewingPublicKey, }; +/// Block id and timestamp the generated transactions' validity windows and nonces are +/// proven/checked against, matching the values used by `bench_aggregator`. +const BLOCK_ID: u64 = 1; +const TIMESTAMP: u64 = 1_700_000_000; + /// Mirror of `test_program_methods::PpeFixture`. Borsh field order must stay in sync. #[derive(BorshSerialize, BorshDeserialize)] struct PpeFixture { @@ -66,35 +80,59 @@ struct PpeFixture { proof_bytes: Vec, } +/// Mirror of `test_program_methods::PpeTxFixtureBundle`. Borsh field order must stay in sync. +#[derive(BorshSerialize, BorshDeserialize)] +struct PpeTxFixtureBundle { + block_id: u64, + timestamp: u64, + labels: Vec, + state_bytes: Vec, + tx_bytes: Vec>, +} + #[derive(Parser)] #[command( name = "ppe_test_data_gen", about = "Generate PPE proof fixtures for aggregation testing" )] struct Cli { - /// Output file path for the Borsh-serialised fixture bundle. + /// Output file path for the Borsh-serialised `Vec` bundle. #[arg(long, default_value = "ppe_fixtures.bin")] output: PathBuf, + /// Output file path for the Borsh-serialised `PpeTxFixtureBundle`. + #[arg(long, default_value = "ppe_tx_fixtures.bin")] + tx_output: PathBuf, + /// Number of independent PPE proofs to generate. #[arg(long, default_value_t = 16)] count: usize, } +/// Derives a deterministic, valid `PrivateKey` for proof 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 mut seed = [50_u8; 32]; + seed[0] = (i & 0xFF) as u8; + seed[1] = ((i >> 8) & 0xFF) as u8; + PrivateKey::try_new(seed).expect("deterministic seed should be a valid private key") +} + fn main() -> Result<()> { let cli = Cli::parse(); let program = Program::authenticated_transfer_program(); let mut fixtures: Vec = Vec::with_capacity(cli.count); + let mut tx_labels: Vec = Vec::with_capacity(cli.count); + let mut transactions: Vec = Vec::with_capacity(cli.count); + let mut genesis_accounts: Vec<(AccountId, u128)> = Vec::with_capacity(cli.count); for i in 0..cli.count { let lo = (i & 0xFF) as u8; let hi = ((i >> 8) & 0xFF) as u8; - // Non-zero bases ensure no key is accidentally all-zero. - let mut nsk = [41_u8; 32]; - nsk[0] = lo; - nsk[1] = hi; - // ViewingPublicKey requires two independent 32-byte seed halves (d, z). let mut d = [42_u8; 32]; d[0] = lo; @@ -112,14 +150,20 @@ fn main() -> Result<()> { let label = format!("public_to_private_{i}"); let vpk = ViewingPublicKey::from_seed(&d, &z); + + // Recipient: fresh private account derived from this proof's index. + let mut nsk = [41_u8; 32]; + nsk[0] = lo; + nsk[1] = hi; let npk = NullifierPublicKey::from(&nsk); // `encapsulate_deterministic` requires `lee_core` with `test_utils` feature. // The recipient output is at index 0 (the only private output in this scenario). - let (ssk, _epk) = SharedSecretKey::encapsulate_deterministic(&vpk, &msg, 0); + let (ssk, epk) = SharedSecretKey::encapsulate_deterministic(&vpk, &msg, 0); - let mut sender_seed = [45_u8; 32]; - sender_seed[0] = lo; - sender_seed[1] = hi; + // 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 { @@ -128,7 +172,7 @@ fn main() -> Result<()> { ..Account::default() }, true, - AccountId::new(sender_seed), + sender_account_id, ); let recipient = AccountWithMetadata::new( Account::default(), @@ -136,9 +180,8 @@ fn main() -> Result<()> { AccountId::for_regular_private_account(&npk, 0), ); - let instruction = - Program::serialize_instruction(Instruction::Transfer { amount }) - .context("serialise instruction")?; + let instruction = Program::serialize_instruction(Instruction::Transfer { amount }) + .context("serialise instruction")?; eprintln!( "[ppe_test_data_gen] ({}/{}) proving '{label}' ...", @@ -161,7 +204,7 @@ fn main() -> Result<()> { ) .with_context(|| format!("execute_and_prove for '{label}'"))?; - let proof_bytes = proof.into_inner(); + let proof_bytes = proof.clone().into_inner(); let output_bytes = output.to_bytes(); eprintln!( @@ -173,10 +216,23 @@ fn main() -> Result<()> { ); fixtures.push(PpeFixture { - label, + label: label.clone(), output_bytes, proof_bytes, }); + + let message = Message::try_from_circuit_output( + vec![sender_account_id], + vec![Nonce(0)], + vec![(npk, vpk, epk)], + output, + ) + .with_context(|| format!("build message for '{label}'"))?; + let witness_set = WitnessSet::for_message(&message, proof, &[&signing_key]); + + transactions.push(PrivacyPreservingTransaction::new(message, witness_set)); + genesis_accounts.push((sender_account_id, amount + 10)); + tx_labels.push(label); } let bundle = borsh::to_vec(&fixtures).context("serialise fixture bundle")?; @@ -189,5 +245,27 @@ fn main() -> Result<()> { cli.output.display(), ); + let state = V03State::new_with_genesis_accounts(&genesis_accounts, vec![], TIMESTAMP); + let tx_bundle = PpeTxFixtureBundle { + block_id: BLOCK_ID, + timestamp: TIMESTAMP, + labels: tx_labels, + state_bytes: borsh::to_vec(&state).context("serialise genesis state")?, + tx_bytes: transactions + .iter() + .map(borsh::to_vec) + .collect::>() + .context("serialise transactions")?, + }; + let tx_bundle_bytes = borsh::to_vec(&tx_bundle).context("serialise tx fixture bundle")?; + std::fs::write(&cli.tx_output, &tx_bundle_bytes).context("write tx output file")?; + + eprintln!( + "[ppe_test_data_gen] wrote {} transactions ({} bytes total) -> {}", + tx_bundle.tx_bytes.len(), + tx_bundle_bytes.len(), + cli.tx_output.display(), + ); + Ok(()) }