//! sigbench — WP34 Research Axis B benchmark binary. //! Everything is synthetic: keys, messages and aux randomness come from a ChaCha20 RNG seeded //! with `--seed`. Output: one JSON document on stdout (provenance is added by run_bench.py). mod msm; mod stats; mod vectors; use clap::Parser; use k256::elliptic_curve::ff::PrimeField; use k256::elliptic_curve::ops::MulVartime; use k256::{FieldBytes, ProjectivePoint, Scalar}; use msm::{MsmAlgo, OpCounts, SigItem}; use rand::{RngCore, SeedableRng}; use rand_chacha::ChaCha20Rng; use secp256k1::{ ecdsa as secp_ecdsa, schnorr as secp_schnorr, Keypair, Message, PublicKey, SecretKey, XOnlyPublicKey, }; use serde_json::{json, Value}; use stats::Timing; use std::hint::black_box; use std::time::Instant; use vectors::to_hex; #[derive(Parser, Debug)] #[command(name = "sigbench", about = "ECDSA / BIP340 Schnorr benchmarks (WP34 Axis B)")] struct Args { /// RNG seed (ChaCha20) for all synthetic keys, messages, aux randomness and batch weights #[arg(long, default_value_t = 34)] seed: u64, /// repetitions per measurement (median / IQR reported) #[arg(long, default_value_t = 7)] reps: usize, /// operations per repetition for sign/verify/scalar-mult benchmarks #[arg(long, default_value_t = 2000)] batch_ops: usize, /// minimum signatures processed per repetition in the batch-verification benchmark #[arg(long, default_value_t = 2000)] batch_min_sigs: usize, #[arg(long, default_value = "1,2,4,8,16,32,64,128,256,512,1024,2048")] batch_sizes: String, /// path to bip340_test_vectors.csv #[arg(long)] vectors: String, /// write (seckey, msg, sig) triples for the Python cross-check here #[arg(long)] crosscheck_out: Option, #[arg(long, default_value_t = 1000)] crosscheck_n: usize, #[arg(long, default_value_t = 200)] python_crosscheck_n: usize, #[arg(long, default_value_t = 2000)] bootstrap: usize, /// skip the constant-time k256 lincomb above this batch size (slow; not a verifier choice) #[arg(long, default_value_t = 256)] ct_lincomb_max_batch: usize, /// force the Pippenger window width (0 = size heuristic pippenger_window(2b+1)) #[arg(long, default_value_t = 0)] pippenger_c: u32, } fn rand32(rng: &mut ChaCha20Rng) -> [u8; 32] { let mut b = [0u8; 32]; rng.fill_bytes(&mut b); b } /// Draw a valid secret key (1 <= d < n) from the seeded RNG. fn rand_seckey(rng: &mut ChaCha20Rng) -> [u8; 32] { loop { let b = rand32(rng); if SecretKey::from_secret_bytes(b).is_ok() { return b; } } } /// Time `reps` repetitions of `ops` operations each; f(rep) runs one whole repetition. /// One untimed warm-up call (rep index 0) precedes the timed repetitions. fn bench(reps: usize, ops: usize, mut f: F) -> Timing { f(0); let mut samples = Vec::with_capacity(reps); for r in 0..reps { let t = Instant::now(); f(r); samples.push(t.elapsed().as_nanos() as f64 / ops as f64); } Timing::from_samples(ops, samples) } struct Pool { sk: Vec<[u8; 32]>, msg: Vec<[u8; 32]>, aux: Vec<[u8; 32]>, secp_sk: Vec, secp_pk: Vec, secp_kp: Vec, secp_xo: Vec, k_ecdsa_sk: Vec, k_ecdsa_vk: Vec, k_schnorr_sk: Vec, k_schnorr_vk: Vec, } fn make_pool(rng: &mut ChaCha20Rng, n: usize) -> Pool { let mut p = Pool { sk: vec![], msg: vec![], aux: vec![], secp_sk: vec![], secp_pk: vec![], secp_kp: vec![], secp_xo: vec![], k_ecdsa_sk: vec![], k_ecdsa_vk: vec![], k_schnorr_sk: vec![], k_schnorr_vk: vec![], }; for _ in 0..n { let sk = rand_seckey(rng); let msg = rand32(rng); let aux = rand32(rng); let ssk = SecretKey::from_secret_bytes(sk).unwrap(); let kp = Keypair::from_secret_key(&ssk); p.secp_pk.push(PublicKey::from_secret_key(&ssk)); p.secp_xo.push(kp.x_only_public_key().0); p.secp_kp.push(kp); p.secp_sk.push(ssk); let ek = k256::ecdsa::SigningKey::from_bytes(&FieldBytes::from(sk)).unwrap(); p.k_ecdsa_vk.push(*ek.verifying_key()); p.k_ecdsa_sk.push(ek); let sk2 = k256::schnorr::SigningKey::from_bytes(&FieldBytes::from(sk)).unwrap(); p.k_schnorr_vk.push(*sk2.verifying_key()); p.k_schnorr_sk.push(sk2); p.sk.push(sk); p.msg.push(msg); p.aux.push(aux); } p } fn timing_json(t: &Timing) -> Value { serde_json::to_value(t).unwrap() } fn main() { let args = Args::parse(); let t_start = Instant::now(); let mut rng = ChaCha20Rng::seed_from_u64(args.seed); let mut out = serde_json::Map::new(); out.insert("binary".into(), json!("sigbench")); out.insert("seed".into(), json!(args.seed)); out.insert( "args".into(), json!({ "reps": args.reps, "batch_ops": args.batch_ops, "batch_min_sigs": args.batch_min_sigs, "batch_sizes": args.batch_sizes, "crosscheck_n": args.crosscheck_n, "python_crosscheck_n": args.python_crosscheck_n, "bootstrap": args.bootstrap, "ct_lincomb_max_batch": args.ct_lincomb_max_batch, "pippenger_c": args.pippenger_c, }), ); 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)")); // ------------------------------------------------------------ 1a. BIP340 vectors let csv = std::fs::read_to_string(&args.vectors).expect("read vectors csv"); let vs = vectors::parse_csv(&csv); let v_secp = vectors::check_secp(&vs); let v_k256 = vectors::check_k256(&vs); eprintln!( "[vectors] secp all_pass={} ({} verify, {}/{} sign) | k256 all_pass={} ({} verify, {}/{} sign)", v_secp.all_pass, v_secp.verify_pass, v_secp.sign_pass, v_secp.sign_cases, v_k256.all_pass, v_k256.verify_pass, v_k256.sign_pass, v_k256.sign_cases ); out.insert( "bip340_vectors".into(), json!({ "file": args.vectors, "n_vectors": vs.len(), "libsecp256k1": serde_json::to_value(&v_secp).unwrap(), "k256": serde_json::to_value(&v_k256).unwrap(), "both_all_pass": v_secp.all_pass && v_k256.all_pass, }), ); // ------------------------------------------------------------ 1b. cross-library checks use k256::ecdsa::signature::hazmat::{PrehashSigner, PrehashVerifier}; let n_cc = args.crosscheck_n; let cc_pool = make_pool(&mut rng, n_cc); let mut ecdsa_k_to_secp = 0usize; let mut ecdsa_secp_to_k = 0usize; let mut ecdsa_identical = 0usize; let mut schnorr_k_to_secp = 0usize; let mut schnorr_secp_to_k = 0usize; let mut schnorr_identical = 0usize; let mut dump = Vec::new(); for i in 0..n_cc { let msg = cc_pool.msg[i]; // ECDSA: k256 sign (RFC 6979, SHA-256, low-S) -> libsecp256k1 verify let ksig: k256::ecdsa::Signature = cc_pool.k_ecdsa_sk[i].sign_prehash(&msg).unwrap(); let kbytes: [u8; 64] = ksig.to_bytes().into(); let ssig = secp_ecdsa::Signature::from_compact(&kbytes).unwrap(); if secp_ecdsa::verify(&ssig, Message::from_digest(msg), &cc_pool.secp_pk[i]).is_ok() { ecdsa_k_to_secp += 1; } // ECDSA: libsecp256k1 sign -> k256 verify let ssig2 = secp_ecdsa::sign(Message::from_digest(msg), &cc_pool.secp_sk[i]); let sbytes = ssig2.serialize_compact(); let ksig2 = k256::ecdsa::Signature::from_slice(&sbytes).unwrap(); if cc_pool.k_ecdsa_vk[i].verify_prehash(&msg, &ksig2).is_ok() { ecdsa_secp_to_k += 1; } if sbytes == kbytes { ecdsa_identical += 1; } // Schnorr: k256 sign -> libsecp256k1 verify let aux = cc_pool.aux[i]; let kss = cc_pool.k_schnorr_sk[i].sign_raw(&msg, &aux).unwrap(); let kssb: [u8; 64] = kss.to_bytes().into(); let s_sig = secp_schnorr::Signature::from_byte_array(kssb); if secp_schnorr::verify(&s_sig, &msg, &cc_pool.secp_xo[i]).is_ok() { schnorr_k_to_secp += 1; } // Schnorr: libsecp256k1 sign -> k256 verify let s_sig2 = secp_schnorr::sign_with_aux_rand(&msg, &cc_pool.secp_kp[i], &aux); let s_sig2b = s_sig2.to_byte_array(); let k_sig2 = k256::schnorr::Signature::from_slice(&s_sig2b).unwrap(); if cc_pool.k_schnorr_vk[i].verify_raw(&msg, &k_sig2).is_ok() { schnorr_secp_to_k += 1; } if s_sig2b == kssb { schnorr_identical += 1; } if i < args.python_crosscheck_n { dump.push(json!({ "i": i, "seckey": to_hex(&cc_pool.sk[i]), "msg": to_hex(&msg), "aux_rand": to_hex(&aux), "pubkey_uncompressed": to_hex(&cc_pool.secp_pk[i].serialize_uncompressed()), "pubkey_xonly": to_hex(&cc_pool.secp_xo[i].to_byte_array()), "ecdsa_sig_k256": to_hex(&kbytes), "ecdsa_sig_libsecp256k1": to_hex(&sbytes), "schnorr_sig_k256": to_hex(&kssb), "schnorr_sig_libsecp256k1": to_hex(&s_sig2b), })); } } eprintln!( "[crosscheck] ecdsa k->secp {}/{} secp->k {}/{} identical {} | schnorr k->secp {}/{} secp->k {}/{} identical {}", ecdsa_k_to_secp, n_cc, ecdsa_secp_to_k, n_cc, ecdsa_identical, schnorr_k_to_secp, n_cc, schnorr_secp_to_k, n_cc, schnorr_identical ); out.insert( "cross_library_checks".into(), json!({ "n": n_cc, "ecdsa_k256_sign_libsecp_verify_pass": ecdsa_k_to_secp, "ecdsa_libsecp_sign_k256_verify_pass": ecdsa_secp_to_k, "ecdsa_signatures_byte_identical": ecdsa_identical, "schnorr_k256_sign_libsecp_verify_pass": schnorr_k_to_secp, "schnorr_libsecp_sign_k256_verify_pass": schnorr_secp_to_k, "schnorr_signatures_byte_identical": schnorr_identical, "all_pass": ecdsa_k_to_secp == n_cc && ecdsa_secp_to_k == n_cc && schnorr_k_to_secp == n_cc && schnorr_secp_to_k == n_cc, }), ); if let Some(path) = &args.crosscheck_out { std::fs::write( path, serde_json::to_string_pretty(&json!({ "seed": args.seed, "n": dump.len(), "note": "synthetic keys from ChaCha20(seed); verified against src/wp34 by tests/test_sig_crosscheck.py", "items": dump, })) .unwrap(), ) .expect("write crosscheck dump"); } // ------------------------------------------------------------ 2. sign / verify benchmarks let n = args.batch_ops; let pool = make_pool(&mut rng, n); let mut sv = serde_json::Map::new(); let msgs: Vec = pool.msg.iter().map(|m| Message::from_digest(*m)).collect(); let mut secp_ecdsa_sigs: Vec = Vec::with_capacity(n); let t = bench(args.reps, n, |_| { secp_ecdsa_sigs.clear(); for i in 0..n { secp_ecdsa_sigs.push(black_box(secp_ecdsa::sign(msgs[i], &pool.secp_sk[i]))); } }); sv.insert("ecdsa_sign_libsecp256k1".into(), timing_json(&t)); let t = bench(args.reps, n, |_| { for i in 0..n { assert!(black_box(secp_ecdsa::verify(&secp_ecdsa_sigs[i], msgs[i], &pool.secp_pk[i])).is_ok()); } }); sv.insert("ecdsa_verify_libsecp256k1".into(), timing_json(&t)); let mut k_ecdsa_sigs: Vec = Vec::with_capacity(n); let t = bench(args.reps, n, |_| { k_ecdsa_sigs.clear(); for i in 0..n { k_ecdsa_sigs.push(black_box(pool.k_ecdsa_sk[i].sign_prehash(&pool.msg[i]).unwrap())); } }); sv.insert("ecdsa_sign_k256".into(), timing_json(&t)); let t = bench(args.reps, n, |_| { for i in 0..n { assert!(black_box(pool.k_ecdsa_vk[i].verify_prehash(&pool.msg[i], &k_ecdsa_sigs[i])).is_ok()); } }); sv.insert("ecdsa_verify_k256".into(), timing_json(&t)); let mut secp_schnorr_sigs: Vec = Vec::with_capacity(n); let t = bench(args.reps, n, |_| { secp_schnorr_sigs.clear(); for i in 0..n { secp_schnorr_sigs.push(black_box(secp_schnorr::sign_with_aux_rand( &pool.msg[i], &pool.secp_kp[i], &pool.aux[i], ))); } }); sv.insert("schnorr_sign_libsecp256k1".into(), timing_json(&t)); let t = bench(args.reps, n, |_| { for i in 0..n { assert!(black_box(secp_schnorr::verify(&secp_schnorr_sigs[i], &pool.msg[i], &pool.secp_xo[i])).is_ok()); } }); let secp_schnorr_verify = t.clone(); sv.insert("schnorr_verify_libsecp256k1".into(), timing_json(&t)); let mut k_schnorr_sigs: Vec = Vec::with_capacity(n); let t = bench(args.reps, n, |_| { k_schnorr_sigs.clear(); for i in 0..n { k_schnorr_sigs.push(black_box(pool.k_schnorr_sk[i].sign_raw(&pool.msg[i], &pool.aux[i]).unwrap())); } }); sv.insert("schnorr_sign_k256".into(), timing_json(&t)); let t = bench(args.reps, n, |_| { for i in 0..n { assert!(black_box(pool.k_schnorr_vk[i].verify_raw(&pool.msg[i], &k_schnorr_sigs[i])).is_ok()); } }); let k256_schnorr_verify = t.clone(); sv.insert("schnorr_verify_k256".into(), timing_json(&t)); for (k, v) in sv.iter() { eprintln!( "[sign/verify] {:<28} {:>10.1} ns/op (IQR {:.1})", k, v["median_ns"].as_f64().unwrap(), v["iqr_ns"].as_f64().unwrap() ); } out.insert("sign_verify".into(), Value::Object(sv)); // ------------------------------------------------------------ 4. scalar multiplication let mut sm = serde_json::Map::new(); let scalars: Vec = pool.sk.iter().map(|b| Scalar::from_repr(FieldBytes::from(*b)).unwrap()).collect(); let points: Vec = pool.k_ecdsa_vk.iter().map(|vk| ProjectivePoint::from(*vk.as_affine())).collect(); let g = ProjectivePoint::GENERATOR; let t = bench(args.reps, n, |_| { for i in 0..n { black_box(g * scalars[i]); } }); sm.insert("k256_generator_mul_ct_glv".into(), timing_json(&t)); let t = bench(args.reps, n, |_| { for i in 0..n { black_box(points[i] * scalars[i]); } }); sm.insert("k256_point_mul_ct_glv".into(), timing_json(&t)); let t = bench(args.reps, n, |_| { for i in 0..n { black_box(ProjectivePoint::mul_by_generator(&scalars[i])); } }); sm.insert("k256_mul_by_generator_precomputed_table".into(), timing_json(&t)); let t = bench(args.reps, n, |_| { for i in 0..n { black_box(points[i].mul_vartime(&scalars[i])); } }); sm.insert("k256_point_mul_vartime_glv_wnaf".into(), timing_json(&t)); let t = bench(args.reps, n, |_| { for i in 0..n { black_box(ProjectivePoint::mul_by_generator_vartime(&scalars[i])); } }); sm.insert("k256_mul_by_generator_vartime_table".into(), timing_json(&t)); // own wNAF-5 double-and-add (no GLV), with op counts let mut own_counts = OpCounts::default(); let t = bench(args.reps, n, |r| { for i in 0..n { let mut c = OpCounts::default(); black_box(msm::straus(&[(points[i], scalars[i])], 5, &mut c)); if r == 0 && i == 0 { own_counts = c; } } }); let mut tj = timing_json(&t); tj["op_counts_example"] = serde_json::to_value(own_counts).unwrap(); sm.insert("own_wnaf5_point_mul_no_glv".into(), tj); let mut c = OpCounts::default(); let own_ok = (0..64).all(|i| msm::straus(&[(points[i], scalars[i])], 5, &mut c) == points[i] * scalars[i]); sm.insert("own_wnaf5_agrees_with_k256_64_cases".into(), json!(own_ok)); let secp_scalars: Vec = pool.sk.iter().map(|b| secp256k1::Scalar::from_be_bytes(*b).unwrap()).collect(); let t = bench(args.reps, n, |_| { for i in 0..n { black_box(PublicKey::from_secret_key(&pool.secp_sk[i])); } }); sm.insert("libsecp256k1_generator_mul_ecmult_gen".into(), timing_json(&t)); let t = bench(args.reps, n, |_| { for i in 0..n { black_box(pool.secp_pk[i].mul_tweak(&secp_scalars[i]).unwrap()); } }); sm.insert("libsecp256k1_point_mul_tweak_ecmult_const".into(), timing_json(&t)); 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).")); for (k, v) in sm.iter() { if let Some(m) = v.get("median_ns") { eprintln!("[scalar-mul] {:<44} {:>10.1} ns/op", k, m.as_f64().unwrap()); } } out.insert("scalar_mult".into(), Value::Object(sm)); // ------------------------------------------------------------ 3. batch verification let th = msm::tagged_hash_prefix("BIP0340/challenge"); let sizes: Vec = args.batch_sizes.split(',').map(|s| s.trim().parse().unwrap()).collect(); let max_b = *sizes.iter().max().unwrap(); let pool_sz = max_b.max(args.batch_min_sigs); let bpool = make_pool(&mut rng, pool_sz); let items: Vec = (0..pool_sz) .map(|i| { let sig = secp_schnorr::sign_with_aux_rand(&bpool.msg[i], &bpool.secp_kp[i], &bpool.aux[i]).to_byte_array(); let pk_x = bpool.secp_xo[i].to_byte_array(); SigItem { pk_x, p: msm::lift_x(&pk_x).unwrap(), msg: bpool.msg[i], sig } }) .collect(); let mut weights: Vec<[u8; 16]> = Vec::with_capacity(pool_sz); for _ in 0..pool_sz { let mut w = [0u8; 16]; rng.fill_bytes(&mut w); weights.push(w); } let algos = [MsmAlgo::Straus, MsmAlgo::Pippenger, MsmAlgo::K256LincombVartime, MsmAlgo::K256LincombCt]; let mut batch_results = Vec::new(); for &b in &sizes { let n_batches = (args.batch_min_sigs + b - 1) / b; let sigs_per_rep = n_batches * b; for algo in algos { if algo == MsmAlgo::K256LincombCt && b > args.ct_lincomb_max_batch { continue; } let mut counts = OpCounts::default(); let mut n_terms = 0usize; let mut window = 0u32; let mut msm_samples: Vec = Vec::new(); let t = bench(args.reps, sigs_per_rep, |r| { let mut msm_ns: u128 = 0; for j in 0..n_batches { let start = ((r * n_batches + j) * b) % pool_sz; let mut refs: Vec<&SigItem> = Vec::with_capacity(b); for k in 0..b { refs.push(&items[(start + k) % pool_sz]); } let wstart = start % (pool_sz - b + 1); let o = msm::batch_verify(&refs, &weights[wstart..wstart + b], algo, &th, args.pippenger_c); assert!(o.valid, "valid batch rejected (b={}, algo={:?})", b, algo); if j == 0 { counts = o.counts; n_terms = o.n_terms; window = o.window; } msm_ns += o.msm_ns; } msm_samples.push(msm_ns as f64 / sigs_per_rep as f64); }); // first msm sample belongs to the untimed warm-up call let msm_samples: Vec = msm_samples.into_iter().skip(1).collect(); let s_k256 = stats::bootstrap_ratio_of_medians( &k256_schnorr_verify.ns_per_op_samples, &t.ns_per_op_samples, args.bootstrap, args.seed ^ (b as u64) ^ 0x51, ); let s_secp = stats::bootstrap_ratio_of_medians( &secp_schnorr_verify.ns_per_op_samples, &t.ns_per_op_samples, args.bootstrap, args.seed ^ (b as u64) ^ 0x52, ); eprintln!( "[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={})", b, algo.name(), t.median_ns, stats::median(&msm_samples), s_k256.point, s_k256.ci95_low, s_k256.ci95_high, s_secp.point, counts.adds, counts.mixed_adds, counts.doublings, n_terms, window ); batch_results.push(json!({ "batch_size": b, "algorithm": algo.name(), "n_batches_per_rep": n_batches, "sigs_per_rep": sigs_per_rep, "per_signature": timing_json(&t), "msm_only_ns_per_sig_median": stats::median(&msm_samples), "msm_only_ns_per_sig_samples": msm_samples, "n_terms": n_terms, "window_bits": window, "op_counts_per_batch": serde_json::to_value(counts).unwrap(), "op_counts_per_signature": { "adds": counts.adds as f64 / b as f64, "mixed_adds": counts.mixed_adds as f64 / b as f64, "doublings": counts.doublings as f64 / b as f64, "total": counts.total() as f64 / b as f64, }, "speedup_vs_k256_single_verify": serde_json::to_value(&s_k256).unwrap(), "speedup_vs_libsecp256k1_single_verify": serde_json::to_value(&s_secp).unwrap(), })); } } // negative controls: one bad signature in a batch of 64 must make every algorithm reject let nb = 64.min(pool_sz); let mut neg = serde_json::Map::new(); let mut all_rejected = true; for (name, mutate) in [("s_plus_one", 0u8), ("wrong_message", 1u8), ("r_plus_one", 2u8)] { let mut bad = SigItem { pk_x: items[7].pk_x, p: items[7].p, msg: items[7].msg, sig: items[7].sig }; match mutate { 0 => { let mut s = u128::from_be_bytes(bad.sig[48..64].try_into().unwrap()); s = s.wrapping_add(1); bad.sig[48..64].copy_from_slice(&s.to_be_bytes()); } 1 => { bad.msg[0] ^= 0x01; } _ => { let mut r = u128::from_be_bytes(bad.sig[16..32].try_into().unwrap()); r = r.wrapping_add(1); bad.sig[16..32].copy_from_slice(&r.to_be_bytes()); } } let mut per_algo = serde_json::Map::new(); for algo in algos { let mut refs: Vec<&SigItem> = (0..nb).map(|k| &items[k]).collect(); refs[7] = &bad; let o = msm::batch_verify(&refs, &weights[..nb], algo, &th, args.pippenger_c); per_algo.insert(algo.name().into(), json!(!o.valid)); if o.valid { all_rejected = false; } } let refs: Vec<&SigItem> = (0..nb).map(|k| &items[k]).collect(); let pos = msm::batch_verify(&refs, &weights[..nb], MsmAlgo::Pippenger, &th, args.pippenger_c).valid; per_algo.insert("unmodified_batch_accepted".into(), json!(pos)); if !pos { all_rejected = false; } neg.insert(name.into(), Value::Object(per_algo)); } eprintln!("[negative control] all invalid batches rejected = {}", all_rejected); out.insert( "batch_verification".into(), json!({ "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", "msm_algorithms": { "straus_wnaf5": "own implementation: interleaved Straus with width-5 wNAF recoding, per-term odd-multiple tables (1 dbl + 7 adds), projective additions", "pippenger": "own implementation: bucket method with c-bit windows (c = pippenger_window(2b+1)), mixed additions into buckets, running-sum trick", "k256_lincomb_vartime": "k256 0.14 library: GLV decomposition + wNAF-5 multiscalar (wnaf crate), variable time", "k256_lincomb_ct": "k256 0.14 library: constant-time GLV + radix-16 fixed-window interleaving (benchmarked up to ct_lincomb_max_batch)" }, "single_verify_reference_ns": { "k256_verify_raw": k256_schnorr_verify.median_ns, "libsecp256k1_schnorr_verify": secp_schnorr_verify.median_ns }, "sizes": sizes, "results": batch_results, "negative_controls": {"batch_size": nb, "all_invalid_batches_rejected": all_rejected, "cases": Value::Object(neg)}, }), ); out.insert("elapsed_s".into(), json!(t_start.elapsed().as_secs_f64())); println!("{}", serde_json::to_string(&Value::Object(out)).unwrap()); }