From 7b099395d62e8006d603ae014e8843c93e2068df Mon Sep 17 00:00:00 2001 From: agureev Date: Tue, 16 Jun 2026 21:14:48 +0400 Subject: [PATCH] Add cycle benchmarks of PPE with private receiver --- tools/cycle_bench/benches/verify.rs | 2 +- tools/cycle_bench/src/main.rs | 11 +++- tools/cycle_bench/src/ppe.rs | 20 +++++-- tools/cycle_bench/src/ppe/ppe_impl.rs | 85 ++++++++++++++++++--------- 4 files changed, 81 insertions(+), 37 deletions(-) diff --git a/tools/cycle_bench/benches/verify.rs b/tools/cycle_bench/benches/verify.rs index 648262f1..83da40e6 100644 --- a/tools/cycle_bench/benches/verify.rs +++ b/tools/cycle_bench/benches/verify.rs @@ -15,7 +15,7 @@ use lee::program_methods::PRIVACY_PRESERVING_CIRCUIT_ID; use risc0_zkvm::{InnerReceipt, Receipt}; fn bench_verify(c: &mut Criterion) { - let (output, proof) = prove_auth_transfer_in_ppe().expect("prove auth_transfer in PPE"); + let (output, proof, _) = prove_auth_transfer_in_ppe(false).expect("prove auth_transfer in PPE"); let journal = output.to_bytes(); let proof_bytes = proof.into_inner(); let inner: InnerReceipt = borsh::from_slice(&proof_bytes) diff --git a/tools/cycle_bench/src/main.rs b/tools/cycle_bench/src/main.rs index bed61f92..276480be 100644 --- a/tools/cycle_bench/src/main.rs +++ b/tools/cycle_bench/src/main.rs @@ -58,6 +58,11 @@ struct Cli { #[arg(long)] ppe: bool, + /// With --ppe, run the `auth_transfer` case with a private receiver. Requires + /// --features ppe at build time. Extremely slow. + #[arg(long)] + ppe_private: 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)] @@ -609,7 +614,11 @@ fn main() -> Result<()> { } #[cfg(feature = "ppe")] - let ppe_results = if cli.ppe { ppe::run_all() } else { Vec::new() }; + let ppe_results = if cli.ppe { + ppe::run_all(cli.ppe_private) + } else { + Vec::new() + }; #[cfg(not(feature = "ppe"))] let ppe_results: Vec = { if cli.ppe { diff --git a/tools/cycle_bench/src/ppe.rs b/tools/cycle_bench/src/ppe.rs index 77ce52dc..1afa32e7 100644 --- a/tools/cycle_bench/src/ppe.rs +++ b/tools/cycle_bench/src/ppe.rs @@ -27,6 +27,8 @@ pub struct PpeBenchResult { pub label: String, pub chain_depth: usize, pub prove_wall_ms: Option, + /// Executor `user_cycles` of the privacy circuit. + pub user_cycles: Option, /// borsh-serialized `InnerReceipt` length (`S_agg` in the fee model). pub proof_bytes: Option, pub error: Option, @@ -34,17 +36,18 @@ pub struct PpeBenchResult { #[cfg(not(feature = "ppe"))] #[must_use] -pub const fn run_all() -> Vec { +pub const fn run_all(_private: bool) -> Vec { Vec::new() } +/// Runs the PPE cases. #[cfg(feature = "ppe")] #[must_use] -pub fn run_all() -> Vec { +pub fn run_all(private: bool) -> Vec { let mut results = Vec::new(); eprintln!("PPE: running composition cost (auth_transfer Transfer in PPE)"); - results.push(ppe_impl::run_auth_transfer_in_ppe()); + results.push(ppe_impl::run_auth_transfer_in_ppe(private)); for depth in [1_u32, 3, 5, 9] { eprintln!("PPE: running chain_caller depth={depth}"); @@ -63,29 +66,34 @@ pub fn print_table(results: &[PpeBenchResult]) { .max("label".len()); println!( - "\n{:5} {:>20} {:>12} {}", + "\n{:5} {:>20} {:>12} {:>12} {}", "label", "depth", "prove_ms (s)", + "user_cycles", "proof_bytes", "error", lw = lw, ); - println!("{}", "-".repeat(lw + 60)); + println!("{}", "-".repeat(lw + 72)); for r in results { let p = r.prove_wall_ms.map_or_else( || "-".to_owned(), |v| format!("{v:.1} ({:.1}s)", v / 1_000.0), ); + let c = r + .user_cycles + .map_or_else(|| "-".to_owned(), |n| n.to_string()); let b = r .proof_bytes .map_or_else(|| "-".to_owned(), |n| n.to_string()); let e = r.error.as_deref().unwrap_or(""); println!( - "{:5} {:>20} {:>12} {}", + "{:5} {:>20} {:>12} {:>12} {}", r.label, r.chain_depth, p, + c, b, e, lw = lw, diff --git a/tools/cycle_bench/src/ppe/ppe_impl.rs b/tools/cycle_bench/src/ppe/ppe_impl.rs index 4d47bebc..ad317ec3 100644 --- a/tools/cycle_bench/src/ppe/ppe_impl.rs +++ b/tools/cycle_bench/src/ppe/ppe_impl.rs @@ -6,12 +6,14 @@ use std::{collections::HashMap, time::Instant}; use lee::{ - execute_and_prove, - privacy_preserving_transaction::circuit::{ProgramWithDependencies, Proof}, + privacy_preserving_transaction::circuit::{ + ProgramWithDependencies, Proof, execute_and_prove_with_cycles, + }, program::Program, }; use lee_core::{ - InputAccountIdentity, PrivacyPreservingCircuitOutput, + EphemeralPublicKey, InputAccountIdentity, NullifierPublicKey, PrivacyPreservingCircuitOutput, + SharedSecretKey, account::{Account, AccountId, AccountWithMetadata}, program::ProgramId, }; @@ -27,17 +29,22 @@ const AUTH_TRANSFER_ELF: &[u8] = lee::program_methods::AUTHENTICATED_TRANSFER_EL 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(); +pub fn run_auth_transfer_in_ppe(private: bool) -> PpeBenchResult { + let label = if private { + "auth_transfer Transfer in PPE (private output)".to_owned() + } else { + "auth_transfer Transfer in PPE (all public)".to_owned() + }; let started = Instant::now(); - match prove_auth_transfer_in_ppe() { - Ok((_out, proof)) => { + match prove_auth_transfer_in_ppe(private) { + Ok((_out, proof, user_cycles)) => { 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()), + user_cycles: Some(user_cycles), error: None, } } @@ -46,18 +53,20 @@ pub fn run_auth_transfer_in_ppe() -> PpeBenchResult { chain_depth: 0, prove_wall_ms: None, proof_bytes: None, + user_cycles: None, error: Some(err.to_string()), }, } } -pub fn prove_auth_transfer_in_ppe() -> anyhow::Result<(PrivacyPreservingCircuitOutput, Proof)> { - let program = Program::new(AUTH_TRANSFER_ELF.to_vec())?; - let pwd = ProgramWithDependencies::from(program); +/// Proves an `auth_transfer` Transfer wrapped in the privacy circuit. +/// `private` flag signals whether the receiver is a private account. +pub fn prove_auth_transfer_in_ppe( + private: bool, +) -> anyhow::Result<(PrivacyPreservingCircuitOutput, Proof, u64)> { + let pwd = ProgramWithDependencies::from(Program::new(AUTH_TRANSFER_ELF.to_vec())?); - // 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. + // Sender must already be claimed by auth_transfer for its balance to be debited. let sender = AccountWithMetadata { account: Account { program_owner: AUTH_TRANSFER_ID, @@ -67,22 +76,38 @@ pub fn prove_auth_transfer_in_ppe() -> anyhow::Result<(PrivacyPreservingCircuitO 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 instruction_data = + to_vec(&authenticated_transfer_core::Instruction::Transfer { amount: 5_000 })?; + + let (recipient, recipient_identity) = if private { + let npk = NullifierPublicKey::from(&[2; 32]); + let recipient = AccountWithMetadata { + account: Account::default(), + is_authorized: false, + account_id: AccountId::for_regular_private_account(&npk, 0), + }; + let identity = InputAccountIdentity::PrivateUnauthorized { + epk: EphemeralPublicKey(Vec::new()), + view_tag: 0, + npk, + ssk: SharedSecretKey([3; 32]), + identifier: 0, + }; + (recipient, identity) + } else { + // Recipient stays default-owned so the first call can claim it. + let recipient = AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([4; 32]), + }; + (recipient, InputAccountIdentity::Public) }; - let pre_states = vec![sender, recipient]; - let instruction = authenticated_transfer_core::Instruction::Transfer { amount: 5_000 }; - let instruction_data = to_vec(&instruction)?; - - let account_identities = vec![InputAccountIdentity::Public; pre_states.len()]; - - Ok(execute_and_prove( - pre_states, + Ok(execute_and_prove_with_cycles( + vec![sender, recipient], instruction_data, - account_identities, + vec![InputAccountIdentity::Public, recipient_identity], &pwd, )?) } @@ -91,13 +116,14 @@ 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)) => { + Ok((_out, proof, user_cycles)) => { 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()), + user_cycles: Some(user_cycles), error: None, } } @@ -106,6 +132,7 @@ pub fn run_chain_caller(depth: u32) -> PpeBenchResult { chain_depth: depth as usize, prove_wall_ms: None, proof_bytes: None, + user_cycles: None, error: Some(err.to_string()), }, } @@ -113,7 +140,7 @@ pub fn run_chain_caller(depth: u32) -> PpeBenchResult { fn prove_chain_caller( num_chain_calls: u32, -) -> anyhow::Result<(PrivacyPreservingCircuitOutput, Proof)> { +) -> anyhow::Result<(PrivacyPreservingCircuitOutput, Proof, u64)> { 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(); @@ -150,7 +177,7 @@ fn prove_chain_caller( let account_identities = vec![InputAccountIdentity::Public; pre_states.len()]; - Ok(execute_and_prove( + Ok(execute_and_prove_with_cycles( pre_states, instruction_data, account_identities,