From b608d10ca18866c6bceab774c54833f7f70d4ac9 Mon Sep 17 00:00:00 2001 From: Moudy Date: Thu, 21 May 2026 16:44:40 +0200 Subject: [PATCH 1/6] feat(crypto_primitives_bench): migrate to criterion harness --- Cargo.lock | 168 +++++++++++++++-- Cargo.toml | 1 + docs/benchmarks/crypto_primitives_bench.md | 33 +++- tools/crypto_primitives_bench/Cargo.toml | 11 +- tools/crypto_primitives_bench/README.md | 25 ++- .../benches/primitives.rs | 90 +++++++++ tools/crypto_primitives_bench/src/main.rs | 175 ------------------ 7 files changed, 294 insertions(+), 209 deletions(-) create mode 100644 tools/crypto_primitives_bench/benches/primitives.rs delete mode 100644 tools/crypto_primitives_bench/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index e81f6326..4236cd7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,6 +91,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -126,6 +135,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anstream" version = "0.6.21" @@ -1405,6 +1420,12 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cbindgen" version = "0.29.2" @@ -1489,6 +1510,33 @@ dependencies = [ "windows-link", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "cipher" version = "0.4.4" @@ -1860,6 +1908,41 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "criterion" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" +dependencies = [ + "alloca", + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools 0.13.0", + "num-traits", + "oorandom", + "page_size", + "plotters", + "rayon", + "regex", + "serde", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" +dependencies = [ + "cast", + "itertools 0.13.0", +] + [[package]] name = "critical-section" version = "1.2.0" @@ -1942,12 +2025,10 @@ dependencies = [ name = "crypto_primitives_bench" version = "0.1.0" dependencies = [ - "anyhow", + "criterion", "key_protocol", "nssa_core", "rand 0.8.5", - "serde", - "serde_json", ] [[package]] @@ -2098,7 +2179,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 2.0.117", ] [[package]] @@ -2582,7 +2663,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3145,6 +3226,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hash32" version = "0.2.1" @@ -3561,7 +3653,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.3", "tokio", "tower-service", "tracing", @@ -6583,6 +6675,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "opaque-debug" version = "0.3.1" @@ -6737,6 +6835,16 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "parking" version = "2.2.1" @@ -6893,6 +7001,34 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "polling" version = "3.11.0" @@ -7235,7 +7371,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2 0.6.3", "thiserror 2.0.18", "tokio", "tracing", @@ -7272,7 +7408,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.3", "tracing", "windows-sys 0.52.0", ] @@ -8167,7 +8303,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -9138,7 +9274,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -9373,6 +9509,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.10.0" @@ -10469,7 +10615,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f8be040a..de8dd74c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -131,6 +131,7 @@ url = { version = "2.5.4", features = ["serde"] } tokio-retry = "0.3.0" schemars = "1.2" async-stream = "0.3.6" +criterion = "0.8" logos-blockchain-common-http-client = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "ee281a447d95a951752461ee0a6e88eb4a0f17cf" } logos-blockchain-key-management-system-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "ee281a447d95a951752461ee0a6e88eb4a0f17cf" } diff --git a/docs/benchmarks/crypto_primitives_bench.md b/docs/benchmarks/crypto_primitives_bench.md index f25650af..74c534fc 100644 --- a/docs/benchmarks/crypto_primitives_bench.md +++ b/docs/benchmarks/crypto_primitives_bench.md @@ -14,15 +14,17 @@ Cryptographic primitives used by client/wallet code. Measures the per-call cost ## Results -100 timed iterations per operation, 2 warmup discarded. +Criterion sample_size = 50, warm_up_time = 2 s, measurement_time = 10 s. Slope-regression point estimate in the middle column; 95% confidence interval bounds in the outer columns. -| Operation | best (µs) | mean (µs) | stdev (µs) | -|---|---:|---:|---:| -| KeyChain::new_os_random | 2,979.62 (2.98 ms) | 3,138.18 (3.14 ms) | 258.59 (0.26 ms) | -| KeyChain::new_mnemonic | 2,979.12 (2.98 ms) | 3,012.76 (3.01 ms) | 46.09 (0.05 ms) | -| SharedSecretKey::new (sender DH) | 74.17 (0.07 ms) | 74.48 (0.07 ms) | 0.22 (<0.01 ms) | -| EncryptionScheme::encrypt | 0.88 (<0.01 ms) | 0.92 (<0.01 ms) | 0.03 (<0.01 ms) | -| EncryptionScheme::decrypt | 0.75 (<0.01 ms) | 0.78 (<0.01 ms) | 0.04 (<0.01 ms) | +| Operation | low | point | high | outliers (mild + severe) | +|---|---:|---:|---:|---:| +| keychain/new_os_random | 3.11 ms | 3.21 ms | 3.34 ms | 3 + 5 | +| keychain/new_mnemonic | 3.05 ms | 3.11 ms | 3.23 ms | 0 + 2 | +| shared_secret_key/sender_dh | 76.7 µs | 78.4 µs | 80.6 µs | 3 + 4 | +| encryption/encrypt | 1.11 µs | 1.17 µs | 1.25 µs | 1 + 5 | +| encryption/decrypt | 907 ns | 928 ns | 954 ns | 0 + 3 | + +Numbers from a single M2 Pro dev box. For full estimates (slope, mean, median, MAD, std-dev) and the noise model, see `target/criterion///estimates.json` after running locally. ## Findings @@ -33,10 +35,21 @@ Cryptographic primitives used by client/wallet code. Measures the per-call cost ## Reproduce ```sh -cargo run --release -p crypto_primitives_bench +cargo bench -p crypto_primitives_bench --bench primitives ``` -JSON output: `target/crypto_primitives_bench.json`. +JSON estimates: `target/criterion///estimates.json`. HTML report: `target/criterion/report/index.html`. + +## Baseline comparison + +```sh +# On main: +cargo bench -p crypto_primitives_bench --bench primitives -- --save-baseline main +# On your branch: +cargo bench -p crypto_primitives_bench --bench primitives -- --baseline main +``` + +Criterion reports per-bench change as a percentage with a 95% confidence interval; deltas within the CI are reported as "no significant change" rather than red. ## Caveats diff --git a/tools/crypto_primitives_bench/Cargo.toml b/tools/crypto_primitives_bench/Cargo.toml index e6d1240c..38005bd0 100644 --- a/tools/crypto_primitives_bench/Cargo.toml +++ b/tools/crypto_primitives_bench/Cargo.toml @@ -8,11 +8,12 @@ publish = false [lints] workspace = true -[dependencies] +[dev-dependencies] key_protocol.workspace = true nssa_core = { workspace = true, features = ["host"] } - -anyhow.workspace = true -serde.workspace = true -serde_json.workspace = true rand = { workspace = true } +criterion = { workspace = true, features = ["html_reports"] } + +[[bench]] +name = "primitives" +harness = false diff --git a/tools/crypto_primitives_bench/README.md b/tools/crypto_primitives_bench/README.md index f45174c4..eb2da149 100644 --- a/tools/crypto_primitives_bench/README.md +++ b/tools/crypto_primitives_bench/README.md @@ -1,20 +1,29 @@ # crypto_primitives_bench -Cryptographic primitive microbenchmarks used by client/wallet code. Single host binary, no live sequencer or Bedrock needed. +Criterion-driven microbenchmarks for the cryptographic primitives client/wallet code uses on every transaction. No live sequencer or Bedrock needed. ## Run ```sh -cargo run --release -p crypto_primitives_bench +cargo bench -p crypto_primitives_bench --bench primitives ``` ## What you'll see -Per-operation `best_us`, `mean_us`, and `stdev_us` over 100 iterations (plus 2 warmup): +Criterion's per-operation report (point estimate, 95% CI, outlier counts) for: -- `KeyChain::new_os_random` — full mnemonic → SSK → NSK/VSK + public-key derivation (HMAC-SHA512 PBKDF dominates). -- `KeyChain::new_mnemonic` — same pipeline, mnemonic exposed. -- `SharedSecretKey::new (sender DH)` — secp256k1 ECDH per recipient. -- `EncryptionScheme::encrypt` / `decrypt` — ChaCha20 over an Account note. +- `keychain/new_os_random`: full mnemonic → SSK → NSK/VSK + public-key derivation (HMAC-SHA512 PBKDF dominates). +- `keychain/new_mnemonic`: same pipeline, mnemonic exposed. +- `shared_secret_key/sender_dh`: secp256k1 ECDH per recipient (includes ephemeral key gen). +- `encryption/encrypt` / `decrypt`: ChaCha20 over an Account note. -JSON output is written to `target/crypto_primitives_bench.json`. +Per-bench JSON estimates are written under `target/criterion///`. HTML reports at `target/criterion/report/index.html`. + +## Baseline comparison + +```sh +# On main: +cargo bench -p crypto_primitives_bench --bench primitives -- --save-baseline main +# On your branch: +cargo bench -p crypto_primitives_bench --bench primitives -- --baseline main +``` diff --git a/tools/crypto_primitives_bench/benches/primitives.rs b/tools/crypto_primitives_bench/benches/primitives.rs new file mode 100644 index 00000000..3691dbea --- /dev/null +++ b/tools/crypto_primitives_bench/benches/primitives.rs @@ -0,0 +1,90 @@ +//! Criterion microbenchmarks for client/wallet cryptographic primitives. +//! +//! Measures: +//! - `KeyChain::new_os_random` (mnemonic → SSK → NSK/VSK + public keys) +//! - `KeyChain::new_mnemonic` (same, but mnemonic exposed) +//! - `SharedSecretKey::new` (Diffie-Hellman shared key derivation, the per-recipient cost) +//! - `EncryptionScheme::encrypt` / `decrypt` (Account note encryption) + +use std::time::Duration; + +use criterion::{Criterion, criterion_group, criterion_main}; +use key_protocol::key_management::KeyChain; +use nssa_core::{ + Commitment, EncryptionScheme, SharedSecretKey, + account::{Account, AccountId}, + encryption::{EphemeralPublicKey, EphemeralSecretKey}, + program::PrivateAccountKind, +}; +use rand::{RngCore as _, rngs::OsRng}; + +fn bench_keychain(c: &mut Criterion) { + let mut g = c.benchmark_group("keychain"); + g.sample_size(50).noise_threshold(0.05); + g.bench_function("new_os_random", |b| b.iter(KeyChain::new_os_random)); + g.bench_function("new_mnemonic", |b| { + b.iter(|| { + let (_kc, _mnemonic) = KeyChain::new_mnemonic(""); + }); + }); + g.finish(); +} + +fn bench_shared_secret_key(c: &mut Criterion) { + // One-time setup: recipient's viewing public key (sender side bench). + let recipient_kc = KeyChain::new_os_random(); + let vpk = recipient_kc.viewing_public_key; + + let mut g = c.benchmark_group("shared_secret_key"); + g.sample_size(50).noise_threshold(0.05); + g.bench_function("sender_dh", |b| { + b.iter(|| { + let mut bytes = [0_u8; 32]; + OsRng.fill_bytes(&mut bytes); + let esk: EphemeralSecretKey = bytes; + let _epk = EphemeralPublicKey::from(&esk); + SharedSecretKey::new(esk, &vpk) + }); + }); + g.finish(); +} + +fn bench_encryption(c: &mut Criterion) { + // One-time setup: a fixed Account/Commitment and a SharedSecretKey to bench + // encrypt/decrypt over a representative note. ESK gen is excluded from the + // measured loop (covered by the SharedSecretKey bench above). + let recipient_kc = KeyChain::new_os_random(); + let vpk = recipient_kc.viewing_public_key; + let account = Account::default(); + let account_id = AccountId::new([7; 32]); + let commitment = Commitment::new(&account_id, &account); + let shared = { + let mut bytes = [0_u8; 32]; + OsRng.fill_bytes(&mut bytes); + let esk: EphemeralSecretKey = bytes; + SharedSecretKey::new(esk, &vpk) + }; + let kind = PrivateAccountKind::Regular(0_u128); + let output_index: u32 = 0; + + let mut g = c.benchmark_group("encryption"); + g.sample_size(50).noise_threshold(0.05); + g.bench_function("encrypt", |b| { + b.iter(|| EncryptionScheme::encrypt(&account, &kind, &shared, &commitment, output_index)); + }); + // One ciphertext for the decrypt bench (encrypt is deterministic given inputs). + let ct = EncryptionScheme::encrypt(&account, &kind, &shared, &commitment, output_index); + g.bench_function("decrypt", |b| { + b.iter(|| EncryptionScheme::decrypt(&ct, &shared, &commitment, output_index)); + }); + g.finish(); +} + +criterion_group! { + name = benches; + config = Criterion::default() + .warm_up_time(Duration::from_secs(2)) + .measurement_time(Duration::from_secs(10)); + targets = bench_keychain, bench_shared_secret_key, bench_encryption +} +criterion_main!(benches); diff --git a/tools/crypto_primitives_bench/src/main.rs b/tools/crypto_primitives_bench/src/main.rs deleted file mode 100644 index 619153c9..00000000 --- a/tools/crypto_primitives_bench/src/main.rs +++ /dev/null @@ -1,175 +0,0 @@ -//! Cryptographic primitive microbenchmarks used by client/wallet code. -//! -//! Measures: -//! - `KeyChain::new_os_random` (mnemonic → SSK → NSK/VSK + public keys) -//! - `KeyChain::new_mnemonic` (same, but mnemonic exposed) -//! - `SharedSecretKey::new` (Diffie-Hellman shared key derivation, the per-recipient cost) -//! - `EncryptionScheme::encrypt` / `decrypt` (Account note encryption) -//! -//! Reports best-of-N wall time per operation. No live stack required. - -#![expect( - clippy::arithmetic_side_effects, - clippy::as_conversions, - clippy::cast_precision_loss, - clippy::float_arithmetic, - clippy::print_stdout, - reason = "Bench tool" -)] - -use std::{path::PathBuf, time::Instant}; - -use anyhow::Result; -use key_protocol::key_management::KeyChain; -use nssa_core::{ - Commitment, EncryptionScheme, SharedSecretKey, - account::{Account, AccountId}, - encryption::{EphemeralPublicKey, EphemeralSecretKey}, - program::PrivateAccountKind, -}; -use rand::{RngCore as _, rngs::OsRng}; -use serde::Serialize; - -const ITERS: usize = 100; - -#[derive(Debug, Serialize)] -struct OpResult { - op: &'static str, - iters: usize, - best_us: f64, - mean_us: f64, - stdev_us: f64, -} - -fn time(op: &'static str, iters: usize, mut f: F) -> OpResult { - // Warmup - for _ in 0..2 { - f(); - } - let mut samples_ns: Vec = Vec::with_capacity(iters); - for _ in 0..iters { - let t = Instant::now(); - f(); - samples_ns.push(t.elapsed().as_nanos() as f64); - } - let best_ns = samples_ns.iter().copied().fold(f64::INFINITY, f64::min); - let mean_ns: f64 = samples_ns.iter().sum::() / iters as f64; - let stdev_ns = if iters > 1 { - let var: f64 = samples_ns - .iter() - .map(|s| (s - mean_ns).powi(2)) - .sum::() - / (iters - 1) as f64; - var.sqrt() - } else { - 0.0 - }; - OpResult { - op, - iters, - best_us: best_ns / 1_000.0, - mean_us: mean_ns / 1_000.0, - stdev_us: stdev_ns / 1_000.0, - } -} - -fn main() -> Result<()> { - let mut results: Vec = Vec::new(); - - results.push(time("KeyChain::new_os_random", ITERS, || { - let _kc = KeyChain::new_os_random(); - })); - - results.push(time("KeyChain::new_mnemonic", ITERS, || { - let (_kc, _mnemonic) = KeyChain::new_mnemonic(""); - })); - - // SharedSecretKey: caller has ephemeral secret, recipient has VSK→VPK. - // We bench the SENDER side: derive ephemeral pubkey, then SharedSecretKey::new(scalar, point). - let recipient_kc = KeyChain::new_os_random(); - let vpk = recipient_kc.viewing_public_key; - results.push(time("SharedSecretKey::new (sender DH)", ITERS, || { - let mut bytes = [0_u8; 32]; - OsRng.fill_bytes(&mut bytes); - let esk: EphemeralSecretKey = bytes; - let _epk = EphemeralPublicKey::from(&esk); - let _ssk = SharedSecretKey::new(esk, &vpk); - })); - - // EncryptionScheme::encrypt / decrypt over a small Account note. - let account = Account::default(); - let account_id = AccountId::new([7; 32]); - let commitment = Commitment::new(&account_id, &account); - let shared = { - let mut bytes = [0_u8; 32]; - OsRng.fill_bytes(&mut bytes); - let esk: EphemeralSecretKey = bytes; - SharedSecretKey::new(esk, &vpk) - }; - let kind = PrivateAccountKind::Regular(0_u128); - let output_index: u32 = 0; - - let mut produced_ct = None; - results.push(time("EncryptionScheme::encrypt", ITERS, || { - let ct = EncryptionScheme::encrypt(&account, &kind, &shared, &commitment, output_index); - produced_ct = Some(ct); - })); - let ct = produced_ct.expect("encrypt produced ciphertext"); - results.push(time("EncryptionScheme::decrypt", ITERS, || { - let _decoded = EncryptionScheme::decrypt(&ct, &shared, &commitment, output_index); - })); - - print_table(&results); - write_json(&results)?; - Ok(()) -} - -fn print_table(results: &[OpResult]) { - let ow = results - .iter() - .map(|r| r.op.len()) - .max() - .unwrap_or(0) - .max("op".len()); - let cw = 22_usize; - println!( - "{:6} {:>cw$} {:>cw$} {:>cw$}", - "op", "iters", "best_us (ms)", "mean_us (ms)", "stdev_us (ms)", - ); - println!("{}", "-".repeat(ow + 6 + cw * 3 + 8)); - for r in results { - println!( - "{:6} {:>cw$} {:>cw$} {:>cw$}", - r.op, - r.iters, - fmt_us_ms(r.best_us), - fmt_us_ms(r.mean_us), - fmt_us_ms(r.stdev_us), - ); - } -} - -fn fmt_us_ms(us: f64) -> String { - let ms = us / 1_000.0; - if ms < 0.01 { - format!("{us:.2} (<0.01 ms)") - } else { - format!("{us:.2} ({ms:.2} ms)") - } -} - -fn write_json(results: &[OpResult]) -> Result<()> { - let workspace_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("..") - .canonicalize()?; - let out_path = workspace_root - .join("target") - .join("crypto_primitives_bench.json"); - if let Some(parent) = out_path.parent() { - std::fs::create_dir_all(parent)?; - } - std::fs::write(&out_path, serde_json::to_string_pretty(&results)?)?; - println!("\nJSON written to {}", out_path.display()); - Ok(()) -} From fb89e7549b40d979ecfa14a77ce8fbc48c9bbdee Mon Sep 17 00:00:00 2001 From: Moudy Date: Thu, 21 May 2026 16:44:40 +0200 Subject: [PATCH 2/6] refactor(cycle_bench): split into lib + binary, drop hand-rolled verify timing --- tools/cycle_bench/Cargo.toml | 1 + tools/cycle_bench/src/lib.rs | 23 ++++++++++++ tools/cycle_bench/src/main.rs | 41 ++------------------- tools/cycle_bench/src/ppe.rs | 46 +++++------------------ tools/cycle_bench/src/ppe/ppe_impl.rs | 53 +++++---------------------- 5 files changed, 45 insertions(+), 119 deletions(-) create mode 100644 tools/cycle_bench/src/lib.rs diff --git a/tools/cycle_bench/Cargo.toml b/tools/cycle_bench/Cargo.toml index 6847b0c5..1a4a9db3 100644 --- a/tools/cycle_bench/Cargo.toml +++ b/tools/cycle_bench/Cargo.toml @@ -16,6 +16,7 @@ ppe = ["prove"] [dependencies] nssa = { workspace = true } nssa_core = { workspace = true, features = ["host"] } +authenticated_transfer_core.workspace = true clock_core.workspace = true token_core.workspace = true amm_core.workspace = true diff --git a/tools/cycle_bench/src/lib.rs b/tools/cycle_bench/src/lib.rs new file mode 100644 index 00000000..7091cf84 --- /dev/null +++ b/tools/cycle_bench/src/lib.rs @@ -0,0 +1,23 @@ +//! `cycle_bench` library: per-program executor/prover cycle measurement helpers +//! shared between the `cycle_bench` binary and the `verify` criterion bench. + +#![expect( + clippy::arithmetic_side_effects, + clippy::as_conversions, + clippy::cast_precision_loss, + clippy::float_arithmetic, + clippy::print_literal, + clippy::print_stdout, + reason = "Bench library: stats arithmetic and table printing are bench-style" +)] +#![cfg_attr( + feature = "ppe", + expect( + clippy::arbitrary_source_item_ordering, + clippy::print_stderr, + reason = "PPE module: re-export ordering and eprintln progress trip strict lints" + ) +)] + +pub mod ppe; +pub mod stats; diff --git a/tools/cycle_bench/src/main.rs b/tools/cycle_bench/src/main.rs index acdd9eff..bb44fb4f 100644 --- a/tools/cycle_bench/src/main.rs +++ b/tools/cycle_bench/src/main.rs @@ -9,15 +9,11 @@ #![expect( clippy::arithmetic_side_effects, - clippy::as_conversions, - clippy::cast_precision_loss, clippy::float_arithmetic, clippy::missing_const_for_fn, clippy::non_ascii_literal, - clippy::print_literal, clippy::print_stderr, clippy::print_stdout, - clippy::ref_patterns, reason = "Bench tool: matches test-style fixture code" )] @@ -31,6 +27,7 @@ use clock_core::{ CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID, ClockAccountData, }; +use cycle_bench::{ppe, stats::Stats}; use nssa::program_methods::{ AMM_ELF, AMM_ID, ASSOCIATED_TOKEN_ACCOUNT_ELF, ASSOCIATED_TOKEN_ACCOUNT_ID, AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, CLOCK_ELF, CLOCK_ID, TOKEN_ELF, @@ -43,12 +40,8 @@ use nssa_core::{ }; use risc0_zkvm::{ExecutorEnv, default_executor, default_prover}; use serde::Serialize; -use stats::Stats; use token_core::{TokenDefinition, TokenHolding}; -mod ppe; -mod stats; - #[derive(Parser, Debug)] #[command(about = "Per-program executor and (optionally) prover cycle measurements")] struct Cli { @@ -62,16 +55,6 @@ struct Cli { #[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. - #[arg(long)] - verify: bool, - - /// Iterations for --verify. Default matches the fee-model handoff target. - #[arg(long, default_value_t = 1000)] - verify_iters: usize, - /// 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)] @@ -428,7 +411,7 @@ fn main() -> Result<()> { AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, authenticated_transfer_transfer(), - &5_000_u128, + &authenticated_transfer_core::Instruction::Transfer { amount: 5_000 }, )?, Case::new( "authenticated_transfer", @@ -436,7 +419,7 @@ fn main() -> Result<()> { AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, authenticated_transfer_init(), - &0_u128, + &authenticated_transfer_core::Instruction::Initialize, )?, Case::new( "token", @@ -532,23 +515,6 @@ fn main() -> Result<()> { ppe::print_table(&ppe_results); } - #[cfg(feature = "ppe")] - let verify_result = if cli.verify { - Some(ppe::run_verify(cli.verify_iters)?) - } else { - None - }; - #[cfg(not(feature = "ppe"))] - let verify_result: Option = { - if cli.verify { - eprintln!("cycle_bench: --verify requires --features ppe at build time. Ignoring."); - } - None - }; - if let Some(ref vr) = verify_result { - ppe::print_verify(vr); - } - let workspace_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("..") .join("..") @@ -560,7 +526,6 @@ fn main() -> Result<()> { let combined = serde_json::json!({ "standalone": results, "ppe": ppe_results, - "verify": verify_result, }); std::fs::write(&out_path, serde_json::to_string_pretty(&combined)?)?; println!("\nJSON written to {}", out_path.display()); diff --git a/tools/cycle_bench/src/ppe.rs b/tools/cycle_bench/src/ppe.rs index e3bc4747..77ce52dc 100644 --- a/tools/cycle_bench/src/ppe.rs +++ b/tools/cycle_bench/src/ppe.rs @@ -5,24 +5,23 @@ //! 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 -//! `iters` iterations. The proof bytes captured here are also the on-wire -//! "outer proof" payload (`S_agg` in the fee model). +//! `Receipt::verify(PRIVACY_PRESERVING_CIRCUIT_ID)` timings (the `G_verify` fee-model +//! parameter) are measured by the `verify` criterion bench under `benches/verify.rs`, +//! which reuses the `prove_auth_transfer_in_ppe` setup helper re-exported below. #![allow( dead_code, reason = "Stubs are used when the `ppe` feature is disabled." )] -use anyhow::Result; use serde::Serialize; -use crate::stats::Stats; - #[cfg(feature = "ppe")] mod ppe_impl; +#[cfg(feature = "ppe")] +pub use ppe_impl::prove_auth_transfer_in_ppe; + #[derive(Debug, Serialize, Clone)] pub struct PpeBenchResult { pub label: String, @@ -33,20 +32,14 @@ pub struct PpeBenchResult { pub error: Option, } -#[derive(Debug, Serialize, Clone)] -pub struct VerifyBenchResult { - pub label: String, - pub stats: Stats, - pub proof_bytes: usize, - pub journal_bytes: usize, -} - #[cfg(not(feature = "ppe"))] -pub fn run_all() -> Vec { +#[must_use] +pub const fn run_all() -> Vec { Vec::new() } #[cfg(feature = "ppe")] +#[must_use] pub fn run_all() -> Vec { let mut results = Vec::new(); @@ -61,16 +54,6 @@ pub fn run_all() -> Vec { results } -#[cfg(not(feature = "ppe"))] -pub fn run_verify(_iters: usize) -> Result { - anyhow::bail!("--verify requires --features ppe at build time") -} - -#[cfg(feature = "ppe")] -pub fn run_verify(iters: usize) -> Result { - ppe_impl::run_verify(iters) -} - pub fn print_table(results: &[PpeBenchResult]) { let lw = results .iter() @@ -109,14 +92,3 @@ pub fn print_table(results: &[PpeBenchResult]) { ); } } - -pub fn print_verify(r: &VerifyBenchResult) { - println!("\nVerify (G_verify):"); - println!(" case : {}", r.label); - println!( - " proof_bytes : {} (borsh InnerReceipt, S_agg)", - r.proof_bytes - ); - println!(" journal_bytes : {}", r.journal_bytes); - println!(" verify_ms : {}", r.stats); -} diff --git a/tools/cycle_bench/src/ppe/ppe_impl.rs b/tools/cycle_bench/src/ppe/ppe_impl.rs index c20db9ac..433c4aa4 100644 --- a/tools/cycle_bench/src/ppe/ppe_impl.rs +++ b/tools/cycle_bench/src/ppe/ppe_impl.rs @@ -1,4 +1,7 @@ -//! Feature-gated implementation of PPE composition and verify benches. +//! Feature-gated implementation of PPE composition benches. +//! +//! `prove_auth_transfer_in_ppe` is reused by the `verify` criterion bench under +//! `benches/verify.rs` (re-exported via `super::prove_auth_transfer_in_ppe`). use std::{collections::HashMap, time::Instant}; @@ -6,17 +9,15 @@ 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 risc0_zkvm::serde::to_vec; -use super::{PpeBenchResult, VerifyBenchResult}; -use crate::stats::Stats; +use super::PpeBenchResult; const AUTH_TRANSFER_ID: ProgramId = nssa::program_methods::AUTHENTICATED_TRANSFER_ID; const AUTH_TRANSFER_ELF: &[u8] = nssa::program_methods::AUTHENTICATED_TRANSFER_ELF; @@ -50,7 +51,7 @@ pub fn run_auth_transfer_in_ppe() -> PpeBenchResult { } } -fn prove_auth_transfer_in_ppe() -> anyhow::Result<(PrivacyPreservingCircuitOutput, Proof)> { +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); @@ -73,8 +74,8 @@ fn prove_auth_transfer_in_ppe() -> anyhow::Result<(PrivacyPreservingCircuitOutpu }; let pre_states = vec![sender, recipient]; - let balance_to_move: u128 = 5_000; - let instruction_data = to_vec(&balance_to_move)?; + 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()]; @@ -156,39 +157,3 @@ fn prove_chain_caller( &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, - }) -} From a9bf3fbfe73e3b69eacb1173926602fdaa4e543b Mon Sep 17 00:00:00 2001 From: Moudy Date: Thu, 21 May 2026 16:44:40 +0200 Subject: [PATCH 3/6] feat(cycle_bench): add criterion verify bench for G_verify --- Cargo.lock | 2 ++ Justfile | 6 ++++ docs/benchmarks/cycle_bench.md | 25 ++++++++------- tools/cycle_bench/Cargo.toml | 8 +++++ tools/cycle_bench/README.md | 28 +++++++++++++---- tools/cycle_bench/benches/verify.rs | 47 +++++++++++++++++++++++++++++ 6 files changed, 99 insertions(+), 17 deletions(-) create mode 100644 tools/cycle_bench/benches/verify.rs diff --git a/Cargo.lock b/Cargo.lock index 4236cd7e..7255dbee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2075,9 +2075,11 @@ dependencies = [ "amm_core", "anyhow", "ata_core", + "authenticated_transfer_core", "borsh", "clap", "clock_core", + "criterion", "nssa", "nssa_core", "risc0-zkvm", diff --git a/Justfile b/Justfile index ac003a15..7af964a6 100644 --- a/Justfile +++ b/Justfile @@ -23,6 +23,12 @@ test: @echo "🧪 Running tests" RISC0_DEV_MODE=1 cargo nextest run --no-fail-fast +# Run criterion benches: fast crypto primitives, then the slow PPE verify (real proving setup). +bench: + @echo "📊 Running criterion benches" + cargo bench -p crypto_primitives_bench --bench primitives + cargo bench -p cycle_bench --features ppe --bench verify + # Run Bedrock node in docker [working-directory: 'bedrock'] run-bedrock: diff --git a/docs/benchmarks/cycle_bench.md b/docs/benchmarks/cycle_bench.md index fca9f12c..0e880070 100644 --- a/docs/benchmarks/cycle_bench.md +++ b/docs/benchmarks/cycle_bench.md @@ -63,23 +63,24 @@ Same `auth_transfer Transfer` instruction, standalone vs wrapped in the privacy Linear fit depth=1..9: ≈ 53 s per additional chained call, intercept ≈ 73 s. Composition tax (single program PPE − standalone): ≈ 48 s. `proof_bytes` is constant: the outer succinct proof has fixed size; the journal carried alongside it scales with public state and is reported separately by `--verify`. -## Verifier (`--verify`) +## Verifier (criterion bench) -One PPE receipt generated once (auth_transfer Transfer in PPE), then `Receipt::verify(PRIVACY_PRESERVING_CIRCUIT_ID)` measured over 1000 iterations. +One PPE receipt generated once (auth_transfer Transfer in PPE), then `Receipt::verify(PRIVACY_PRESERVING_CIRCUIT_ID)` measured under criterion's statistical sampler. Bench file: `tools/cycle_bench/benches/verify.rs`. Setup (one full PPE prove) is outside the timed `iter` loop. -| Field | Value | -|---|---| -| case | auth_transfer Transfer in PPE | -| proof_bytes (S_agg) | 223,551 | -| journal_bytes | 412 | -| verify_ms (best / mean ± stdev, n=1000) | 11.71 / 12.06 ± 1.99 | +Numbers from the most recent local run on the machine listed above. Criterion sample_size = 100, measurement_time = 15 s, warm_up_time = 2 s. Slope-regression point estimate in the middle column; 95% CI bounds on either side. Run `cargo bench -p cycle_bench --features ppe --bench verify` to refresh. + +| Bench | low | point | high | outliers (mild + severe) | +|---|---:|---:|---:|---:| +| ppe/verify_auth_transfer | 12.016 ms | 12.215 ms | 12.469 ms | 1 + 10 | + +The corresponding `proof_bytes` (S_agg) for the bench receipt is captured by `--ppe` above; the verify bench itself only times the verify call. ## Findings - Proving cost scales with po2-bucketed `total_cycles`, not raw `user_cycles`. Trimming user_cycles only helps if it crosses a 2^N boundary. - Single-program PPE composition tax on M2 Pro CPU: ≈ 48 s (61.5 − 13.7). - Chained-call cost is linear at ≈ 53 s per call. A max-depth chain (10) would take ≈ 600 s standalone on this CPU. -- `G_verify` is ≈ 12 ms and roughly constant per outer receipt (1000-iter stdev ≈ 2 ms). The succinct outer proof is fixed at 223,551 bytes (S_agg); verify is not on the latency critical path. +- `G_verify` is ≈ 12 ms (criterion CI: 12.0–12.5 ms over 100 samples) and roughly constant per outer receipt. The succinct outer proof is fixed at 223,551 bytes (S_agg); verify is not on the latency critical path. ## Reproduce @@ -87,10 +88,12 @@ One PPE receipt generated once (auth_transfer Transfer in PPE), then `Receipt::v cargo run --release -p cycle_bench cargo run --release -p cycle_bench --features prove -- --prove cargo run --release -p cycle_bench --features ppe -- --prove --ppe -cargo run --release -p cycle_bench --features ppe -- --verify --verify-iters 1000 + +# Verifier microbench via criterion: +cargo bench -p cycle_bench --features ppe --bench verify ``` -JSON output: `target/cycle_bench.json`. +JSON output: `target/cycle_bench.json` (bin), `target/criterion/ppe/verify_auth_transfer/` (verify bench). ## Caveats diff --git a/tools/cycle_bench/Cargo.toml b/tools/cycle_bench/Cargo.toml index 1a4a9db3..aa30ebc6 100644 --- a/tools/cycle_bench/Cargo.toml +++ b/tools/cycle_bench/Cargo.toml @@ -28,3 +28,11 @@ serde.workspace = true serde_json.workspace = true anyhow.workspace = true clap = { workspace = true } + +[dev-dependencies] +criterion = { workspace = true, features = ["html_reports"] } + +[[bench]] +name = "verify" +harness = false +required-features = ["ppe"] diff --git a/tools/cycle_bench/README.md b/tools/cycle_bench/README.md index 3b416dc8..7f3b3d95 100644 --- a/tools/cycle_bench/README.md +++ b/tools/cycle_bench/README.md @@ -4,6 +4,8 @@ Per-program Risc0 cycle counts, prover wall time, PPE composition cost, and veri ## Run +The binary handles executor cycles, prover wall time, and PPE composition cost: + ```sh # Executor cycles only (fast, ~seconds) cargo run --release -p cycle_bench @@ -13,16 +15,30 @@ cargo run --release -p cycle_bench --features prove -- --prove # + PPE composition cases (very slow, ~hour) cargo run --release -p cycle_bench --features ppe -- --prove --ppe - -# + verifier microbench (G_verify): generates one PPE receipt, times verify x1000 -cargo run --release -p cycle_bench --features ppe -- --verify --verify-iters 1000 ``` -`RISC0_DEV_MODE=1` skips proving entirely and is only useful for the executor path. Combine flags freely; output is printed to stdout and written to `target/cycle_bench.json` for regression diffs. +The verifier microbenchmark (`G_verify`) lives in a criterion bench under `benches/verify.rs`: + +```sh +# Generates one PPE receipt for auth_transfer Transfer (~minutes of setup), +# then times Receipt::verify under criterion's statistical sampler. +cargo bench -p cycle_bench --features ppe --bench verify +``` + +`RISC0_DEV_MODE=1` skips proving entirely and is only useful for the executor path. The bin writes to `target/cycle_bench.json`; criterion writes per-bench estimates under `target/criterion/`. ## What you'll see - Per-program executor cycles and segments, plus exec wall time as `best / mean ± stdev (n=N)`. - With `--prove`: prover total cycles, paging cycles, segments, and wall time. -- With `--ppe`: end-to-end `execute_and_prove` wall time and S_agg (the borsh-serialized InnerReceipt length) for one auth-transfer-in-PPE case and a chain-caller depth sweep. -- With `--verify`: verify wall time `best / mean ± stdev`, plus `proof_bytes` and `journal_bytes`. +- With `--ppe`: end-to-end `execute_and_prove` wall time and `S_agg` (the borsh-serialized InnerReceipt length) for one auth-transfer-in-PPE case and a chain-caller depth sweep. +- From the `verify` criterion bench: `ppe/verify_auth_transfer` slope-regression point estimate with 95% CI bounds. + +## Baseline comparison (verify bench) + +```sh +# On main: +cargo bench -p cycle_bench --features ppe --bench verify -- --save-baseline main +# On your branch: +cargo bench -p cycle_bench --features ppe --bench verify -- --baseline main +``` diff --git a/tools/cycle_bench/benches/verify.rs b/tools/cycle_bench/benches/verify.rs new file mode 100644 index 00000000..d7bdfbe3 --- /dev/null +++ b/tools/cycle_bench/benches/verify.rs @@ -0,0 +1,47 @@ +//! Criterion bench for `Receipt::verify(PRIVACY_PRESERVING_CIRCUIT_ID)`. +//! +//! Produces the `G_verify` fee-model parameter. Setup: one full PPE prove of an +//! `auth_transfer` Transfer (minutes, runs once outside the timed loop). Measured +//! op: `Receipt::verify` over a real PPE receipt. +//! +//! Run with: `cargo bench -p cycle_bench --features ppe --bench verify`. + +use std::{hint::black_box, time::Duration}; + +use anyhow::Context as _; +use criterion::{Criterion, criterion_group, criterion_main}; +use cycle_bench::ppe::prove_auth_transfer_in_ppe; +use nssa::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 journal = output.to_bytes(); + let proof_bytes = proof.into_inner(); + let inner: InnerReceipt = borsh::from_slice(&proof_bytes) + .context("decode InnerReceipt") + .expect("InnerReceipt deserialize"); + let receipt = Receipt::new(inner, journal); + + // Sanity check before the timed loop. + receipt + .verify(PRIVACY_PRESERVING_CIRCUIT_ID) + .expect("verify sanity check"); + + let mut g = c.benchmark_group("ppe"); + g.sample_size(100) + .warm_up_time(Duration::from_secs(2)) + .measurement_time(Duration::from_secs(15)) + .noise_threshold(0.05); + g.bench_function("verify_auth_transfer", |b| { + b.iter(|| { + receipt + .verify(black_box(PRIVACY_PRESERVING_CIRCUIT_ID)) + .expect("verify failed mid-loop"); + }); + }); + g.finish(); +} + +criterion_group!(benches, bench_verify); +criterion_main!(benches); From d064f87ad7d4234514ab7179a3e0160b4ee18ff2 Mon Sep 17 00:00:00 2001 From: Moudy Date: Thu, 21 May 2026 16:44:41 +0200 Subject: [PATCH 4/6] refactor: lift criterion html_reports feature to workspace declaration --- Cargo.toml | 2 +- tools/crypto_primitives_bench/Cargo.toml | 2 +- tools/cycle_bench/Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index de8dd74c..d3b0921c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -131,7 +131,7 @@ url = { version = "2.5.4", features = ["serde"] } tokio-retry = "0.3.0" schemars = "1.2" async-stream = "0.3.6" -criterion = "0.8" +criterion = { version = "0.8", features = ["html_reports"] } logos-blockchain-common-http-client = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "ee281a447d95a951752461ee0a6e88eb4a0f17cf" } logos-blockchain-key-management-system-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "ee281a447d95a951752461ee0a6e88eb4a0f17cf" } diff --git a/tools/crypto_primitives_bench/Cargo.toml b/tools/crypto_primitives_bench/Cargo.toml index 38005bd0..8e99f79f 100644 --- a/tools/crypto_primitives_bench/Cargo.toml +++ b/tools/crypto_primitives_bench/Cargo.toml @@ -12,7 +12,7 @@ workspace = true key_protocol.workspace = true nssa_core = { workspace = true, features = ["host"] } rand = { workspace = true } -criterion = { workspace = true, features = ["html_reports"] } +criterion.workspace = true [[bench]] name = "primitives" diff --git a/tools/cycle_bench/Cargo.toml b/tools/cycle_bench/Cargo.toml index aa30ebc6..13ea0023 100644 --- a/tools/cycle_bench/Cargo.toml +++ b/tools/cycle_bench/Cargo.toml @@ -30,7 +30,7 @@ anyhow.workspace = true clap = { workspace = true } [dev-dependencies] -criterion = { workspace = true, features = ["html_reports"] } +criterion.workspace = true [[bench]] name = "verify" From fdec52791dff560e24214c33bcb1a528dcd4dd5a Mon Sep 17 00:00:00 2001 From: Moudy Date: Thu, 21 May 2026 16:44:41 +0200 Subject: [PATCH 5/6] refactor(crypto_primitives_bench): derive account_id from key in encryption bench --- tools/crypto_primitives_bench/benches/primitives.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/crypto_primitives_bench/benches/primitives.rs b/tools/crypto_primitives_bench/benches/primitives.rs index 3691dbea..cfa0858f 100644 --- a/tools/crypto_primitives_bench/benches/primitives.rs +++ b/tools/crypto_primitives_bench/benches/primitives.rs @@ -55,8 +55,9 @@ fn bench_encryption(c: &mut Criterion) { // measured loop (covered by the SharedSecretKey bench above). let recipient_kc = KeyChain::new_os_random(); let vpk = recipient_kc.viewing_public_key; + let npk = recipient_kc.nullifier_public_key; let account = Account::default(); - let account_id = AccountId::new([7; 32]); + let account_id = AccountId::for_regular_private_account(&npk, 0); let commitment = Commitment::new(&account_id, &account); let shared = { let mut bytes = [0_u8; 32]; From ebfc3e5ad268b0a85b8444ccca6fa122019e406c Mon Sep 17 00:00:00 2001 From: Moudy Date: Thu, 21 May 2026 16:53:44 +0200 Subject: [PATCH 6/6] chore(deny): fix --- .deny.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/.deny.toml b/.deny.toml index 320a9eda..fb1ce3cf 100644 --- a/.deny.toml +++ b/.deny.toml @@ -16,7 +16,6 @@ ignore = [ { id = "RUSTSEC-2026-0097", reason = "`rand` v0.8.5 is present transitively from logos crates, modification may break integration" }, { id = "RUSTSEC-2026-0118", reason = "`hickory-proto` v0.25.0-alpha.5 is present transitively from logos crates, modification may break integration" }, { id = "RUSTSEC-2026-0119", reason = "`hickory-proto` v0.25.0-alpha.5 is present transitively from logos crates, modification may break integration" }, - { id = "RUSTSEC-2026-0145", reason = "`astral-tokio-tar` v0.6.1 is pulled transitively via testcontainers (integration_tests dev/test path); waiting on upstream fix" }, ] yanked = "deny" unused-ignored-advisory = "deny"