SPB Git

spb/wp34_uqo

Public
25.2 KB · 585 lines rust
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
Files · main
Menu

Theme