mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-05-23 00:19:50 +00:00
Merge pull request #480 from logos-blockchain/moudy/wallet-crypto-bench-tool
This commit is contained in:
commit
aa53e591d8
12
Cargo.lock
generated
12
Cargo.lock
generated
@ -1951,6 +1951,18 @@ dependencies = [
|
|||||||
"hybrid-array",
|
"hybrid-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto_primitives_bench"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"key_protocol",
|
||||||
|
"nssa_core",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ctr"
|
name = "ctr"
|
||||||
version = "0.9.2"
|
version = "0.9.2"
|
||||||
|
|||||||
@ -42,6 +42,7 @@ members = [
|
|||||||
"testnet_initial_state",
|
"testnet_initial_state",
|
||||||
"indexer/ffi",
|
"indexer/ffi",
|
||||||
"tools/cycle_bench",
|
"tools/cycle_bench",
|
||||||
|
"tools/crypto_primitives_bench",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
|||||||
@ -5,5 +5,6 @@ Bench tools live under `tools/` with READMEs for how to run each one. This direc
|
|||||||
| Bench | Doc |
|
| Bench | Doc |
|
||||||
|---|---|
|
|---|---|
|
||||||
| cycle_bench | [cycle_bench.md](cycle_bench.md) |
|
| cycle_bench | [cycle_bench.md](cycle_bench.md) |
|
||||||
|
| crypto_primitives_bench | [crypto_primitives_bench.md](crypto_primitives_bench.md) |
|
||||||
|
|
||||||
All numbers are from a single M2 Pro dev box unless noted otherwise.
|
All numbers are from a single M2 Pro dev box unless noted otherwise.
|
||||||
|
|||||||
43
docs/benchmarks/crypto_primitives_bench.md
Normal file
43
docs/benchmarks/crypto_primitives_bench.md
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# crypto_primitives_bench
|
||||||
|
|
||||||
|
Cryptographic primitives used by client/wallet code. 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 crypto_primitives_bench
|
||||||
|
```
|
||||||
|
|
||||||
|
JSON output: `target/crypto_primitives_bench.json`.
|
||||||
|
|
||||||
|
## Caveats
|
||||||
|
|
||||||
|
- Single-thread, no SIMD acceleration. Bench dev box uses the pure-Rust secp256k1 backend.
|
||||||
18
tools/crypto_primitives_bench/Cargo.toml
Normal file
18
tools/crypto_primitives_bench/Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "crypto_primitives_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
|
||||||
|
rand = { workspace = true }
|
||||||
20
tools/crypto_primitives_bench/README.md
Normal file
20
tools/crypto_primitives_bench/README.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# crypto_primitives_bench
|
||||||
|
|
||||||
|
Cryptographic primitive microbenchmarks used by client/wallet code. Single host binary, no live sequencer or Bedrock needed.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run --release -p crypto_primitives_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/crypto_primitives_bench.json`.
|
||||||
175
tools/crypto_primitives_bench/src/main.rs
Normal file
175
tools/crypto_primitives_bench/src/main.rs
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
//! 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<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;
|
||||||
|
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!(
|
||||||
|
"{:<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("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(())
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user