diff --git a/Cargo.lock b/Cargo.lock index 1c49d7e7..856e038b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1971,6 +1971,24 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "cycle_bench" +version = "0.1.0" +dependencies = [ + "amm_core", + "anyhow", + "ata_core", + "borsh", + "clap", + "clock_core", + "nssa", + "nssa_core", + "risc0-zkvm", + "serde", + "serde_json", + "token_core", +] + [[package]] name = "darling" version = "0.20.11" diff --git a/tools/cycle_bench/src/main.rs b/tools/cycle_bench/src/main.rs index b87bdb3e..6f254868 100644 --- a/tools/cycle_bench/src/main.rs +++ b/tools/cycle_bench/src/main.rs @@ -7,50 +7,24 @@ //! Run with `cargo run --release -p cycle_bench`. `RISC0_DEV_MODE` has no effect on //! executor cycle counts. -#![allow( - clippy::arbitrary_source_item_ordering, +#![expect( clippy::arithmetic_side_effects, clippy::as_conversions, clippy::cast_precision_loss, - clippy::doc_markdown, clippy::float_arithmetic, - clippy::ignored_unit_patterns, - clippy::items_after_statements, - clippy::let_underscore_must_use, - clippy::let_underscore_untyped, - clippy::map_unwrap_or, clippy::missing_const_for_fn, - clippy::missing_docs_in_private_items, - clippy::module_inception, - clippy::module_name_repetitions, clippy::needless_pass_by_value, - clippy::no_effect_underscore_binding, clippy::non_ascii_literal, clippy::print_literal, clippy::print_stderr, clippy::print_stdout, - clippy::redundant_type_annotations, - clippy::ref_option, clippy::ref_patterns, - clippy::similar_names, - clippy::single_call_fn, - clippy::single_match_else, - clippy::std_instead_of_alloc, - clippy::std_instead_of_core, clippy::too_many_arguments, - clippy::too_many_lines, - clippy::unnecessary_wraps, - clippy::unwrap_used, - clippy::useless_format, - clippy::wildcard_enum_match_arm, reason = "Bench tool: matches test-style fixture code" )] use std::{path::PathBuf, time::Instant}; -mod ppe; -mod stats; - use amm_core::{PoolDefinition, compute_liquidity_token_pda, compute_pool_pda, compute_vault_pda}; use anyhow::Result; use ata_core::{compute_ata_seed, get_associated_token_account_id}; @@ -71,9 +45,13 @@ use nssa_core::{ }; use risc0_zkvm::{ExecutorEnv, default_executor, default_prover}; use serde::Serialize; -use stats::Stats; use token_core::{TokenDefinition, TokenHolding}; +use stats::Stats; + +mod ppe; +mod stats; + #[derive(Parser, Debug)] #[command(about = "Per-program executor and (optionally) prover cycle measurements")] struct Cli { @@ -82,14 +60,14 @@ struct Cli { prove: bool, /// Also run privacy-preserving execution circuit (PPE) composition cases: - /// (a) single auth_transfer Transfer through `execute_and_prove`, (b) chain_caller + /// (a) single `auth_transfer` Transfer through `execute_and_prove`, (b) `chain_caller` /// with depth N=1,3,5,9. Requires --features ppe at build time. Very slow. #[arg(long)] ppe: bool, /// After running --ppe-style proving once for auth_transfer-in-PPE, time - /// receipt.verify(PRIVACY_PRESERVING_CIRCUIT_ID) over many iterations. - /// Produces G_verify for the fee model. Requires --features ppe. + /// `receipt.verify(PRIVACY_PRESERVING_CIRCUIT_ID)` over many iterations. + /// Produces `G_verify` for the fee model. Requires --features ppe. #[arg(long)] verify: bool, @@ -343,11 +321,11 @@ fn amm_lp_def_id() -> AccountId { compute_liquidity_token_pda(AMM_ID, amm_pool_id()) } -/// Pool seeded with reserves 1_000 / 500, lp supply sqrt(1000*500) = 707. +/// Pool seeded with reserves `1_000` / `500`, lp supply `sqrt(1000*500) = 707`. fn amm_pool_account() -> AccountWithMetadata { let reserve_a: u128 = 1_000; let reserve_b: u128 = 500; - let lp_supply: u128 = (reserve_a * reserve_b).isqrt(); + let lp_supply = (reserve_a * reserve_b).isqrt(); AccountWithMetadata { account: Account { program_owner: AMM_ID, @@ -384,7 +362,7 @@ fn amm_add_liquidity_pre_states() -> Vec { let pool = amm_pool_account(); let vault_a = token_holding(amm_token_a_def_id(), amm_vault_a_id(), 1_000, true); let vault_b = token_holding(amm_token_b_def_id(), amm_vault_b_id(), 500, true); - let lp_supply: u128 = (1_000_u128 * 500_u128).isqrt(); + let lp_supply = (1_000_u128 * 500_u128).isqrt(); let lp_def = token_definition(amm_lp_def_id(), lp_supply, true); let user_a = token_holding(amm_token_a_def_id(), AccountId::new([45; 32]), 1_000, true); let user_b = token_holding(amm_token_b_def_id(), AccountId::new([46; 32]), 500, true); @@ -538,7 +516,7 @@ fn main() -> Result<()> { print_table(&results, prove); #[cfg(feature = "ppe")] - let ppe_results = if cli.ppe { ppe::run_all()? } else { Vec::new() }; + let ppe_results = if cli.ppe { ppe::run_all() } else { Vec::new() }; #[cfg(not(feature = "ppe"))] let ppe_results: Vec = { if cli.ppe { @@ -637,16 +615,14 @@ fn print_table(results: &[BenchResult], prove: bool) { for r in results { let total = r .prove_total_cycles - .map(|c| c.to_string()) - .unwrap_or_else(|| "-".to_owned()); - let pms = r - .prove_stats - .map(|s| format!("{:.1} ({:.1}s)", s.best_ms, s.best_ms / 1_000.0)) - .unwrap_or_else(|| "-".to_owned()); + .map_or_else(|| "-".to_owned(), |c| c.to_string()); + let pms = r.prove_stats.map_or_else( + || "-".to_owned(), + |s| format!("{:.1} ({:.1}s)", s.best_ms, s.best_ms / 1_000.0), + ); let psegs = r .prove_segments - .map(|s| s.to_string()) - .unwrap_or_else(|| "-".to_owned()); + .map_or_else(|| "-".to_owned(), |s| s.to_string()); println!( "{:pcw$} {:>pwallw$} {:>psw$}", r.program, r.instruction, total, pms, psegs, diff --git a/tools/cycle_bench/src/ppe.rs b/tools/cycle_bench/src/ppe.rs index c564980f..0390f533 100644 --- a/tools/cycle_bench/src/ppe.rs +++ b/tools/cycle_bench/src/ppe.rs @@ -1,14 +1,14 @@ -//! Privacy-preserving execution (PPE) cases for cycle_bench. +//! Privacy-preserving execution (PPE) cases for `cycle_bench`. //! //! Composition cost is the delta between standalone `prover.prove(env, elf)` for //! a single program (measured in the main bench) and a full `execute_and_prove` //! that wraps the same program in the privacy circuit. Chained-call depth sweep //! uses the `chain_caller` test program (loaded from artifacts/) with N=1, 3, 5, 9. //! -//! `run_verify` produces G_verify for the fee model: it generates one PPE -//! receipt (auth_transfer Transfer in PPE) and times `Receipt::verify` over +//! `run_verify` produces `G_verify` for the fee model: it generates one PPE +//! receipt (`auth_transfer` Transfer in PPE) and times `Receipt::verify` over //! `iters` iterations. The proof bytes captured here are also the on-wire -//! "outer proof" payload (S_agg in the fee model). +//! "outer proof" payload (`S_agg` in the fee model). #![allow( dead_code, @@ -20,12 +20,15 @@ use serde::Serialize; use crate::stats::Stats; +#[cfg(feature = "ppe")] +mod ppe_impl; + #[derive(Debug, Serialize, Clone)] pub struct PpeBenchResult { pub label: String, pub chain_depth: usize, pub prove_wall_ms: Option, - /// borsh-serialized InnerReceipt length (S_agg in the fee model). + /// borsh-serialized `InnerReceipt` length (`S_agg` in the fee model). pub proof_bytes: Option, pub error: Option, } @@ -39,12 +42,12 @@ pub struct VerifyBenchResult { } #[cfg(not(feature = "ppe"))] -pub fn run_all() -> Result> { - Ok(Vec::new()) +pub fn run_all() -> Vec { + Vec::new() } #[cfg(feature = "ppe")] -pub fn run_all() -> Result> { +pub fn run_all() -> Vec { let mut results = Vec::new(); eprintln!("PPE: running composition cost (auth_transfer Transfer in PPE)"); @@ -55,7 +58,7 @@ pub fn run_all() -> Result> { results.push(ppe_impl::run_chain_caller(depth)); } - Ok(results) + results } #[cfg(not(feature = "ppe"))] @@ -87,14 +90,13 @@ pub fn print_table(results: &[PpeBenchResult]) { ); println!("{}", "-".repeat(lw + 60)); for r in results { - let p = r - .prove_wall_ms - .map(|v| format!("{v:.1} ({:.1}s)", v / 1_000.0)) - .unwrap_or_else(|| "-".to_owned()); + let p = r.prove_wall_ms.map_or_else( + || "-".to_owned(), + |v| format!("{v:.1} ({:.1}s)", v / 1_000.0), + ); let b = r .proof_bytes - .map(|n| n.to_string()) - .unwrap_or_else(|| "-".to_owned()); + .map_or_else(|| "-".to_owned(), |n| n.to_string()); let e = r.error.as_deref().unwrap_or(""); println!( "{:5} {:>20} {:>12} {}", @@ -118,199 +120,3 @@ pub fn print_verify(r: &VerifyBenchResult) { println!(" journal_bytes : {}", r.journal_bytes); println!(" verify_ms : {}", r.stats.format()); } - -#[cfg(feature = "ppe")] -mod ppe_impl { - use std::{collections::HashMap, time::Instant}; - - use nssa::{ - execute_and_prove, - privacy_preserving_transaction::circuit::{ProgramWithDependencies, Proof}, - program::Program, - program_methods::PRIVACY_PRESERVING_CIRCUIT_ID, - }; - use nssa_core::{ - InputAccountIdentity, PrivacyPreservingCircuitOutput, - account::{Account, AccountId, AccountWithMetadata}, - program::ProgramId, - }; - use risc0_zkvm::{InnerReceipt, Receipt, serde::to_vec}; - - use super::{PpeBenchResult, VerifyBenchResult}; - use crate::stats::Stats; - - const AUTH_TRANSFER_ID: ProgramId = nssa::program_methods::AUTHENTICATED_TRANSFER_ID; - const AUTH_TRANSFER_ELF: &[u8] = nssa::program_methods::AUTHENTICATED_TRANSFER_ELF; - - /// chain_caller bytecode shipped at artifacts/test_program_methods/chain_caller.bin. - /// Loaded at compile time so we don't need a dev-dependency on test_program_methods. - const CHAIN_CALLER_ELF: &[u8] = - include_bytes!("../../../artifacts/test_program_methods/chain_caller.bin"); - - pub fn run_auth_transfer_in_ppe() -> PpeBenchResult { - let label = "auth_transfer Transfer in PPE".to_owned(); - let started = Instant::now(); - match prove_auth_transfer_in_ppe() { - Ok((_out, proof)) => { - let prove_ms = started.elapsed().as_secs_f64() * 1_000.0; - PpeBenchResult { - label, - chain_depth: 0, - prove_wall_ms: Some(prove_ms), - proof_bytes: Some(proof.into_inner().len()), - error: None, - } - } - Err(err) => PpeBenchResult { - label, - chain_depth: 0, - prove_wall_ms: None, - proof_bytes: None, - error: Some(err.to_string()), - }, - } - } - - fn prove_auth_transfer_in_ppe() -> anyhow::Result<(PrivacyPreservingCircuitOutput, Proof)> { - let program = Program::new(AUTH_TRANSFER_ELF.to_vec())?; - let pwd = ProgramWithDependencies::from(program); - - // For PPE to allow the sender's balance to be decremented by this - // program, the sender must already be claimed by auth_transfer. - // Recipient stays default-owned so the first call can claim it. - let sender = AccountWithMetadata { - account: Account { - program_owner: AUTH_TRANSFER_ID, - balance: 1_000_000, - ..Account::default() - }, - is_authorized: true, - account_id: AccountId::new([1; 32]), - }; - let recipient = AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([2; 32]), - }; - let pre_states = vec![sender, recipient]; - - let balance_to_move: u128 = 5_000; - let instruction_data = to_vec(&balance_to_move)?; - - let account_identities = vec![InputAccountIdentity::Public; pre_states.len()]; - - Ok(execute_and_prove( - pre_states, - instruction_data, - account_identities, - &pwd, - )?) - } - - pub fn run_chain_caller(depth: u32) -> PpeBenchResult { - let label = format!("chain_caller depth={depth}"); - let started = Instant::now(); - match prove_chain_caller(depth) { - Ok((_out, proof)) => { - let prove_ms = started.elapsed().as_secs_f64() * 1_000.0; - PpeBenchResult { - label, - chain_depth: depth as usize, - prove_wall_ms: Some(prove_ms), - proof_bytes: Some(proof.into_inner().len()), - error: None, - } - } - Err(err) => PpeBenchResult { - label, - chain_depth: depth as usize, - prove_wall_ms: None, - proof_bytes: None, - error: Some(err.to_string()), - }, - } - } - - fn prove_chain_caller( - num_chain_calls: u32, - ) -> anyhow::Result<(PrivacyPreservingCircuitOutput, Proof)> { - let chain_caller = Program::new(CHAIN_CALLER_ELF.to_vec())?; - let auth_transfer = Program::new(AUTH_TRANSFER_ELF.to_vec())?; - let mut deps = HashMap::new(); - deps.insert(AUTH_TRANSFER_ID, auth_transfer); - let pwd = ProgramWithDependencies::new(chain_caller, deps); - - // Both accounts pre-claimed by auth_transfer. chain_caller doesn't - // track recipient's post-claim program_owner, so a default recipient - // would cause a state mismatch on subsequent chained calls. - let recipient_pre = AccountWithMetadata { - account: Account { - program_owner: AUTH_TRANSFER_ID, - ..Account::default() - }, - is_authorized: true, - account_id: AccountId::new([2; 32]), - }; - let sender_pre = AccountWithMetadata { - account: Account { - program_owner: AUTH_TRANSFER_ID, - balance: 1_000_000, - ..Account::default() - }, - is_authorized: true, - account_id: AccountId::new([1; 32]), - }; - // chain_caller expects pre_states = [recipient, sender]. - let pre_states = vec![recipient_pre, sender_pre]; - - let balance: u128 = 1; - let pda_seed: Option = None; - let instruction = (balance, AUTH_TRANSFER_ID, num_chain_calls, pda_seed); - let instruction_data = to_vec(&instruction)?; - - let account_identities = vec![InputAccountIdentity::Public; pre_states.len()]; - - Ok(execute_and_prove( - pre_states, - instruction_data, - account_identities, - &pwd, - )?) - } - - pub fn run_verify(iters: usize) -> anyhow::Result { - eprintln!("verify: generating PPE receipt for auth_transfer Transfer (~1 prove)"); - let (output, proof) = prove_auth_transfer_in_ppe()?; - let journal = output.to_bytes(); - let journal_bytes = journal.len(); - let proof_bytes_vec = proof.into_inner(); - let proof_bytes = proof_bytes_vec.len(); - - let inner: InnerReceipt = borsh::from_slice(&proof_bytes_vec) - .map_err(|e| anyhow::anyhow!("InnerReceipt deserialize: {e}"))?; - let receipt = Receipt::new(inner, journal); - - // Sanity-check before the timing loop so we don't measure 1000 failures. - receipt - .verify(PRIVACY_PRESERVING_CIRCUIT_ID) - .map_err(|e| anyhow::anyhow!("verify sanity check failed: {e}"))?; - - eprintln!("verify: timing {iters} iters of receipt.verify(...)"); - let mut samples = Vec::with_capacity(iters); - for _ in 0..iters { - let started = Instant::now(); - receipt - .verify(PRIVACY_PRESERVING_CIRCUIT_ID) - .map_err(|e| anyhow::anyhow!("verify failed mid-loop: {e}"))?; - samples.push(started.elapsed().as_secs_f64() * 1_000.0); - } - let stats = Stats::from_samples(&samples); - - Ok(VerifyBenchResult { - label: "auth_transfer Transfer in PPE".to_owned(), - stats, - proof_bytes, - journal_bytes, - }) - } -} diff --git a/tools/cycle_bench/src/ppe/ppe_impl.rs b/tools/cycle_bench/src/ppe/ppe_impl.rs new file mode 100644 index 00000000..c20db9ac --- /dev/null +++ b/tools/cycle_bench/src/ppe/ppe_impl.rs @@ -0,0 +1,194 @@ +//! Feature-gated implementation of PPE composition and verify benches. + +use std::{collections::HashMap, time::Instant}; + +use nssa::{ + execute_and_prove, + privacy_preserving_transaction::circuit::{ProgramWithDependencies, Proof}, + program::Program, + program_methods::PRIVACY_PRESERVING_CIRCUIT_ID, +}; +use nssa_core::{ + InputAccountIdentity, PrivacyPreservingCircuitOutput, + account::{Account, AccountId, AccountWithMetadata}, + program::ProgramId, +}; +use risc0_zkvm::{InnerReceipt, Receipt, serde::to_vec}; + +use super::{PpeBenchResult, VerifyBenchResult}; +use crate::stats::Stats; + +const AUTH_TRANSFER_ID: ProgramId = nssa::program_methods::AUTHENTICATED_TRANSFER_ID; +const AUTH_TRANSFER_ELF: &[u8] = nssa::program_methods::AUTHENTICATED_TRANSFER_ELF; + +/// `chain_caller` bytecode shipped at `artifacts/test_program_methods/chain_caller.bin`. +/// Loaded at compile time so we don't need a dev-dependency on `test_program_methods`. +const CHAIN_CALLER_ELF: &[u8] = + include_bytes!("../../../../artifacts/test_program_methods/chain_caller.bin"); + +pub fn run_auth_transfer_in_ppe() -> PpeBenchResult { + let label = "auth_transfer Transfer in PPE".to_owned(); + let started = Instant::now(); + match prove_auth_transfer_in_ppe() { + Ok((_out, proof)) => { + let prove_ms = started.elapsed().as_secs_f64() * 1_000.0; + PpeBenchResult { + label, + chain_depth: 0, + prove_wall_ms: Some(prove_ms), + proof_bytes: Some(proof.into_inner().len()), + error: None, + } + } + Err(err) => PpeBenchResult { + label, + chain_depth: 0, + prove_wall_ms: None, + proof_bytes: None, + error: Some(err.to_string()), + }, + } +} + +fn prove_auth_transfer_in_ppe() -> anyhow::Result<(PrivacyPreservingCircuitOutput, Proof)> { + let program = Program::new(AUTH_TRANSFER_ELF.to_vec())?; + let pwd = ProgramWithDependencies::from(program); + + // For PPE to allow the sender's balance to be decremented by this + // program, the sender must already be claimed by auth_transfer. + // Recipient stays default-owned so the first call can claim it. + let sender = AccountWithMetadata { + account: Account { + program_owner: AUTH_TRANSFER_ID, + balance: 1_000_000, + ..Account::default() + }, + is_authorized: true, + account_id: AccountId::new([1; 32]), + }; + let recipient = AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([2; 32]), + }; + let pre_states = vec![sender, recipient]; + + let balance_to_move: u128 = 5_000; + let instruction_data = to_vec(&balance_to_move)?; + + let account_identities = vec![InputAccountIdentity::Public; pre_states.len()]; + + Ok(execute_and_prove( + pre_states, + instruction_data, + account_identities, + &pwd, + )?) +} + +pub fn run_chain_caller(depth: u32) -> PpeBenchResult { + let label = format!("chain_caller depth={depth}"); + let started = Instant::now(); + match prove_chain_caller(depth) { + Ok((_out, proof)) => { + let prove_ms = started.elapsed().as_secs_f64() * 1_000.0; + PpeBenchResult { + label, + chain_depth: depth as usize, + prove_wall_ms: Some(prove_ms), + proof_bytes: Some(proof.into_inner().len()), + error: None, + } + } + Err(err) => PpeBenchResult { + label, + chain_depth: depth as usize, + prove_wall_ms: None, + proof_bytes: None, + error: Some(err.to_string()), + }, + } +} + +fn prove_chain_caller( + num_chain_calls: u32, +) -> anyhow::Result<(PrivacyPreservingCircuitOutput, Proof)> { + let chain_caller = Program::new(CHAIN_CALLER_ELF.to_vec())?; + let auth_transfer = Program::new(AUTH_TRANSFER_ELF.to_vec())?; + let mut deps = HashMap::new(); + deps.insert(AUTH_TRANSFER_ID, auth_transfer); + let pwd = ProgramWithDependencies::new(chain_caller, deps); + + // Both accounts pre-claimed by auth_transfer. chain_caller doesn't + // track recipient's post-claim program_owner, so a default recipient + // would cause a state mismatch on subsequent chained calls. + let recipient_pre = AccountWithMetadata { + account: Account { + program_owner: AUTH_TRANSFER_ID, + ..Account::default() + }, + is_authorized: true, + account_id: AccountId::new([2; 32]), + }; + let sender_pre = AccountWithMetadata { + account: Account { + program_owner: AUTH_TRANSFER_ID, + balance: 1_000_000, + ..Account::default() + }, + is_authorized: true, + account_id: AccountId::new([1; 32]), + }; + // chain_caller expects pre_states = [recipient, sender]. + let pre_states = vec![recipient_pre, sender_pre]; + + let balance: u128 = 1; + let pda_seed: Option = None; + let instruction = (balance, AUTH_TRANSFER_ID, num_chain_calls, pda_seed); + let instruction_data = to_vec(&instruction)?; + + let account_identities = vec![InputAccountIdentity::Public; pre_states.len()]; + + Ok(execute_and_prove( + pre_states, + instruction_data, + account_identities, + &pwd, + )?) +} + +pub fn run_verify(iters: usize) -> anyhow::Result { + eprintln!("verify: generating PPE receipt for auth_transfer Transfer (~1 prove)"); + let (output, proof) = prove_auth_transfer_in_ppe()?; + let journal = output.to_bytes(); + let journal_bytes = journal.len(); + let proof_bytes_vec = proof.into_inner(); + let proof_bytes = proof_bytes_vec.len(); + + let inner: InnerReceipt = borsh::from_slice(&proof_bytes_vec) + .map_err(|e| anyhow::anyhow!("InnerReceipt deserialize: {e}"))?; + let receipt = Receipt::new(inner, journal); + + // Sanity-check before the timing loop so we don't measure 1000 failures. + receipt + .verify(PRIVACY_PRESERVING_CIRCUIT_ID) + .map_err(|e| anyhow::anyhow!("verify sanity check failed: {e}"))?; + + eprintln!("verify: timing {iters} iters of receipt.verify(...)"); + let mut samples = Vec::with_capacity(iters); + for _ in 0..iters { + let started = Instant::now(); + receipt + .verify(PRIVACY_PRESERVING_CIRCUIT_ID) + .map_err(|e| anyhow::anyhow!("verify failed mid-loop: {e}"))?; + samples.push(started.elapsed().as_secs_f64() * 1_000.0); + } + let stats = Stats::from_samples(&samples); + + Ok(VerifyBenchResult { + label: "auth_transfer Transfer in PPE".to_owned(), + stats, + proof_bytes, + journal_bytes, + }) +}