1//! sigbench — WP34 Research Axis B benchmark binary.2//! Everything is synthetic: keys, messages and aux randomness come from a ChaCha20 RNG seeded3//! with `--seed`. Output: one JSON document on stdout (provenance is added by run_bench.py).45mod msm;6mod stats;7mod vectors;89use clap::Parser;10use k256::elliptic_curve::ff::PrimeField;11use k256::elliptic_curve::ops::MulVartime;12use k256::{FieldBytes, ProjectivePoint, Scalar};13use msm::{MsmAlgo, OpCounts, SigItem};14use rand::{RngCore, SeedableRng};15use rand_chacha::ChaCha20Rng;16use secp256k1::{17 ecdsa as secp_ecdsa, schnorr as secp_schnorr, Keypair, Message, PublicKey, SecretKey, XOnlyPublicKey,18};19use serde_json::{json, Value};20use stats::Timing;21use std::hint::black_box;22use std::time::Instant;23use vectors::to_hex;2425#[derive(Parser, Debug)]26#[command(name = "sigbench", about = "ECDSA / BIP340 Schnorr benchmarks (WP34 Axis B)")]27struct Args {28 /// RNG seed (ChaCha20) for all synthetic keys, messages, aux randomness and batch weights29 #[arg(long, default_value_t = 34)]30 seed: u64,31 /// repetitions per measurement (median / IQR reported)32 #[arg(long, default_value_t = 7)]33 reps: usize,34 /// operations per repetition for sign/verify/scalar-mult benchmarks35 #[arg(long, default_value_t = 2000)]36 batch_ops: usize,37 /// minimum signatures processed per repetition in the batch-verification benchmark38 #[arg(long, default_value_t = 2000)]39 batch_min_sigs: usize,40 #[arg(long, default_value = "1,2,4,8,16,32,64,128,256,512,1024,2048")]41 batch_sizes: String,42 /// path to bip340_test_vectors.csv43 #[arg(long)]44 vectors: String,45 /// write (seckey, msg, sig) triples for the Python cross-check here46 #[arg(long)]47 crosscheck_out: Option<String>,48 #[arg(long, default_value_t = 1000)]49 crosscheck_n: usize,50 #[arg(long, default_value_t = 200)]51 python_crosscheck_n: usize,52 #[arg(long, default_value_t = 2000)]53 bootstrap: usize,54 /// skip the constant-time k256 lincomb above this batch size (slow; not a verifier choice)55 #[arg(long, default_value_t = 256)]56 ct_lincomb_max_batch: usize,57 /// force the Pippenger window width (0 = size heuristic pippenger_window(2b+1))58 #[arg(long, default_value_t = 0)]59 pippenger_c: u32,60}6162fn rand32(rng: &mut ChaCha20Rng) -> [u8; 32] {63 let mut b = [0u8; 32];64 rng.fill_bytes(&mut b);65 b66}6768/// Draw a valid secret key (1 <= d < n) from the seeded RNG.69fn rand_seckey(rng: &mut ChaCha20Rng) -> [u8; 32] {70 loop {71 let b = rand32(rng);72 if SecretKey::from_secret_bytes(b).is_ok() {73 return b;74 }75 }76}7778/// Time `reps` repetitions of `ops` operations each; f(rep) runs one whole repetition.79/// One untimed warm-up call (rep index 0) precedes the timed repetitions.80fn bench<F: FnMut(usize)>(reps: usize, ops: usize, mut f: F) -> Timing {81 f(0);82 let mut samples = Vec::with_capacity(reps);83 for r in 0..reps {84 let t = Instant::now();85 f(r);86 samples.push(t.elapsed().as_nanos() as f64 / ops as f64);87 }88 Timing::from_samples(ops, samples)89}9091struct Pool {92 sk: Vec<[u8; 32]>,93 msg: Vec<[u8; 32]>,94 aux: Vec<[u8; 32]>,95 secp_sk: Vec<SecretKey>,96 secp_pk: Vec<PublicKey>,97 secp_kp: Vec<Keypair>,98 secp_xo: Vec<XOnlyPublicKey>,99 k_ecdsa_sk: Vec<k256::ecdsa::SigningKey>,100 k_ecdsa_vk: Vec<k256::ecdsa::VerifyingKey>,101 k_schnorr_sk: Vec<k256::schnorr::SigningKey>,102 k_schnorr_vk: Vec<k256::schnorr::VerifyingKey>,103}104105fn make_pool(rng: &mut ChaCha20Rng, n: usize) -> Pool {106 let mut p = Pool {107 sk: vec![],108 msg: vec![],109 aux: vec![],110 secp_sk: vec![],111 secp_pk: vec![],112 secp_kp: vec![],113 secp_xo: vec![],114 k_ecdsa_sk: vec![],115 k_ecdsa_vk: vec![],116 k_schnorr_sk: vec![],117 k_schnorr_vk: vec![],118 };119 for _ in 0..n {120 let sk = rand_seckey(rng);121 let msg = rand32(rng);122 let aux = rand32(rng);123 let ssk = SecretKey::from_secret_bytes(sk).unwrap();124 let kp = Keypair::from_secret_key(&ssk);125 p.secp_pk.push(PublicKey::from_secret_key(&ssk));126 p.secp_xo.push(kp.x_only_public_key().0);127 p.secp_kp.push(kp);128 p.secp_sk.push(ssk);129 let ek = k256::ecdsa::SigningKey::from_bytes(&FieldBytes::from(sk)).unwrap();130 p.k_ecdsa_vk.push(*ek.verifying_key());131 p.k_ecdsa_sk.push(ek);132 let sk2 = k256::schnorr::SigningKey::from_bytes(&FieldBytes::from(sk)).unwrap();133 p.k_schnorr_vk.push(*sk2.verifying_key());134 p.k_schnorr_sk.push(sk2);135 p.sk.push(sk);136 p.msg.push(msg);137 p.aux.push(aux);138 }139 p140}141142fn timing_json(t: &Timing) -> Value {143 serde_json::to_value(t).unwrap()144}145146fn main() {147 let args = Args::parse();148 let t_start = Instant::now();149 let mut rng = ChaCha20Rng::seed_from_u64(args.seed);150 let mut out = serde_json::Map::new();151 out.insert("binary".into(), json!("sigbench"));152 out.insert("seed".into(), json!(args.seed));153 out.insert(154 "args".into(),155 json!({156 "reps": args.reps, "batch_ops": args.batch_ops, "batch_min_sigs": args.batch_min_sigs,157 "batch_sizes": args.batch_sizes, "crosscheck_n": args.crosscheck_n,158 "python_crosscheck_n": args.python_crosscheck_n, "bootstrap": args.bootstrap,159 "ct_lincomb_max_batch": args.ct_lincomb_max_batch, "pippenger_c": args.pippenger_c,160 }),161 );162 out.insert("threading".into(), json!("single thread (all work on the main thread; no rayon; macOS offers no hard CPU pinning from user space - affinity tags are hints only)"));163164 // ------------------------------------------------------------ 1a. BIP340 vectors165 let csv = std::fs::read_to_string(&args.vectors).expect("read vectors csv");166 let vs = vectors::parse_csv(&csv);167 let v_secp = vectors::check_secp(&vs);168 let v_k256 = vectors::check_k256(&vs);169 eprintln!(170 "[vectors] secp all_pass={} ({} verify, {}/{} sign) | k256 all_pass={} ({} verify, {}/{} sign)",171 v_secp.all_pass, v_secp.verify_pass, v_secp.sign_pass, v_secp.sign_cases,172 v_k256.all_pass, v_k256.verify_pass, v_k256.sign_pass, v_k256.sign_cases173 );174 out.insert(175 "bip340_vectors".into(),176 json!({177 "file": args.vectors, "n_vectors": vs.len(),178 "libsecp256k1": serde_json::to_value(&v_secp).unwrap(),179 "k256": serde_json::to_value(&v_k256).unwrap(),180 "both_all_pass": v_secp.all_pass && v_k256.all_pass,181 }),182 );183184 // ------------------------------------------------------------ 1b. cross-library checks185 use k256::ecdsa::signature::hazmat::{PrehashSigner, PrehashVerifier};186 let n_cc = args.crosscheck_n;187 let cc_pool = make_pool(&mut rng, n_cc);188 let mut ecdsa_k_to_secp = 0usize;189 let mut ecdsa_secp_to_k = 0usize;190 let mut ecdsa_identical = 0usize;191 let mut schnorr_k_to_secp = 0usize;192 let mut schnorr_secp_to_k = 0usize;193 let mut schnorr_identical = 0usize;194 let mut dump = Vec::new();195 for i in 0..n_cc {196 let msg = cc_pool.msg[i];197 // ECDSA: k256 sign (RFC 6979, SHA-256, low-S) -> libsecp256k1 verify198 let ksig: k256::ecdsa::Signature = cc_pool.k_ecdsa_sk[i].sign_prehash(&msg).unwrap();199 let kbytes: [u8; 64] = ksig.to_bytes().into();200 let ssig = secp_ecdsa::Signature::from_compact(&kbytes).unwrap();201 if secp_ecdsa::verify(&ssig, Message::from_digest(msg), &cc_pool.secp_pk[i]).is_ok() {202 ecdsa_k_to_secp += 1;203 }204 // ECDSA: libsecp256k1 sign -> k256 verify205 let ssig2 = secp_ecdsa::sign(Message::from_digest(msg), &cc_pool.secp_sk[i]);206 let sbytes = ssig2.serialize_compact();207 let ksig2 = k256::ecdsa::Signature::from_slice(&sbytes).unwrap();208 if cc_pool.k_ecdsa_vk[i].verify_prehash(&msg, &ksig2).is_ok() {209 ecdsa_secp_to_k += 1;210 }211 if sbytes == kbytes {212 ecdsa_identical += 1;213 }214 // Schnorr: k256 sign -> libsecp256k1 verify215 let aux = cc_pool.aux[i];216 let kss = cc_pool.k_schnorr_sk[i].sign_raw(&msg, &aux).unwrap();217 let kssb: [u8; 64] = kss.to_bytes().into();218 let s_sig = secp_schnorr::Signature::from_byte_array(kssb);219 if secp_schnorr::verify(&s_sig, &msg, &cc_pool.secp_xo[i]).is_ok() {220 schnorr_k_to_secp += 1;221 }222 // Schnorr: libsecp256k1 sign -> k256 verify223 let s_sig2 = secp_schnorr::sign_with_aux_rand(&msg, &cc_pool.secp_kp[i], &aux);224 let s_sig2b = s_sig2.to_byte_array();225 let k_sig2 = k256::schnorr::Signature::from_slice(&s_sig2b).unwrap();226 if cc_pool.k_schnorr_vk[i].verify_raw(&msg, &k_sig2).is_ok() {227 schnorr_secp_to_k += 1;228 }229 if s_sig2b == kssb {230 schnorr_identical += 1;231 }232 if i < args.python_crosscheck_n {233 dump.push(json!({234 "i": i,235 "seckey": to_hex(&cc_pool.sk[i]),236 "msg": to_hex(&msg),237 "aux_rand": to_hex(&aux),238 "pubkey_uncompressed": to_hex(&cc_pool.secp_pk[i].serialize_uncompressed()),239 "pubkey_xonly": to_hex(&cc_pool.secp_xo[i].to_byte_array()),240 "ecdsa_sig_k256": to_hex(&kbytes),241 "ecdsa_sig_libsecp256k1": to_hex(&sbytes),242 "schnorr_sig_k256": to_hex(&kssb),243 "schnorr_sig_libsecp256k1": to_hex(&s_sig2b),244 }));245 }246 }247 eprintln!(248 "[crosscheck] ecdsa k->secp {}/{} secp->k {}/{} identical {} | schnorr k->secp {}/{} secp->k {}/{} identical {}",249 ecdsa_k_to_secp, n_cc, ecdsa_secp_to_k, n_cc, ecdsa_identical,250 schnorr_k_to_secp, n_cc, schnorr_secp_to_k, n_cc, schnorr_identical251 );252 out.insert(253 "cross_library_checks".into(),254 json!({255 "n": n_cc,256 "ecdsa_k256_sign_libsecp_verify_pass": ecdsa_k_to_secp,257 "ecdsa_libsecp_sign_k256_verify_pass": ecdsa_secp_to_k,258 "ecdsa_signatures_byte_identical": ecdsa_identical,259 "schnorr_k256_sign_libsecp_verify_pass": schnorr_k_to_secp,260 "schnorr_libsecp_sign_k256_verify_pass": schnorr_secp_to_k,261 "schnorr_signatures_byte_identical": schnorr_identical,262 "all_pass": ecdsa_k_to_secp == n_cc && ecdsa_secp_to_k == n_cc263 && schnorr_k_to_secp == n_cc && schnorr_secp_to_k == n_cc,264 }),265 );266 if let Some(path) = &args.crosscheck_out {267 std::fs::write(268 path,269 serde_json::to_string_pretty(&json!({270 "seed": args.seed, "n": dump.len(),271 "note": "synthetic keys from ChaCha20(seed); verified against src/wp34 by tests/test_sig_crosscheck.py",272 "items": dump,273 }))274 .unwrap(),275 )276 .expect("write crosscheck dump");277 }278279 // ------------------------------------------------------------ 2. sign / verify benchmarks280 let n = args.batch_ops;281 let pool = make_pool(&mut rng, n);282 let mut sv = serde_json::Map::new();283284 let msgs: Vec<Message> = pool.msg.iter().map(|m| Message::from_digest(*m)).collect();285 let mut secp_ecdsa_sigs: Vec<secp_ecdsa::Signature> = Vec::with_capacity(n);286 let t = bench(args.reps, n, |_| {287 secp_ecdsa_sigs.clear();288 for i in 0..n {289 secp_ecdsa_sigs.push(black_box(secp_ecdsa::sign(msgs[i], &pool.secp_sk[i])));290 }291 });292 sv.insert("ecdsa_sign_libsecp256k1".into(), timing_json(&t));293 let t = bench(args.reps, n, |_| {294 for i in 0..n {295 assert!(black_box(secp_ecdsa::verify(&secp_ecdsa_sigs[i], msgs[i], &pool.secp_pk[i])).is_ok());296 }297 });298 sv.insert("ecdsa_verify_libsecp256k1".into(), timing_json(&t));299300 let mut k_ecdsa_sigs: Vec<k256::ecdsa::Signature> = Vec::with_capacity(n);301 let t = bench(args.reps, n, |_| {302 k_ecdsa_sigs.clear();303 for i in 0..n {304 k_ecdsa_sigs.push(black_box(pool.k_ecdsa_sk[i].sign_prehash(&pool.msg[i]).unwrap()));305 }306 });307 sv.insert("ecdsa_sign_k256".into(), timing_json(&t));308 let t = bench(args.reps, n, |_| {309 for i in 0..n {310 assert!(black_box(pool.k_ecdsa_vk[i].verify_prehash(&pool.msg[i], &k_ecdsa_sigs[i])).is_ok());311 }312 });313 sv.insert("ecdsa_verify_k256".into(), timing_json(&t));314315 let mut secp_schnorr_sigs: Vec<secp_schnorr::Signature> = Vec::with_capacity(n);316 let t = bench(args.reps, n, |_| {317 secp_schnorr_sigs.clear();318 for i in 0..n {319 secp_schnorr_sigs.push(black_box(secp_schnorr::sign_with_aux_rand(320 &pool.msg[i],321 &pool.secp_kp[i],322 &pool.aux[i],323 )));324 }325 });326 sv.insert("schnorr_sign_libsecp256k1".into(), timing_json(&t));327 let t = bench(args.reps, n, |_| {328 for i in 0..n {329 assert!(black_box(secp_schnorr::verify(&secp_schnorr_sigs[i], &pool.msg[i], &pool.secp_xo[i])).is_ok());330 }331 });332 let secp_schnorr_verify = t.clone();333 sv.insert("schnorr_verify_libsecp256k1".into(), timing_json(&t));334335 let mut k_schnorr_sigs: Vec<k256::schnorr::Signature> = Vec::with_capacity(n);336 let t = bench(args.reps, n, |_| {337 k_schnorr_sigs.clear();338 for i in 0..n {339 k_schnorr_sigs.push(black_box(pool.k_schnorr_sk[i].sign_raw(&pool.msg[i], &pool.aux[i]).unwrap()));340 }341 });342 sv.insert("schnorr_sign_k256".into(), timing_json(&t));343 let t = bench(args.reps, n, |_| {344 for i in 0..n {345 assert!(black_box(pool.k_schnorr_vk[i].verify_raw(&pool.msg[i], &k_schnorr_sigs[i])).is_ok());346 }347 });348 let k256_schnorr_verify = t.clone();349 sv.insert("schnorr_verify_k256".into(), timing_json(&t));350 for (k, v) in sv.iter() {351 eprintln!(352 "[sign/verify] {:<28} {:>10.1} ns/op (IQR {:.1})",353 k,354 v["median_ns"].as_f64().unwrap(),355 v["iqr_ns"].as_f64().unwrap()356 );357 }358 out.insert("sign_verify".into(), Value::Object(sv));359360 // ------------------------------------------------------------ 4. scalar multiplication361 let mut sm = serde_json::Map::new();362 let scalars: Vec<Scalar> = pool.sk.iter().map(|b| Scalar::from_repr(FieldBytes::from(*b)).unwrap()).collect();363 let points: Vec<ProjectivePoint> = pool.k_ecdsa_vk.iter().map(|vk| ProjectivePoint::from(*vk.as_affine())).collect();364 let g = ProjectivePoint::GENERATOR;365 let t = bench(args.reps, n, |_| {366 for i in 0..n {367 black_box(g * scalars[i]);368 }369 });370 sm.insert("k256_generator_mul_ct_glv".into(), timing_json(&t));371 let t = bench(args.reps, n, |_| {372 for i in 0..n {373 black_box(points[i] * scalars[i]);374 }375 });376 sm.insert("k256_point_mul_ct_glv".into(), timing_json(&t));377 let t = bench(args.reps, n, |_| {378 for i in 0..n {379 black_box(ProjectivePoint::mul_by_generator(&scalars[i]));380 }381 });382 sm.insert("k256_mul_by_generator_precomputed_table".into(), timing_json(&t));383 let t = bench(args.reps, n, |_| {384 for i in 0..n {385 black_box(points[i].mul_vartime(&scalars[i]));386 }387 });388 sm.insert("k256_point_mul_vartime_glv_wnaf".into(), timing_json(&t));389 let t = bench(args.reps, n, |_| {390 for i in 0..n {391 black_box(ProjectivePoint::mul_by_generator_vartime(&scalars[i]));392 }393 });394 sm.insert("k256_mul_by_generator_vartime_table".into(), timing_json(&t));395 // own wNAF-5 double-and-add (no GLV), with op counts396 let mut own_counts = OpCounts::default();397 let t = bench(args.reps, n, |r| {398 for i in 0..n {399 let mut c = OpCounts::default();400 black_box(msm::straus(&[(points[i], scalars[i])], 5, &mut c));401 if r == 0 && i == 0 {402 own_counts = c;403 }404 }405 });406 let mut tj = timing_json(&t);407 tj["op_counts_example"] = serde_json::to_value(own_counts).unwrap();408 sm.insert("own_wnaf5_point_mul_no_glv".into(), tj);409 let mut c = OpCounts::default();410 let own_ok = (0..64).all(|i| msm::straus(&[(points[i], scalars[i])], 5, &mut c) == points[i] * scalars[i]);411 sm.insert("own_wnaf5_agrees_with_k256_64_cases".into(), json!(own_ok));412 let secp_scalars: Vec<secp256k1::Scalar> =413 pool.sk.iter().map(|b| secp256k1::Scalar::from_be_bytes(*b).unwrap()).collect();414 let t = bench(args.reps, n, |_| {415 for i in 0..n {416 black_box(PublicKey::from_secret_key(&pool.secp_sk[i]));417 }418 });419 sm.insert("libsecp256k1_generator_mul_ecmult_gen".into(), timing_json(&t));420 let t = bench(args.reps, n, |_| {421 for i in 0..n {422 black_box(pool.secp_pk[i].mul_tweak(&secp_scalars[i]).unwrap());423 }424 });425 sm.insert("libsecp256k1_point_mul_tweak_ecmult_const".into(), timing_json(&t));426 sm.insert("glv_note".into(), json!("k256: every constant-time multiplication goes through lincomb(), which always applies the GLV decomposition (no public feature toggle); the vartime path uses GLV + wNAF-5; the only non-GLV reference is our own wNAF-5 double-and-add above. libsecp256k1: the GLV endomorphism has been unconditional since v0.1 (USE_ENDOMORPHISM removed) and ecmult is not exposed by rust-secp256k1's public API; mul_tweak maps to secp256k1_ec_pubkey_tweak_mul (constant-time ecmult_const) and from_secret_key to ecmult_gen (precomputed generator table)."));427 for (k, v) in sm.iter() {428 if let Some(m) = v.get("median_ns") {429 eprintln!("[scalar-mul] {:<44} {:>10.1} ns/op", k, m.as_f64().unwrap());430 }431 }432 out.insert("scalar_mult".into(), Value::Object(sm));433434 // ------------------------------------------------------------ 3. batch verification435 let th = msm::tagged_hash_prefix("BIP0340/challenge");436 let sizes: Vec<usize> = args.batch_sizes.split(',').map(|s| s.trim().parse().unwrap()).collect();437 let max_b = *sizes.iter().max().unwrap();438 let pool_sz = max_b.max(args.batch_min_sigs);439 let bpool = make_pool(&mut rng, pool_sz);440 let items: Vec<SigItem> = (0..pool_sz)441 .map(|i| {442 let sig = secp_schnorr::sign_with_aux_rand(&bpool.msg[i], &bpool.secp_kp[i], &bpool.aux[i]).to_byte_array();443 let pk_x = bpool.secp_xo[i].to_byte_array();444 SigItem { pk_x, p: msm::lift_x(&pk_x).unwrap(), msg: bpool.msg[i], sig }445 })446 .collect();447 let mut weights: Vec<[u8; 16]> = Vec::with_capacity(pool_sz);448 for _ in 0..pool_sz {449 let mut w = [0u8; 16];450 rng.fill_bytes(&mut w);451 weights.push(w);452 }453454 let algos = [MsmAlgo::Straus, MsmAlgo::Pippenger, MsmAlgo::K256LincombVartime, MsmAlgo::K256LincombCt];455 let mut batch_results = Vec::new();456 for &b in &sizes {457 let n_batches = (args.batch_min_sigs + b - 1) / b;458 let sigs_per_rep = n_batches * b;459 for algo in algos {460 if algo == MsmAlgo::K256LincombCt && b > args.ct_lincomb_max_batch {461 continue;462 }463 let mut counts = OpCounts::default();464 let mut n_terms = 0usize;465 let mut window = 0u32;466 let mut msm_samples: Vec<f64> = Vec::new();467 let t = bench(args.reps, sigs_per_rep, |r| {468 let mut msm_ns: u128 = 0;469 for j in 0..n_batches {470 let start = ((r * n_batches + j) * b) % pool_sz;471 let mut refs: Vec<&SigItem> = Vec::with_capacity(b);472 for k in 0..b {473 refs.push(&items[(start + k) % pool_sz]);474 }475 let wstart = start % (pool_sz - b + 1);476 let o = msm::batch_verify(&refs, &weights[wstart..wstart + b], algo, &th, args.pippenger_c);477 assert!(o.valid, "valid batch rejected (b={}, algo={:?})", b, algo);478 if j == 0 {479 counts = o.counts;480 n_terms = o.n_terms;481 window = o.window;482 }483 msm_ns += o.msm_ns;484 }485 msm_samples.push(msm_ns as f64 / sigs_per_rep as f64);486 });487 // first msm sample belongs to the untimed warm-up call488 let msm_samples: Vec<f64> = msm_samples.into_iter().skip(1).collect();489 let s_k256 = stats::bootstrap_ratio_of_medians(490 &k256_schnorr_verify.ns_per_op_samples,491 &t.ns_per_op_samples,492 args.bootstrap,493 args.seed ^ (b as u64) ^ 0x51,494 );495 let s_secp = stats::bootstrap_ratio_of_medians(496 &secp_schnorr_verify.ns_per_op_samples,497 &t.ns_per_op_samples,498 args.bootstrap,499 args.seed ^ (b as u64) ^ 0x52,500 );501 eprintln!(502 "[batch] b={:<5} {:<22} {:>9.1} ns/sig (msm {:>9.1}) S_k256={:.2} [{:.2},{:.2}] S_secp={:.2} ops/batch: {} adds {} mixed {} dbl (terms {}, w={})",503 b, algo.name(), t.median_ns, stats::median(&msm_samples), s_k256.point, s_k256.ci95_low, s_k256.ci95_high,504 s_secp.point, counts.adds, counts.mixed_adds, counts.doublings, n_terms, window505 );506 batch_results.push(json!({507 "batch_size": b, "algorithm": algo.name(), "n_batches_per_rep": n_batches, "sigs_per_rep": sigs_per_rep,508 "per_signature": timing_json(&t),509 "msm_only_ns_per_sig_median": stats::median(&msm_samples),510 "msm_only_ns_per_sig_samples": msm_samples,511 "n_terms": n_terms, "window_bits": window,512 "op_counts_per_batch": serde_json::to_value(counts).unwrap(),513 "op_counts_per_signature": {514 "adds": counts.adds as f64 / b as f64, "mixed_adds": counts.mixed_adds as f64 / b as f64,515 "doublings": counts.doublings as f64 / b as f64, "total": counts.total() as f64 / b as f64,516 },517 "speedup_vs_k256_single_verify": serde_json::to_value(&s_k256).unwrap(),518 "speedup_vs_libsecp256k1_single_verify": serde_json::to_value(&s_secp).unwrap(),519 }));520 }521 }522523 // negative controls: one bad signature in a batch of 64 must make every algorithm reject524 let nb = 64.min(pool_sz);525 let mut neg = serde_json::Map::new();526 let mut all_rejected = true;527 for (name, mutate) in [("s_plus_one", 0u8), ("wrong_message", 1u8), ("r_plus_one", 2u8)] {528 let mut bad = SigItem { pk_x: items[7].pk_x, p: items[7].p, msg: items[7].msg, sig: items[7].sig };529 match mutate {530 0 => {531 let mut s = u128::from_be_bytes(bad.sig[48..64].try_into().unwrap());532 s = s.wrapping_add(1);533 bad.sig[48..64].copy_from_slice(&s.to_be_bytes());534 }535 1 => {536 bad.msg[0] ^= 0x01;537 }538 _ => {539 let mut r = u128::from_be_bytes(bad.sig[16..32].try_into().unwrap());540 r = r.wrapping_add(1);541 bad.sig[16..32].copy_from_slice(&r.to_be_bytes());542 }543 }544 let mut per_algo = serde_json::Map::new();545 for algo in algos {546 let mut refs: Vec<&SigItem> = (0..nb).map(|k| &items[k]).collect();547 refs[7] = &bad;548 let o = msm::batch_verify(&refs, &weights[..nb], algo, &th, args.pippenger_c);549 per_algo.insert(algo.name().into(), json!(!o.valid));550 if o.valid {551 all_rejected = false;552 }553 }554 let refs: Vec<&SigItem> = (0..nb).map(|k| &items[k]).collect();555 let pos = msm::batch_verify(&refs, &weights[..nb], MsmAlgo::Pippenger, &th, args.pippenger_c).valid;556 per_algo.insert("unmodified_batch_accepted".into(), json!(pos));557 if !pos {558 all_rejected = false;559 }560 neg.insert(name.into(), Value::Object(per_algo));561 }562 eprintln!("[negative control] all invalid batches rejected = {}", all_rejected);563 out.insert(564 "batch_verification".into(),565 json!({566 "equation": "sum_i a_i s_i G = sum_i a_i R_i + sum_i a_i e_i P_i, a_1 = 1, a_i uniform 128-bit from ChaCha20(seed); R_i = lift_x(r_i), P_i = lift_x(pk_i); merged into one MSM with 2b+1 terms whose result must be the identity",567 "msm_algorithms": {568 "straus_wnaf5": "own implementation: interleaved Straus with width-5 wNAF recoding, per-term odd-multiple tables (1 dbl + 7 adds), projective additions",569 "pippenger": "own implementation: bucket method with c-bit windows (c = pippenger_window(2b+1)), mixed additions into buckets, running-sum trick",570 "k256_lincomb_vartime": "k256 0.14 library: GLV decomposition + wNAF-5 multiscalar (wnaf crate), variable time",571 "k256_lincomb_ct": "k256 0.14 library: constant-time GLV + radix-16 fixed-window interleaving (benchmarked up to ct_lincomb_max_batch)"572 },573 "single_verify_reference_ns": {574 "k256_verify_raw": k256_schnorr_verify.median_ns,575 "libsecp256k1_schnorr_verify": secp_schnorr_verify.median_ns576 },577 "sizes": sizes, "results": batch_results,578 "negative_controls": {"batch_size": nb, "all_invalid_batches_rejected": all_rejected, "cases": Value::Object(neg)},579 }),580 );581582 out.insert("elapsed_s".into(), json!(t_start.elapsed().as_secs_f64()));583 println!("{}", serde_json::to_string(&Value::Object(out)).unwrap());584}585