feat: add wallet_crypto_bench tool for wallet-side cryptographic primitives

This commit is contained in:
Moudy 2026-05-15 10:51:51 +02:00
parent 41fa494e32
commit 84a1fec942
6 changed files with 266 additions and 0 deletions

View File

@ -38,6 +38,7 @@ members = [
"examples/program_deployment/methods/guest",
"testnet_initial_state",
"indexer/ffi",
"tools/wallet_crypto_bench",
]
[workspace.dependencies]

10
docs/benchmarks/README.md Normal file
View File

@ -0,0 +1,10 @@
# Benchmarks
Bench tools live under `tools/` with READMEs for how to run each one. This directory holds the result write-ups: machine, raw tables, and short findings.
| Bench | Doc |
|---|---|
| cycle_bench | [cycle_bench.md](cycle_bench.md) |
| wallet_crypto_bench | [wallet_crypto_bench.md](wallet_crypto_bench.md) |
All numbers are from a single M2 Pro dev box unless noted otherwise.

View File

@ -0,0 +1,43 @@
# wallet_crypto_bench
Wallet-side cryptographic primitives. Measures the per-call cost of key derivation, sender-side DH for note encryption, and Account note symmetric encrypt/decrypt. Standalone host binary, no live stack required.
## Machine
| Field | Value |
|---|---|
| Chip | Apple M2 Pro (8P+4E) |
| RAM | 16 GB |
| OS | macOS 15.5 |
| Rust | 1.94.0 |
| Profile | release |
## Results
100 timed iterations per operation, 2 warmup discarded.
| 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) |
## Findings
- Keychain creation is dominated by the 2048-round HMAC-SHA512 PBKDF in the mnemonic-to-SSK path. ≈ 3 ms.
- Per-recipient DH (secp256k1) is ≈ 80 µs. Outbound shielded transfers to N recipients cost ≈ 80·N µs of crypto on top of proving.
- Symmetric encrypt/decrypt over a 49-byte Account note is sub-µs. Bulk encryption is not the bottleneck.
## Reproduce
```sh
cargo run --release -p wallet_crypto_bench
```
JSON output: `target/wallet_crypto_bench.json`.
## Caveats
- Single-thread, no SIMD acceleration. Bench dev box uses the pure-Rust secp256k1 backend.

View File

@ -0,0 +1,19 @@
[package]
name = "wallet_crypto_bench"
version = "0.1.0"
edition = "2024"
license = { workspace = true }
publish = false
[lints]
workspace = true
[dependencies]
key_protocol.workspace = true
nssa_core = { workspace = true, features = ["host"] }
anyhow.workspace = true
serde.workspace = true
serde_json.workspace = true
bip39.workspace = true
rand = { workspace = true }

View File

@ -0,0 +1,20 @@
# wallet_crypto_bench
Wallet-side cryptographic microbenchmarks. Single host binary, no live sequencer or Bedrock needed.
## Run
```sh
cargo run --release -p wallet_crypto_bench
```
## What you'll see
Per-operation `best_us`, `mean_us`, and `stdev_us` over 100 iterations (plus 2 warmup):
- `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.
JSON output is written to `target/wallet_crypto_bench.json`.

View File

@ -0,0 +1,173 @@
//! Wallet-side cryptographic microbenchmarks.
//!
//! 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.
#![allow(
clippy::arithmetic_side_effects,
clippy::print_stdout,
clippy::print_stderr,
clippy::std_instead_of_alloc,
clippy::std_instead_of_core,
clippy::float_arithmetic,
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},
};
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<F: FnMut()>(op: &'static str, iters: usize, mut f: F) -> OpResult {
// Warmup
for _ in 0..2 {
f();
}
let mut samples_ns: Vec<f64> = 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::<f64>() / iters as f64;
let stdev_ns = if iters > 1 {
let var: f64 = samples_ns
.iter()
.map(|s| (s - mean_ns).powi(2))
.sum::<f64>()
/ (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<OpResult> = 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.clone();
results.push(time("SharedSecretKey::new (sender DH)", ITERS, || {
let mut bytes = [0u8; 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 = [0u8; 32];
OsRng.fill_bytes(&mut bytes);
let esk: EphemeralSecretKey = bytes;
SharedSecretKey::new(&esk, &vpk)
};
let identifier: u128 = 0;
let output_index: u32 = 0;
let mut produced_ct = None;
results.push(time("EncryptionScheme::encrypt", ITERS, || {
let ct = EncryptionScheme::encrypt(&account, identifier, &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!(
"{:<ow$} {:>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!(
"{:<ow$} {:>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("wallet_crypto_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(())
}