SPB Git

spb/wp34_uqo

Public

WP34: foundations, literature, ECDLP/signature studies, Lean formal layer, draft manuscript

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Nov 1, 2026) Browse files

182 changed files +20,001 −0

182 changed files
Unified Split
added .gitignore +44 −0
@@ -0,0 +1,44 @@
1 +# Raw & processed data — never committed (see data/raw/README.md for provenance)
2 +data/raw/*
3 +!data/raw/README.md
4 +data/processed/*
5 +!data/processed/.gitkeep
6 +
7 +# Python
8 +__pycache__/
9 +*.pyc
10 +.pytest_cache/
11 +.venv/
12 +*.egg-info/
13 +
14 +# LaTeX build artifacts (paper/main.pdf IS committed)
15 +paper/*.aux
16 +paper/*.log
17 +paper/*.out
18 +paper/*.toc
19 +paper/*.lof
20 +paper/*.lot
21 +paper/*.fls
22 +paper/*.fdb_latexmk
23 +paper/*.bbl
24 +paper/*.blg
25 +paper/*.spl
26 +paper/*.synctex.gz
27 +paper/*.run.xml
28 +paper/*.bcf
29 +
30 +# Logs
31 +logs/
32 +*.log
33 +
34 +# macOS
35 +.DS_Store
36 +
37 +# Lean build log is evidence, keep it
38 +!formal/build.log
39 +
40 +# build artefacts
41 +formal/.lake/
42 +experiments/**/target/
43 +results/**/*.chunk*
44 +*.err
added CLAUDE.md +43 −0
@@ -0,0 +1,43 @@
1 +# CLAUDE.md — UQO Working Paper 34
2 +
3 +**BEYOND THE SQUARE-ROOT BARRIER — Automated Discovery of Algebraic and Algorithmic Shortcuts in Bitcoin Cryptography**
4 +
5 +Project: WP34-UQO · Institution: Université du Québec en Outaouais · Author: Simon-Pierre Boucher
6 +Research domain: mathematical cryptography, computational number theory, algorithm discovery, automated scientific research.
7 +Target: a publication-quality theoretical and experimental research paper (English). Research mode: autonomous,
8 +hypothesis-driven, reproducible computational research.
9 +
10 +The complete mandate (sections 1–19 as pasted by the author on 2026-11-01) is archived verbatim in `docs/MANDATE.md`.
11 +This file is the operational summary used by Claude Code while working in this repository.
12 +
13 +## Mission (short form)
14 +
15 +Investigate whether unexplored mathematical structures, algebraic transformations, algorithmic reductions or computational
16 +strategies can improve on established methods for: the ECDLP, the structure of secp256k1, ECDSA, BIP340 Schnorr, SHA-256,
17 +and the relations between curve arithmetic and hash functions. **Do not assume shortcuts exist.** A carefully documented
18 +failed hypothesis is a valid result. Every result is classified as one of: new verified result · known result independently
19 +reproduced · conditional result · empirical observation · rejected conjecture · unresolved direction. Never describe a
20 +constant-factor optimisation as a cryptographic break.
21 +
22 +## Integrity boundaries (non-negotiable)
23 +
24 +- Experiments run only on synthetic instances, reduced-security constructions, or keys generated for the experiments.
25 +- No real Bitcoin/Ethereum wallets, live addresses, third-party public keys, deployed systems, blockchain scanning.
26 +- Full-strength standards are analysed theoretically and benchmarked through legitimate implementations only.
27 +- Every experiment preserves what is needed for independent replication (seed, parameters, host, versions, command).
28 +- Never claim an experiment ran unless the command actually ran and its outputs are saved under `results/`.
29 +
30 +## Repository conventions
31 +
32 +- Python environment: `.venv` (uv, Python 3.12) — run everything with `.venv/bin/python`.
33 +- Reusable exact-arithmetic code: `src/wp34/` (pure Python integers, no floats in field arithmetic).
34 +- Native engines: Rust crates under `experiments/<family>/<crate>/` (built with `cargo build --release`).
35 +- Experiments: `experiments/<family>/` scripts; each writes `results/<family>/<name>.json` with the provenance block
36 + produced by `wp34.provenance.record()` (host, cpu, ram, os, versions, seed, command, timestamp).
37 +- Hypotheses: `hypotheses/HNN-<slug>.md` with YAML front matter (axis, status, classification) — one file per hypothesis.
38 +- Literature: `literature/` (structured database `literature/db.yaml` + `paper/references.bib`, every entry verified).
39 +- Formal: `formal/` Lean 4 project (`lake build` must pass; machine-checked statements listed in `formal/README.md`).
40 +- Research log: `research_log/` dated entries + `research_log/discovery_log.jsonl` (one JSON line per evaluated hypothesis).
41 +- Figures: generated by scripts into `figures/` and copied to `paper/figures/`; tables into `paper/tables/`.
42 +- Paper: `paper/main.tex` (UQO series template, pdflatex + bibtex/aer.bst, `make` in `paper/`).
43 +- Tests: `tests/` (pytest) — cross-implementation agreement is a gate for every arithmetic component.
added README.md +125 −0
@@ -0,0 +1,125 @@
1 +<!-- Author: Simon-Pierre Boucher — simon-pierre.boucher@uqo.ca -->
2 +# 🔐 Beyond the Square-Root Barrier
3 +
4 +**UQO Working Paper No. 34** — *Automated Discovery of Algebraic and Algorithmic Shortcuts in Bitcoin Cryptography*
5 +
6 +[![Paper](https://img.shields.io/badge/paper-PDF-b31b1b?logo=latex&logoColor=white)](paper/main.pdf)
7 +[![Python](https://img.shields.io/badge/python-3.12-3776AB?logo=python&logoColor=white)](requirements.txt)
8 +[![Rust](https://img.shields.io/badge/rust-1.95-000000?logo=rust)](experiments/synthetic_ecdlp/rho_engine)
9 +[![Lean](https://img.shields.io/badge/Lean-4.35%20bv__decide-blue)](formal/)
10 +[![Reproducible](https://img.shields.io/badge/reproducible-seeds%20%2B%20provenance-2e7d32)](results/)
11 +[![Institution](https://img.shields.io/badge/UQO-D%C3%A9pt.%20des%20sciences%20administratives-16365c)](https://uqo.ca)
12 +[![Author](https://img.shields.io/badge/author-Simon--Pierre%20Boucher-16365c)](mailto:simon-pierre.boucher@uqo.ca)
13 +
14 +> **TL;DR** — Can a systematic, automated exploration of secp256k1, ECDSA, BIP340 Schnorr and SHA-256 find a
15 +> computational shortcut the literature has missed? We ran a six-stage discovery pipeline (verified literature
16 +> database → 25 structured hypotheses → exact symbolic checks → seeded experiments with provenance → Lean 4
17 +> machine-checked statements → six-way classification) autonomously under a written mandate. Outcome: **no shortcut**;
18 +> every secp256k1 invariant verified twice with exact arithmetic; the libsecp256k1 GLV constants shown to be the
19 +> Eisenstein ideal (π̄−1): a1 = (t−f)/2 − 1, b1 = −f; the "orbit quotient group" conjecture refuted; 6 000 synthetic
20 +> ECDLP instances (20–56 bits) giving α ∈ [0.48, 0.50] and the rho constant 1.254·√n (theory 1.2533); order-6
21 +> automorphism classes 2.42× in iterations (√6) but ≈ 1× in wall time in our engine; BIP340 batch verification 3.1×;
22 +> reduced-step SHA-256 diffusion/bias/SAT calibrations far below the published 31-step records; bit-level SHA-256
23 +> identities and signature facts machine-checked. Everything synthetic; nothing bears on deployed systems.
24 +
25 +---
26 +
27 +## 📌 Status
28 +
29 +| Item | Value |
30 +|---|---|
31 +| Version | 1.0 (November 2026) |
32 +| SSRN | not yet submitted |
33 +| Journal | — (candidate venues: Journal of Cryptology, DCC, IACR ToSC/TCHES depending on emphasis) |
34 +| Code & data | fully reproducible from this repository (synthetic instances only; see Appendix A of the paper) |
35 +
36 +## 🎯 What this paper does
37 +
38 +The mandate (`docs/MANDATE.md`, pasted by the author on 2026-11-01) asks an autonomous research agent to test whether
39 +unexplored algebraic transformations or algorithmic strategies improve on the established attacks on Bitcoin's
40 +primitives — and to reject them rigorously if not. Four axes: (A) secp256k1 structure, (B) algorithmic complexity of
41 +the ECDLP and of signature verification, (C) reduced-step SHA-256, (D) the discovery methodology and formal
42 +verification. Hypotheses live in `hypotheses/` (one file each, 16 mandatory fields), their evaluation in
43 +`research_log/discovery_log.jsonl` and Table 15 of the paper.
44 +
45 +Core objects:
46 +
47 +```
48 +E: y² = x³ + 7 over F_p, p = 2²⁵⁶ − 2³² − 977, #E = n prime
49 +φ(x, y) = (βx, y) = [λ](x, y), End(E) = Z[ω], GLV lattice = (π̄ − 1)·Z[ω]
50 +Q = dG (generic cost Θ(√n); √6 from Aut(E); nothing more)
51 +sG = R + eP (BIP340); H(x) = y (SHA-256, 64 steps)
52 +```
53 +
54 +## 🏆 Headline results
55 +
56 +| Object | Result | Classification |
57 +|---|---|---|
58 +| secp256k1 parameters and invariants (14 facts) | Python = PARI/GP; twist cofactor 220-bit prime; embedding degree 253-bit; no 2-torsion; 4p = t² + 3f² | known, reproduced |
59 +| libsecp256k1 GLV constants | a1 = (t−f)/2 − 1, b1 = −f, a2 = a1 − b1, b2 = a1; lattice = ideal (π̄−1); found by the identity search, then proved | known structure, explicit form |
60 +| Orbit-quotient "group of order n/6" | impossible (proof + 1 500/1 500 counterexamples) | rejected |
61 +| ECDLP scaling, 3 840 generic + 2 400 j=0 instances | α ∈ [0.478, 0.500]; rho 1.254·√n [1.21, 1.30]; BSGS 1.50·√n; kangaroo 1.94·√n | known, reproduced |
62 +| Order-6 automorphism classes | 2.42× [2.19, 2.70] fewer iterations (√6 = 2.449); wall-clock 0.95× in this engine | known, reproduced + implementation observation |
63 +| Automated iteration-function search (25 designs) | nothing below the random-mapping constant; r = 2, 3 walks 3.8× / 2.3× worse | rejected shortcut |
64 +| Parallel DP collision search | 22.1× on 28 threads (M3 Ultra), 14.8× on 16 | known, reproduced |
65 +| Preprocessing | online cost 1.5·√(n/T); per-instance speed-up up to 42× | known, reproduced |
66 +| BIP340 batch verification | 7.9 µs/sig at b = 2048, 3.10× [3.05, 3.16] vs single (k256); saturation explained | known, reproduced |
67 +| Implementations | pure Python 7.4 ms vs libsecp256k1 13.2 µs per Schnorr verify (560×) | empirical |
68 +| SHA-256 reduced steps | see `results/reduced_round_sha256/summary.json` and paper §10.9 | empirical / known |
69 +| Lean 4 | SHA-256 word identities, step bijection, schedule inversion, linearised local collision, ECDSA malleability, Schnorr correctness (see `formal/README.md`) | machine-checked |
70 +
71 +## 🗂 Repository layout
72 +
73 +```
74 +beyond-square-root-barrier/ (git: wp34_uqo)
75 +├── CLAUDE.md, docs/MANDATE.md # operational summary and the verbatim mandate
76 +├── src/wp34/ # exact-arithmetic library: ec, secp256k1, ecdsa, schnorr (BIP340), sha256, dlp, provenance
77 +├── theory/ # derivation notes + SymPy symbolic checks + cost models
78 +├── experiments/
79 +│ ├── baselines/ # secp256k1_facts.py (Python + PARI/GP), scalar_mult_opcount.py
80 +│ ├── synthetic_ecdlp/ # rho_engine (Rust), ecdlp_study.py, run_batch_remote.py, analyze_ecdlp.py, preprocessing, index-calculus probe
81 +│ ├── signature_benchmarks/ # sigbench (Rust: libsecp256k1 + k256, own Straus/Pippenger), run_bench.py
82 +│ ├── reduced_round_sha256/ # diffusion, bias, CNF encoder, SAT/SMT preimage & collision grids, local collision
83 +│ └── automated_discovery/ # identity_search.py, registry.py (hypothesis registry → discovery log + table)
84 +├── formal/ # Lean 4 project (bv_decide), formal/README.md lists every theorem
85 +├── hypotheses/ # H01–H25 structured records
86 +├── literature/ # db.yaml (82 items), records.md, research_gaps.md, unverified.md
87 +├── results/ # JSON results with provenance blocks (host, versions, seed, command)
88 +├── figures/ # generated figures (copied to paper/figures)
89 +├── tests/ # pytest gates (core, DLP reference, Rust engine fixtures, signature cross-checks, CNF)
90 +├── scripts/ # make_tables.py (numbers.tex), write_hypotheses.py, fig_ec_geometry.py
91 +├── research_log/ # dated narrative entries + discovery_log.jsonl
92 +└── paper/ # main.tex, sections/, appendix/, tables/ (generated), figures/, references.bib (129 verified) → main.pdf
93 +```
94 +
95 +## 🚀 Quick start
96 +
97 +```bash
98 +uv venv --python 3.12 .venv && uv pip install -r requirements.txt
99 +brew install pari cadical kissat z3 # PARI/GP (point counting), SAT/SMT solvers
100 +.venv/bin/python -m pytest -q tests/ # cross-implementation gates
101 +.venv/bin/python experiments/baselines/secp256k1_facts.py
102 +.venv/bin/python experiments/automated_discovery/identity_search.py
103 +cargo build --release --manifest-path experiments/synthetic_ecdlp/rho_engine/Cargo.toml
104 +.venv/bin/python experiments/synthetic_ecdlp/ecdlp_study.py all # job files (PARI/GP)
105 +.venv/bin/python experiments/synthetic_ecdlp/run_batch_remote.py --engine experiments/synthetic_ecdlp/rho_engine/target/release/rho_engine \
106 + --jobs results/synthetic_ecdlp/scaling_generic.jobs.json --out results/synthetic_ecdlp/scaling_generic_$(hostname -s).json --parallel 8
107 +.venv/bin/python experiments/synthetic_ecdlp/analyze_ecdlp.py
108 +(cd formal && lake build)
109 +.venv/bin/python experiments/automated_discovery/registry.py && .venv/bin/python scripts/make_tables.py
110 +(cd paper && make)
111 +```
112 +
113 +## 🔬 Integrity
114 +
115 +Synthetic curves and keys only; no real wallets, addresses, third-party keys or blockchain data; full-strength
116 +primitives analysed through exact algebra and legitimate implementations. Every number in the paper is generated from
117 +a saved result file by `scripts/make_tables.py` or `analyze_ecdlp.py` (LaTeX macros in `paper/tables/numbers*.tex`).
118 +
119 +## 📝 Notes
120 +
121 +- Series standard: `Recherche-UQO/HARMONISATION.md`. Deviation: the mandate's required directories (`theory/`,
122 + `experiments/`, `formal/`, `hypotheses/`, `literature/`, `research_log/`) are kept at the root in addition to the
123 + standard `src/`, `scripts/`, `results/`, `figures/`, `paper/`.
124 +- Built autonomously by Claude Code (Claude Fable 5.1) on 2026-11-01 on the author's laptop and MacLustr nodes
125 + M3U96a, M3U96b, M2U64; see `research_log/`.
added data/vectors/bip340_test_vectors.csv +20 −0
@@ -0,0 +1,20 @@
1 +index,secret key,public key,aux_rand,message,signature,verification result,comment
2 +0,0000000000000000000000000000000000000000000000000000000000000003,F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9,0000000000000000000000000000000000000000000000000000000000000000,0000000000000000000000000000000000000000000000000000000000000000,E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA821525F66A4A85EA8B71E482A74F382D2CE5EBEEE8FDB2172F477DF4900D310536C0,TRUE,
3 +1,B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,0000000000000000000000000000000000000000000000000000000000000001,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6896BD60EEAE296DB48A229FF71DFE071BDE413E6D43F917DC8DCF8C78DE33418906D11AC976ABCCB20B091292BFF4EA897EFCB639EA871CFA95F6DE339E4B0A,TRUE,
4 +2,C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C9,DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8,C87AA53824B4D7AE2EB035A2B5BBBCCC080E76CDC6D1692C4B0B62D798E6D906,7E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C,5831AAEED7B44BB74E5EAB94BA9D4294C49BCF2A60728D8B4C200F50DD313C1BAB745879A5AD954A72C45A91C3A51D3C7ADEA98D82F8481E0E1E03674A6F3FB7,TRUE,
5 +3,0B432B2677937381AEF05BB02A66ECD012773062CF3FA2549E44F58ED2401710,25D1DFF95105F5253C4022F628A996AD3A0D95FBF21D468A1B33F8C160D8F517,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,7EB0509757E246F19449885651611CB965ECC1A187DD51B64FDA1EDC9637D5EC97582B9CB13DB3933705B32BA982AF5AF25FD78881EBB32771FC5922EFC66EA3,TRUE,test fails if msg is reduced modulo p or n
6 +4,,D69C3509BB99E412E68B0FE8544E72837DFA30746D8BE2AA65975F29D22DC7B9,,4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703,00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C6376AFB1548AF603B3EB45C9F8207DEE1060CB71C04E80F593060B07D28308D7F4,TRUE,
7 +5,,EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,public key not on the curve
8 +6,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,FFF97BD5755EEEA420453A14355235D382F6472F8568A18B2F057A14602975563CC27944640AC607CD107AE10923D9EF7A73C643E166BE5EBEAFA34B1AC553E2,FALSE,has_even_y(R) is false
9 +7,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,1FA62E331EDBC21C394792D2AB1100A7B432B013DF3F6FF4F99FCB33E0E1515F28890B3EDB6E7189B630448B515CE4F8622A954CFE545735AAEA5134FCCDB2BD,FALSE,negated message
10 +8,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769961764B3AA9B2FFCB6EF947B6887A226E8D7C93E00C5ED0C1834FF0D0C2E6DA6,FALSE,negated s value
11 +9,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,0000000000000000000000000000000000000000000000000000000000000000123DDA8328AF9C23A94C1FEECFD123BA4FB73476F0D594DCB65C6425BD186051,FALSE,sG - eP is infinite. Test fails in single verification if has_even_y(inf) is defined as true and x(inf) as 0
12 +10,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,00000000000000000000000000000000000000000000000000000000000000017615FBAF5AE28864013C099742DEADB4DBA87F11AC6754F93780D5A1837CF197,FALSE,sG - eP is infinite. Test fails in single verification if has_even_y(inf) is defined as true and x(inf) as 1
13 +11,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,4A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,sig[0:32] is not an X coordinate on the curve
14 +12,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,sig[0:32] is equal to field size
15 +13,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141,FALSE,sig[32:64] is equal to curve order
16 +14,,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,public key is not a valid X coordinate because it exceeds the field size
17 +15,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,,71535DB165ECD9FBBC046E5FFAEA61186BB6AD436732FCCC25291A55895464CF6069CE26BF03466228F19A3A62DB8A649F2D560FAC652827D1AF0574E427AB63,TRUE,message of size 0 (added 2022-12)
18 +16,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,11,08A20A0AFEF64124649232E0693C583AB1B9934AE63B4C3511F3AE1134C6A303EA3173BFEA6683BD101FA5AA5DBC1996FE7CACFC5A577D33EC14564CEC2BACBF,TRUE,message of size 1 (added 2022-12)
19 +17,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,0102030405060708090A0B0C0D0E0F1011,5130F39A4059B43BC7CAC09A19ECE52B5D8699D1A71E3C52DA9AFDB6B50AC370C4A482B77BF960F8681540E25B6771ECE1E5A37FD80E5A51897C5566A97EA5A5,TRUE,message of size 17 (added 2022-12)
20 +18,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999,403B12B0D8555A344175EA7EC746566303321E5DBFA8BE6F091635163ECA79A8585ED3E3170807E7C03B720FC54C7B23897FCBA0E9D0B4A06894CFD249F22367,TRUE,message of size 100 (added 2022-12)
added docs/CONVENTIONS.md +14 −0
@@ -0,0 +1,14 @@
1 +# WP34 — Working conventions for every contributor (human or agent)
2 +
3 +1. Exact arithmetic only for field/group operations (Python `int`, Rust `u64/u128` with Montgomery or Barrett reduction).
4 + Never substitute floating point for a field operation. Floats are allowed only for statistics and plotting.
5 +2. Each experiment script must: accept `--seed`, `--out`, write JSON via `wp34.provenance.record(...)`, and be re-runnable.
6 +3. Record operation counts (group additions, doublings, field multiplications, solver conflicts/decisions) next to wall-clock.
7 +4. Repeated runs (≥ 10 for stochastic algorithms, ≥ 5 for timings); report mean, sd, and a bootstrap 95 % CI.
8 +5. Each experimental family includes a negative control (e.g. random oracle / random mapping / full-round hash).
9 +6. Results that cannot be reproduced from a saved command are not results. Keep the command in the JSON.
10 +7. Classification vocabulary (exact strings): `new_verified`, `known_reproduced`, `conditional`, `empirical_observation`,
11 + `rejected`, `unresolved`.
12 +8. Every substantial literature claim cites a BibTeX key that exists in `paper/references.bib` and has been verified
13 + (DOI / ePrint / standard URL). Do not invent references.
14 +9. Synthetic instances only. Keys and nonces are generated with the experiment seed. No real-world public keys.
added docs/MANDATE.md +215 −0
@@ -0,0 +1,215 @@
1 +# Mandate — UQO Working Paper 34 (verbatim, pasted by the author on 2026-11-01)
2 +
3 +CLAUDE.md — UQO Working Paper 34
4 +
5 +BEYOND THE SQUARE-ROOT BARRIER
6 +
7 +Automated Discovery of Algebraic and Algorithmic Shortcuts in Bitcoin Cryptography
8 +
9 +Project: WP34-UQO
10 +Institution: Université du Québec en Outaouais
11 +Research domain: Mathematical Cryptography, Computational Number Theory, Algorithm Discovery, Automated Scientific Research
12 +Target: A publication-quality theoretical and experimental research paper
13 +Primary language: English
14 +Research mode: Autonomous, hypothesis-driven, reproducible computational research
15 +
16 +## 1. PRIMARY MISSION
17 +
18 +You are an autonomous mathematical research scientist operating through Claude Code.
19 +
20 +Your mission is to investigate whether previously unexplored mathematical structures, algebraic transformations, algorithmic reductions, or computational strategies can improve upon established methods relevant to:
21 +
22 +1. The elliptic curve discrete logarithm problem (ECDLP).
23 +2. The mathematical structure of secp256k1.
24 +3. ECDSA signature security and verification.
25 +4. BIP340 Schnorr signatures.
26 +5. SHA-256 cryptanalysis.
27 +6. Relationships between elliptic-curve arithmetic and cryptographic hash functions.
28 +
29 +The central scientific question is:
30 +
31 +Can systematic mathematical exploration and automated algorithm discovery reveal nontrivial computational shortcuts in the cryptographic primitives underlying Bitcoin?
32 +
33 +Do not assume such shortcuts exist.
34 +
35 +The purpose is to discover, test, rigorously evaluate, and potentially reject candidate mathematical approaches.
36 +
37 +A failed hypothesis is a valid scientific result if it is carefully documented and produces useful theoretical or empirical knowledge.
38 +
39 +The final work must distinguish:
40 +
41 +* Mathematical discoveries.
42 +* New algorithms.
43 +* New proofs or conditional bounds.
44 +* Constant-factor optimizations.
45 +* Implementation improvements.
46 +* Previously known results.
47 +* Experimental observations.
48 +* Unsupported conjectures.
49 +
50 +Never describe a computational optimization as a cryptographic break unless the evidence justifies that conclusion.
51 +
52 +## 2. RESEARCH INTEGRITY AND EXPERIMENTAL BOUNDARIES
53 +
54 +All experimental attacks must be conducted on synthetic instances, deliberately reduced-security constructions, or keys generated specifically for the experiments.
55 +
56 +Do not target real Bitcoin or Ethereum wallets, live addresses, third-party public keys, or deployed systems.
57 +
58 +Do not scan blockchain data for vulnerable keys or operationalize private-key recovery against real users.
59 +
60 +The project is about mathematical understanding and controlled cryptanalysis, not compromising deployed assets.
61 +
62 +Full-strength cryptographic standards may be analyzed theoretically and benchmarked through legitimate implementations.
63 +
64 +Every experiment must preserve sufficient information for independent replication.
65 +
66 +## 3. REQUIRED MATHEMATICAL FOUNDATIONS
67 +
68 +### 3.1 Elliptic curves over finite fields
69 +
70 +Develop a rigorous mathematical foundation for elliptic curves of the form:
71 +
72 +E(F_p): y^2 = x^3 + ax + b (mod p).
73 +
74 +Derive and explain: finite-field arithmetic; elliptic-curve group laws; point addition and doubling; scalar multiplication; group order and subgroup structure; Hasse's theorem; curve isomorphisms; endomorphisms; scalar decomposition; generic group complexity; algebraic versus generic attacks.
75 +
76 +For secp256k1, use E: y^2 = x^3 + 7 (mod p) with p = 2^256 - 2^32 - 977.
77 +
78 +Retrieve and verify the complete standardized domain parameters, including the generator, prime subgroup order, and cofactor.
79 +
80 +Explain why the special form of secp256k1 is mathematically interesting without assuming that this structure makes the curve insecure.
81 +
82 +### 3.2 Elliptic-curve discrete logarithms
83 +
84 +Study the problem Q = dG. Given G and Q, investigate the mathematical complexity of determining d.
85 +
86 +Establish the classical generic-group baseline T(n) = O(sqrt n).
87 +
88 +Investigate established methods including: baby-step giant-step; Pollard's rho; Pollard's kangaroo; distinguished-point techniques; parallel collision search; endomorphism-based improvements; Pohlig–Hellman under appropriate group-order assumptions; index-calculus approaches and their applicability limits; relevant generic-group lower bounds.
89 +
90 +Provide rigorous complexity analysis and literature attribution.
91 +
92 +## 4. RESEARCH AXIS A — SECP256K1 ALGEBRAIC STRUCTURE
93 +
94 +Investigate whether the particular algebraic structure of secp256k1 offers any underexplored mathematical opportunities. Study its efficiently computable endomorphism and related scalar decompositions. For the endomorphism phi(x,y) = (beta x, y), study the conditions under which phi(P) = [lambda]P. Verify the constants and algebraic relationships using independent implementations.
95 +
96 +Research questions
97 +
98 +A1. Can the endomorphism structure produce meaningful improvements beyond known constant-factor speedups?
99 +A2. Can equivalence classes or automorphism orbits reduce redundant computation?
100 +A3. Are there unexplored useful representations of scalar multiplication?
101 +A4. Can algebraic transformations improve generic discrete-log experiments on synthetic groups?
102 +A5. Can candidate algorithms be shown theoretically incapable of surpassing known lower bounds under specified assumptions?
103 +
104 +Required experiments
105 +
106 +Implement small and medium-size synthetic elliptic-curve instances. Compare established discrete-log methods under identical experimental conditions. Measure: group operations; memory consumption; runtime; success probability; scaling with subgroup size; parallel efficiency. Fit empirical complexity models T(n) = a n^alpha (log n)^beta. Use repeated experiments, confidence intervals, and sensitivity analysis. Do not extrapolate reduced-size experiments into claims that secp256k1 has been broken.
107 +
108 +## 5. RESEARCH AXIS B — ECDSA AND SCHNORR
109 +
110 +### 5.1 ECDSA
111 +
112 +Develop the mathematical construction R = kG, r = x(R) mod n, s = k^{-1}(z + r d) mod n. Explain signing, verification, correctness, and security assumptions. Investigate the roles of: secret-key entropy; nonce entropy; deterministic nonce generation; signature malleability; hash-to-integer conversion; scalar multiplication; side-channel resistance. Compare ECDSA with Schnorr using mathematically justified criteria.
113 +
114 +### 5.2 BIP340 Schnorr
115 +
116 +Study the signature relation sG = R + eP where the challenge e is derived using the BIP340 tagged-hash construction. Cover: x-only public keys; nonce generation; key prefixing; tagged hashing; batch verification; signature aggregation; security reductions; random-oracle assumptions.
117 +
118 +Research questions
119 +
120 +B1. What mathematical structures distinguish the security proofs of ECDSA and Schnorr?
121 +B2. Can verification be accelerated without modifying consensus semantics?
122 +B3. Which algebraic simplifications are genuinely new rather than reformulations of known identities?
123 +B4. Can automated proof search reveal useful equivalences or sharper theoretical bounds?
124 +B5. Which implementation choices produce measurable performance differences?
125 +
126 +Required experiments
127 +
128 +Use synthetic key pairs and official test vectors. Benchmark legitimate signing and verification implementations. Evaluate batch verification on controlled datasets. Do not use real private keys or search for vulnerable third-party signatures.
129 +
130 +## 6. RESEARCH AXIS C — SHA-256
131 +
132 +Investigate SHA-256 as a mathematical and computational object. Cover: Merkle–Damgård construction; message padding; message scheduling; compression rounds; Boolean functions; modular additions; rotations and shifts; diffusion and avalanche properties; collision resistance; preimage resistance; second-preimage resistance. Distinguish the complete SHA-256 hash construction from its compression function.
133 +
134 +Mathematical objective: study the complexity of finding H(x) = y and H(x1) = H(x2), x1 != x2. Establish generic benchmarks for a 256-bit ideal hash: T_preimage ≈ 2^256, T_collision ≈ 2^128.
135 +
136 +Research questions
137 +
138 +C1. Can reduced-round SHA-256 exhibit exploitable mathematical regularities?
139 +C2. Can SAT/SMT formulations improve the understanding of reduced-round constraints?
140 +C3. Which differential characteristics remain statistically meaningful as the number of rounds increases?
141 +C4. Can automated searches reproduce known reduced-round results?
142 +C5. Can proposed mathematical simplifications be ruled out by counterexamples?
143 +
144 +Required experiments
145 +
146 +Construct reduced-round educational variants. Evaluate diffusion and differential behavior. Use controlled SAT/SMT experiments. Measure solver runtime, memory, and scaling. Compare with published reduced-round cryptanalysis. Never claim a full SHA-256 weakness from reduced-round results alone.
147 +
148 +## 7. RESEARCH AXIS D — AUTOMATED MATHEMATICAL DISCOVERY
149 +
150 +This is the central methodological contribution of WP34. Design an autonomous scientific discovery system that generates, tests, rejects, and refines mathematical hypotheses. The workflow must contain six stages.
151 +
152 +Stage 1 — Literature extraction: build a structured research database containing theorems, known algorithms, security assumptions, complexity bounds, published experimental results, open problems, known negative results. Record the original source for every substantial claim.
153 +
154 +Stage 2 — Hypothesis generation: generate candidate research hypotheses based on mathematical structures and gaps identified in the literature. For each hypothesis, record: mathematical statement; motivation; assumptions; expected contribution; existing related work; possible counterexamples; verification strategy; computational budget.
155 +
156 +Stage 3 — Symbolic investigation: use SageMath, SymPy, and suitable formal tools. Test algebraic identities and derive mathematical relationships. Use exact arithmetic whenever possible.
157 +
158 +Stage 4 — Experimental evaluation: implement reproducible experiments in Python, SageMath, Rust, or C++. Run experiments on controlled cryptographic instances. Record all outputs.
159 +
160 +Stage 5 — Formal verification: where feasible, use Lean 4 or another proof assistant. Separate machine-checked statements from informal proofs. A conjecture must never be represented as a theorem.
161 +
162 +Stage 6 — Scientific evaluation: compare candidate discoveries with existing literature. Classify each result as: 1. New verified result. 2. Known result independently reproduced. 3. Conditional result. 4. Empirical observation. 5. Rejected conjecture. 6. Unresolved research direction. Maintain a complete discovery log.
163 +
164 +## 8. HYPOTHESIS PORTFOLIO
165 +
166 +Investigate at least 20 distinct hypotheses across the four research axes. At least five must concern algebraic structure. At least five must concern algorithmic complexity. At least five must concern SHA-256 or reduced-round constructions. At least five must concern automated discovery methodology or formal verification. The hypothesis portfolio must not consist of superficial variations of the same idea. Each hypothesis must receive an explicit evaluation. For promising hypotheses, allocate additional computational effort only when justified by preliminary results. For failed hypotheses, document why the approach failed and what can be learned.
167 +
168 +## 9. EXPERIMENTAL DESIGN
169 +
170 +Establish a reproducible baseline before attempting novel algorithms. All experiments must include: explicit research question; null and alternative hypotheses when appropriate; fixed parameter definitions; reproducible random seeds; hardware information; software versions; independent correctness checks; repeated runs; runtime measurements; memory measurements; uncertainty estimates; comparison against established methods. Avoid misleading benchmarking practices. Use operation counts alongside wall-clock measurements. For each algorithm, report its time-memory trade-off. Use log-log scaling plots to evaluate empirical complexity. Include at least one negative control for each experimental family.
171 +
172 +## 10. COMPUTATIONAL ENVIRONMENT
173 +
174 +First inspect the available computational environment. Identify: CPU architecture; available RAM; GPU availability; operating system; compiler toolchains; SageMath availability; Python libraries; Rust toolchain; Lean installation; SMT solvers. Prefer efficient native implementations for expensive numerical experiments. Use arbitrary-precision integer arithmetic. Validate custom mathematical code against independent reference implementations. Never silently substitute an approximate calculation for exact finite-field arithmetic. Maintain separate development, validation, and benchmarking environments. Do not claim experiments were executed unless the commands actually ran and their results were saved.
175 +
176 +## 11. STATISTICAL AND COMPLEXITY ANALYSIS
177 +
178 +For each algorithmic comparison, report S(n) = T_baseline(n) / T_candidate(n). Estimate empirical complexity exponents. Report confidence intervals for estimated speedups. Use bootstrap inference when appropriate. Distinguish: theoretical asymptotic improvement; constant-factor speedup; parallel speedup; hardware-specific optimization; statistical noise. Explicitly discuss generic-group lower bounds. Do not infer an asymptotic breakthrough from a finite range of benchmarks.
179 +
180 +## 12. REQUIRED FIGURES
181 +
182 +Produce at least 12 publication-quality figures, including: 1. Cryptographic dependency map of Bitcoin. 2. Geometry of elliptic-curve group operations. 3. secp256k1 algebraic structure diagram. 4. ECDLP algorithm complexity comparison. 5. Empirical runtime scaling. 6. Time-memory trade-off. 7. Endomorphism benchmarking. 8. ECDSA versus Schnorr verification. 9. SHA-256 compression architecture. 10. Reduced-round diffusion behavior. 11. SAT/SMT experimental scaling. 12. Automated discovery pipeline. Every figure must be generated from verified mathematics or actual experimental data. Illustrative diagrams must be identified as such.
183 +
184 +## 13. REQUIRED TABLES
185 +
186 +Produce at least 10 substantial tables covering: cryptographic primitives; secp256k1 parameters; literature comparison; algorithmic complexity; experimental configuration; ECDLP benchmarking; ECDSA/Schnorr comparison; SHA-256 experiments; hypothesis evaluation; research contributions and limitations. Never fabricate measurements.
187 +
188 +## 14. PAPER STRUCTURE
189 +
190 +Write a complete LaTeX research manuscript. Proposed structure: 1. Introduction 2. Cryptographic Foundations of Bitcoin 3. Literature Review and Research Gaps 4. Mathematical Structure of secp256k1 5. Generic and Algebraic Discrete-Log Algorithms 6. ECDSA and Schnorr: Security and Computational Structure 7. SHA-256: Mathematical Structure and Reduced-Round Analysis 8. Autonomous Mathematical Discovery Framework 9. Experimental Design 10. Computational Results 11. Formal Results and Negative Findings 12. Implications for Cryptographic Security 13. Limitations 14. Conclusion 15. References 16. Technical Appendices. The manuscript must contain complete derivations, proofs where available, algorithms, equations, figures, and tables. Target a substantial journal-quality paper, not a short technical report. Quality is more important than arbitrary page count.
191 +
192 +## 15. PUBLICATION STANDARDS
193 +
194 +Target the methodological rigor expected by venues such as: IACR TCHES; IACR ToSC; Journal of Cryptology; Designs, Codes and Cryptography. Venue suitability depends on the actual contributions. Use BibTeX with verifiable references. Prioritize original research papers and standards. Do not invent citations, theorems, performance results, or novelty claims. Include a reproducibility statement and a responsible-disclosure assessment if an unexpected security-relevant result emerges.
195 +
196 +## 16. REQUIRED REPOSITORY STRUCTURE
197 +
198 +wp34_uqo/ with CLAUDE.md, README.md, paper/{main.tex, references.bib, sections/, figures/, tables/}, theory/{elliptic_curves, discrete_log, ecdsa, schnorr, sha256}, experiments/{baselines, synthetic_ecdlp, signature_benchmarks, reduced_round_sha256, automated_discovery}, formal/, results/, hypotheses/, literature/, tests/, scripts/, research_log/.
199 +
200 +## 17. RESEARCH EXECUTION PLAN
201 +
202 +Phase I — Foundations: definitions, verified parameters, literature database, bounds, research-gap report.
203 +Phase II — Reproducible baselines: standard algorithms implemented and verified; synthetic experiments; baseline measurements.
204 +Phase III — Discovery: 20-hypothesis portfolio investigated systematically; originality and falsifiability.
205 +Phase IV — Independent validation: attempt to disprove promising results; alternative implementations; literature search for earlier discoveries; formal verification where feasible.
206 +Phase V — Manuscript production: complete paper, figures, tables, compiled PDF, verified citations and equations.
207 +Phase VI — Critical review: skeptical referee pass; unsupported claims; novelty; reproducibility; revise until defensible.
208 +
209 +## 18. SCIENTIFIC SUCCESS CRITERIA
210 +
211 +Outcome A — New mathematical contribution. Outcome B — New algorithmic contribution (reproducible, independently validated). Outcome C — New methodological contribution (validated autonomous discovery framework outperforming a specified baseline). Outcome D — Valuable negative result (rigorous explanation or strong controlled evidence that a plausible family of shortcuts does not work). A claim of breaking secp256k1, ECDSA, Schnorr, or SHA-256 requires extraordinary evidence and independent validation.
212 +
213 +## 19. FINAL DELIVERABLES
214 +
215 +1. Complete research paper in LaTeX. 2. Successfully compiled PDF. 3. Reproducible experiment code.
added experiments/automated_discovery/identity_search.py +132 −0
@@ -0,0 +1,132 @@
1 +"""Stage 2–3 automation: exhaustive search for exact integer / modular identities among the secp256k1 constants.
2 +
3 +The generator enumerates candidate relations of bounded complexity and the checker evaluates each one with exact
4 +arithmetic. Every surviving identity is then classified against the literature (CM theory of j = 0 curves,
5 +GLV lattice structure). This is a deliberately simple "discovery engine": its purpose is to measure how many of the
6 +known structural identities an automated search rediscovers, and whether it finds any that are NOT explained by theory.
7 +
8 +Search spaces
9 + Q1 binary quadratic forms: u² + c·u·v + d·v² = target for (u, v) among constant pairs, |c|,|d| ≤ 6, target ∈ {p, n, 4p, 4n, …}
10 + Q2 modular polynomial relations: x² + c·x + d ≡ 0 (mod m) for x ∈ {β, λ, …}, m ∈ {p, n}, |c|,|d| ≤ 6
11 + Q3 linear relations among the GLV basis entries: a2 = e1·a1 + e2·b1, b2 = e3·a1 + e4·b1, |e_i| ≤ 3
12 + Q4 "λ ≡ polynomial(β)" style cross-modulus relations (expected to fail: different moduli) — negative control
13 + Q5 small-coefficient relations between Gx, Gy, β, p (expected to fail — the generator is not special) — negative control
14 +Writes results/automated_discovery/identity_search.json.
15 +"""
16 +import itertools, json, math, sys, time, pathlib
17 +ROOT = pathlib.Path(__file__).resolve().parents[2]
18 +sys.path.insert(0, str(ROOT / "src"))
19 +from wp34 import secp256k1 as k1
20 +from wp34.provenance import record
21 +
22 +p, n, beta, lam = k1.p, k1.n, k1.beta, k1.lam
23 +t = p + 1 - n
24 +f = math.isqrt((4 * p - t * t) // 3)
25 +consts = {"p": p, "n": n, "t": t, "f": f, "a1": k1.a1, "b1": k1.b1, "a2": k1.a2, "b2": k1.b2,
26 + "beta": beta, "lambda": lam, "Gx": k1.Gx, "Gy": k1.Gy}
27 +KNOWN = {
28 + "Q1:t^2+0*t*f+3*f^2=4p": "CM theory: 4p = t² + 3f² for j = 0 (Frobenius π = (t + f√−3)/2 ∈ Z[ω])",
29 + "Q1:a1^2-1*a1*b1+1*b1^2=n": "n = N(a1 + b1·ω): the GLV basis vector has Eisenstein norm n (GLV 2001 / Z[ω] structure)",
30 + "Q1:a2^2-1*a2*b2+1*b2^2=n": "same for the second basis vector",
31 + "Q3:a2=1*a1-1*b1": "second GLV vector = ω·(first) up to sign: (a1,b1)·ω rotates the lattice (Eisenstein units)",
32 + "Q3:b2=1*a1+0*b1": "same",
33 + "Q2:beta^2+1*beta+1=0 mod p": "β is a primitive cube root of unity (β² + β + 1 = 0)",
34 + "Q2:lambda^2+1*lambda+1=0 mod n": "λ is a primitive cube root of unity in Z/nZ",
35 + "Q1:t^2+0*t*b1+3*b1^2=4p": "b1 = −f: the GLV vector's second coordinate is the CM conductor (4p = t² + 3f²)",
36 +}
37 +# Closed form found by the search and then explained (CM theory): the GLV lattice {(k1,k2): k1 + k2·λ ≡ 0 (mod n)} is the
38 +# principal ideal (π̄ − 1)·Z[ω] of Z[ω] where π̄ = (t − f√−3)/2 is the Frobenius conjugate mapping ω ↦ λ, hence
39 +# a1 = (t − f)/2 − 1, b1 = −f, a2 = (t + f)/2 − 1 = a1 − b1, b2 = a1, and a1² − a1·b1 + b1² = N(π̄ − 1) = n.
40 +CLOSED_FORM = {"a1": (t - f) // 2 - 1, "b1": -f, "a2": (t + f) // 2 - 1, "b2": (t - f) // 2 - 1}
41 +EISENSTEIN_NORM_FAMILY = "quadratic form u² ± uv + v² = n among {a1, b1, a2, b2, f}: all are Eisenstein norms of unit multiples / conjugates of π̄ − 1 (CM theory, GLV lattice = ideal (π̄ − 1))"
42 +
43 +
44 +def search():
45 + found, tested = [], 0
46 + names = list(consts)
47 + targets = {"p": p, "n": n, "4p": 4 * p, "4n": 4 * n, "2p": 2 * p, "3p": 3 * p, "3n": 3 * n, "p-1": p - 1, "n-1": n - 1}
48 + # Q1 binary quadratic forms (ordered pairs; forms u² + c uv + d v²)
49 + for u, v in itertools.permutations(names, 2):
50 + U, V = consts[u], consts[v]
51 + for c in range(-6, 7):
52 + for d in range(-6, 7):
53 + val = U * U + c * U * V + d * V * V
54 + tested += 1
55 + for tn, tv in targets.items():
56 + if val == tv:
57 + cs = f"{c:+d}" if c else ""
58 + found.append({"space": "Q1", "id": f"Q1:{u}^2{c:+d}*{u}*{v}{d:+d}*{v}^2={tn}".replace("+", "+").replace("^2+0*", "^2+0*"),
59 + "relation": f"{u}^2 {c:+d}·{u}·{v} {d:+d}·{v}^2 = {tn}"})
60 + # Q2 modular quadratics
61 + for xname in ("beta", "lambda", "Gx", "Gy", "t", "f", "a1", "b1"):
62 + X = consts[xname]
63 + for mname, m in (("p", p), ("n", n)):
64 + for c in range(-6, 7):
65 + for d in range(-6, 7):
66 + tested += 1
67 + if (X * X + c * X + d) % m == 0:
68 + found.append({"space": "Q2", "id": f"Q2:{xname}^2{c:+d}*{xname}{d:+d}=0 mod {mname}",
69 + "relation": f"{xname}^2 {c:+d}·{xname} {d:+d} ≡ 0 (mod {mname})"})
70 + # Q3 linear relations in the GLV basis
71 + for e1, e2 in itertools.product(range(-3, 4), repeat=2):
72 + tested += 2
73 + if k1.a2 == e1 * k1.a1 + e2 * k1.b1:
74 + found.append({"space": "Q3", "id": f"Q3:a2={e1:+d}*a1{e2:+d}*b1", "relation": f"a2 = {e1}·a1 {e2:+d}·b1"})
75 + if k1.b2 == e1 * k1.a1 + e2 * k1.b1:
76 + found.append({"space": "Q3", "id": f"Q3:b2={e1:+d}*a1{e2:+d}*b1", "relation": f"b2 = {e1}·a1 {e2:+d}·b1"})
77 + # Q4 negative control: λ ≡ c0 + c1 β + c2 β² (mod n) for small c_i and also mod p — must not hold (except trivialities)
78 + q4 = 0
79 + for c0, c1, c2 in itertools.product(range(-6, 7), repeat=3):
80 + tested += 2
81 + if (c0 + c1 * beta + c2 * beta * beta - lam) % n == 0 or (c0 + c1 * beta + c2 * beta * beta - lam) % p == 0:
82 + q4 += 1
83 + found.append({"space": "Q4", "id": f"Q4:lambda={c0}+{c1}beta+{c2}beta^2", "relation": "cross-modulus relation (unexpected)"})
84 + # Q5 negative control: Gy² ≡ Gx³ + 7 is the curve equation (expected, trivial) — exclude it; look for
85 + # small-coefficient linear relations c1·Gx + c2·Gy + c3·beta + c4 ≡ 0 (mod p), |c_i| ≤ 6, not all zero
86 + q5 = 0
87 + for c in itertools.product(range(-6, 7), repeat=4):
88 + if not any(c):
89 + continue
90 + tested += 1
91 + if (c[0] * k1.Gx + c[1] * k1.Gy + c[2] * beta + c[3]) % p == 0:
92 + q5 += 1
93 + found.append({"space": "Q5", "id": f"Q5:{c}", "relation": "small linear relation among Gx, Gy, beta (unexpected)"})
94 + return found, tested, q4, q5
95 +
96 +
97 +def main():
98 + t0 = time.time()
99 + found, tested, q4, q5 = search()
100 + # normalise ids and classify
101 + out = []
102 + for h in found:
103 + hid = h["id"].replace("+0*", "+0*")
104 + known = None
105 + for k, v in KNOWN.items():
106 + # tolerant matching on structure
107 + kk = k.replace("+", "").replace("-", "").replace("*", "")
108 + hh = hid.replace("+", "").replace("-", "").replace("*", "")
109 + if kk == hh:
110 + known = v
111 + if known is None and h["space"] == "Q1" and h["relation"].endswith("= n") and all(v in h["relation"] for v in ["^2"]):
112 + known = EISENSTEIN_NORM_FAMILY
113 + h["classification"] = "known_reproduced" if known else ("unexpected_investigate" if h["space"] in ("Q4", "Q5") else "found_unclassified")
114 + h["explanation"] = known
115 + out.append(h)
116 + # The expected count of chance hits for Q5: 13^4 trials, each true with prob 1/p ≈ 2^-256 → 0.
117 + closed_ok = all(getattr(k1, k) == v for k, v in CLOSED_FORM.items())
118 + meas = {"candidates_tested": tested, "identities_found": len(out), "found": out,
119 + "closed_form_glv_basis": {k: str(v) for k, v in CLOSED_FORM.items()}, "closed_form_matches_libsecp256k1": closed_ok,
120 + "closed_form_statement": "a1 = (t−f)/2 − 1, b1 = −f, a2 = (t+f)/2 − 1, b2 = a1 with 4p = t² + 3f²; lattice = ideal (π̄−1)Z[ω]",
121 + "negative_controls": {"Q4_cross_modulus_hits": q4, "Q5_small_linear_hits": q5,
122 + "expected_chance_hits": tested / p},
123 + "unexplained": [h for h in out if h["classification"] != "known_reproduced"]}
124 + record("identity_search_secp256k1_constants", {"spaces": ["Q1", "Q2", "Q3", "Q4", "Q5"], "coeff_bound": 6}, meas,
125 + out=str(ROOT / "results/automated_discovery/identity_search.json"), started=t0)
126 + print(f"tested {tested} candidates, found {len(out)} identities; negative controls Q4={q4} Q5={q5}; closed-form GLV basis matches: {closed_ok}")
127 + for h in out:
128 + print(f" [{h['classification']}] {h['relation']} — {h['explanation'] or ''}")
129 +
130 +
131 +if __name__ == "__main__":
132 + main()
added experiments/automated_discovery/registry.py +81 −0
@@ -0,0 +1,81 @@
1 +"""Hypothesis registry: parses hypotheses/H*.md (YAML front matter), validates required fields, optionally executes the
2 +linked verification commands, and emits research_log/discovery_log.jsonl + paper/tables/hypotheses.tex.
3 +
4 +Front matter schema (all required):
5 + id, title, axis (A|B|C|D), kind (algebraic|algorithmic|sha256|methodology|formal), statement, motivation, assumptions,
6 + expected_contribution, related_work (list of bib keys), possible_counterexamples, verification_strategy,
7 + computational_budget, status (planned|running|evaluated), classification (new_verified|known_reproduced|conditional|
8 + empirical_observation|rejected|unresolved|pending), evidence (list of result files), verdict (one sentence).
9 +"""
10 +import argparse, datetime, json, pathlib, re, subprocess, sys
11 +import yaml
12 +ROOT = pathlib.Path(__file__).resolve().parents[2]
13 +REQ = ["id", "title", "axis", "kind", "statement", "motivation", "assumptions", "expected_contribution", "related_work",
14 + "possible_counterexamples", "verification_strategy", "computational_budget", "status", "classification", "evidence", "verdict"]
15 +CLASSES = {"new_verified", "known_reproduced", "conditional", "empirical_observation", "rejected", "unresolved", "pending"}
16 +LABEL = {"new_verified": "New verified result", "known_reproduced": "Known result reproduced", "conditional": "Conditional result",
17 + "empirical_observation": "Empirical observation", "rejected": "Rejected conjecture", "unresolved": "Unresolved direction", "pending": "Pending"}
18 +
19 +
20 +def load():
21 + hyps = []
22 + for f in sorted((ROOT / "hypotheses").glob("H*.md")):
23 + txt = f.read_text()
24 + m = re.match(r"^---\n(.*?)\n---\n(.*)$", txt, re.S)
25 + if not m:
26 + raise SystemExit(f"{f}: missing front matter")
27 + meta = yaml.safe_load(m.group(1)); meta["body"] = m.group(2).strip(); meta["file"] = f.name
28 + missing = [k for k in REQ if k not in meta]
29 + if missing:
30 + raise SystemExit(f"{f}: missing fields {missing}")
31 + if meta["classification"] not in CLASSES:
32 + raise SystemExit(f"{f}: bad classification {meta['classification']}")
33 + for ev in meta["evidence"]:
34 + if not (ROOT / ev).exists():
35 + print(f"WARNING {f.name}: evidence file missing: {ev}")
36 + hyps.append(meta)
37 + return hyps
38 +
39 +
40 +def tex_escape(s: str) -> str:
41 + return s.replace("&", r"\&").replace("%", r"\%").replace("_", r"\_").replace("#", r"\#").replace("√", r"$\sqrt{\ }$").replace("≈", r"$\approx$").replace("≤", r"$\le$").replace("≥", r"$\ge$").replace("→", r"$\to$").replace("×", r"$\times$").replace("²", r"$^2$").replace("Ω", r"$\Omega$").replace("φ", r"$\varphi$").replace("λ", r"$\lambda$").replace("β", r"$\beta$").replace("Σ", r"$\Sigma$").replace("σ", r"$\sigma$").replace("δ", r"$\delta$").replace("π", r"$\pi$").replace("ω", r"$\omega$").replace("−", "--").replace("–", "--").replace("—", "---").replace("·", r"$\cdot$").replace("∈", r"$\in$").replace("⊕", r"$\oplus$").replace("≡", r"$\equiv$").replace("∞", r"$\infty$").replace("ℓ", r"$\ell$").replace("ε", r"$\varepsilon$")
42 +
43 +
44 +def main():
45 + ap = argparse.ArgumentParser(); ap.add_argument("--run", action="store_true", help="execute verification commands in front matter `commands`")
46 + a = ap.parse_args()
47 + hyps = load()
48 + counts = {}
49 + for h in hyps:
50 + counts[h["kind"]] = counts.get(h["kind"], 0) + 1
51 + log = ROOT / "research_log/discovery_log.jsonl"
52 + with log.open("w") as fh:
53 + for h in hyps:
54 + if a.run and h.get("commands"):
55 + for cmd in h["commands"]:
56 + print(f"[{h['id']}] $ {cmd}")
57 + subprocess.run(cmd, shell=True, check=True, cwd=ROOT)
58 + fh.write(json.dumps({"id": h["id"], "title": h["title"], "axis": h["axis"], "kind": h["kind"], "status": h["status"],
59 + "classification": h["classification"], "verdict": h["verdict"], "evidence": h["evidence"],
60 + "logged_utc": datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds")}) + "\n")
61 + # LaTeX table (longtable) for the paper
62 + rows = []
63 + for h in hyps:
64 + rows.append(f"{h['id']} & {h['axis']} & {tex_escape(h['title'])} & {LABEL[h['classification']]} & {tex_escape(h['verdict'])} \\\\")
65 + tex = ["% generated by experiments/automated_discovery/registry.py — do not edit",
66 + r"\begin{longtable}{@{}p{0.8cm}p{0.5cm}p{4.2cm}p{2.6cm}p{6.4cm}@{}}",
67 + r"\caption{Hypothesis portfolio and evaluation outcome. Classification vocabulary follows the mandate (new verified result; known result independently reproduced; conditional result; empirical observation; rejected conjecture; unresolved direction).}\label{tab:hypotheses}\\",
68 + r"\toprule ID & Axis & Hypothesis & Classification & Verdict \\ \midrule \endfirsthead",
69 + r"\toprule ID & Axis & Hypothesis & Classification & Verdict \\ \midrule \endhead",
70 + r"\bottomrule \endfoot"] + rows + [r"\end{longtable}"]
71 + (ROOT / "paper/tables").mkdir(exist_ok=True, parents=True)
72 + (ROOT / "paper/tables/hypotheses.tex").write_text("\n".join(tex) + "\n")
73 + summary = {"n_hypotheses": len(hyps), "by_kind": counts,
74 + "by_classification": {c: sum(1 for h in hyps if h["classification"] == c) for c in CLASSES}}
75 + (ROOT / "results/automated_discovery").mkdir(exist_ok=True, parents=True)
76 + (ROOT / "results/automated_discovery/hypothesis_summary.json").write_text(json.dumps(summary, indent=1))
77 + print(json.dumps(summary, indent=1))
78 +
79 +
80 +if __name__ == "__main__":
81 + main()
added experiments/baselines/scalar_mult_opcount.py +101 −0
@@ -0,0 +1,101 @@
1 +"""Operation counts for scalar multiplication on secp256k1: double-and-add, wNAF, GLV + interleaved wNAF.
2 +
3 +Counts group additions and doublings (affine model) for random 256-bit scalars; the GLV decomposition is the
4 +published libsecp256k1 lattice basis re-verified in secp256k1_facts.py. Writes results/baselines/scalar_mult_opcount.json.
5 +"""
6 +import argparse, json, random, sys, time, pathlib, statistics as st
7 +ROOT = pathlib.Path(__file__).resolve().parents[2]
8 +sys.path.insert(0, str(ROOT / "src"))
9 +from wp34 import secp256k1 as k1
10 +from wp34.ec import Curve
11 +from wp34.provenance import record
12 +
13 +E: Curve = k1.CURVE
14 +phi = lambda P: (k1.beta * P[0] % k1.p, P[1])
15 +
16 +
17 +def wnaf(k: int, w: int):
18 + naf = []
19 + while k > 0:
20 + if k & 1:
21 + d = k % (1 << w)
22 + if d >= (1 << (w - 1)):
23 + d -= 1 << w
24 + naf.append(d); k -= d
25 + else:
26 + naf.append(0)
27 + k >>= 1
28 + return naf
29 +
30 +
31 +def mul_glv_interleaved(k: int, P, w: int = 4):
32 + """[k]P = [k1]P + [k2]φ(P) with |k_i| ≲ √n, interleaved wNAF (Straus/Shamir with signed windows)."""
33 + k1_, k2_ = k1.glv_decompose(k)
34 + P1, P2 = P, phi(P)
35 + if k1_ < 0: k1_, P1 = -k1_, E.neg(P1)
36 + if k2_ < 0: k2_, P2 = -k2_, E.neg(P2)
37 + tabs = []
38 + for B in (P1, P2):
39 + twoB = E.double(B)
40 + t = [B]
41 + for _ in range((1 << (w - 2)) - 1):
42 + t.append(E.add(t[-1], twoB))
43 + tabs.append(t)
44 + n1, n2 = wnaf(k1_, w), wnaf(k2_, w)
45 + L = max(len(n1), len(n2))
46 + n1 += [0] * (L - len(n1)); n2 += [0] * (L - len(n2))
47 + R = None
48 + for i in range(L - 1, -1, -1):
49 + R = E.double(R)
50 + for d, t in ((n1[i], tabs[0]), (n2[i], tabs[1])):
51 + if d > 0: R = E.add(R, t[(d - 1) // 2])
52 + elif d < 0: R = E.add(R, E.neg(t[(-d - 1) // 2]))
53 + return R
54 +
55 +
56 +def main():
57 + ap = argparse.ArgumentParser(); ap.add_argument("--seed", type=int, default=34); ap.add_argument("--n", type=int, default=200)
58 + ap.add_argument("--out", default=str(ROOT / "results/baselines/scalar_mult_opcount.json"))
59 + a = ap.parse_args(); rng = random.Random(a.seed); t0 = time.time()
60 + methods = {
61 + "double_and_add": lambda k, P: E.mul(k, P),
62 + "wnaf_w4": lambda k, P: E.mul_wnaf(k, P, 4),
63 + "wnaf_w5": lambda k, P: E.mul_wnaf(k, P, 5),
64 + "glv_wnaf_w4": lambda k, P: mul_glv_interleaved(k, P, 4),
65 + "glv_wnaf_w5": lambda k, P: mul_glv_interleaved(k, P, 5),
66 + }
67 + rows = {m: {"add": [], "dbl": [], "total": [], "time_s": []} for m in methods}
68 + for i in range(a.n):
69 + k = rng.randrange(1, k1.n)
70 + P = E.mul(rng.randrange(1, k1.n), E.G)
71 + ref = None
72 + for m, f in methods.items():
73 + E.ops.reset(); t = time.perf_counter(); R = f(k, P); dt = time.perf_counter() - t
74 + if ref is None: ref = R
75 + assert R == ref, (m, i)
76 + rows[m]["add"].append(E.ops.add); rows[m]["dbl"].append(E.ops.dbl)
77 + rows[m]["total"].append(E.ops.add + E.ops.dbl); rows[m]["time_s"].append(dt)
78 + summary = {}
79 + base = st.mean(rows["double_and_add"]["total"])
80 + for m, r in rows.items():
81 + summary[m] = {"add_mean": st.mean(r["add"]), "dbl_mean": st.mean(r["dbl"]), "total_mean": st.mean(r["total"]),
82 + "total_sd": st.pstdev(r["total"]), "total_min": min(r["total"]), "total_max": max(r["total"]),
83 + "speedup_vs_double_and_add_ops": base / st.mean(r["total"]),
84 + "time_mean_ms": 1000 * st.mean(r["time_s"])}
85 + # GLV sub-scalar sizes
86 + sizes = []
87 + for _ in range(2000):
88 + k = rng.randrange(1, k1.n); a1_, a2_ = k1.glv_decompose(k)
89 + sizes.append((abs(a1_).bit_length(), abs(a2_).bit_length()))
90 + summary["glv_subscalar_bits"] = {"k1_max": max(s[0] for s in sizes), "k2_max": max(s[1] for s in sizes),
91 + "k1_mean": st.mean(s[0] for s in sizes), "k2_mean": st.mean(s[1] for s in sizes), "samples": 2000}
92 + record("scalar_mult_opcount_secp256k1", {"n_scalars": a.n, "model": "affine, counts group add/dbl"}, summary,
93 + seed=a.seed, out=a.out, started=t0)
94 + for m, s in summary.items():
95 + if m != "glv_subscalar_bits":
96 + print(f"{m:18s} add {s['add_mean']:7.1f} dbl {s['dbl_mean']:7.1f} total {s['total_mean']:7.1f} ± {s['total_sd']:.1f} speedup(ops) {s['speedup_vs_double_and_add_ops']:.3f} {s['time_mean_ms']:.1f} ms")
97 + print("GLV sub-scalars:", summary["glv_subscalar_bits"])
98 +
99 +
100 +if __name__ == "__main__":
101 + main()
added experiments/baselines/secp256k1_facts.py +120 −0
@@ -0,0 +1,120 @@
1 +"""Exact verification of the secp256k1 domain parameters and algebraic structure, cross-checked with PARI/GP.
2 +
3 +Writes results/baselines/secp256k1_facts.json. Every fact is computed with exact integer arithmetic twice:
4 +once in pure Python (src/wp34) and once in PARI/GP (independent implementation), and the two must agree.
5 +"""
6 +import argparse, json, subprocess, sys, time, pathlib
7 +ROOT = pathlib.Path(__file__).resolve().parents[2]
8 +sys.path.insert(0, str(ROOT / "src"))
9 +from wp34 import secp256k1 as k1
10 +from wp34.provenance import record
11 +
12 +
13 +def gp(script: str) -> list[str]:
14 + out = subprocess.run(["gp", "-q", "-f"], input="default(parisize,128000000);\n" + script + "\n",
15 + capture_output=True, text=True, timeout=1200)
16 + if out.returncode != 0:
17 + raise RuntimeError(out.stderr)
18 + return out.stdout.split("\n")
19 +
20 +
21 +def main():
22 + ap = argparse.ArgumentParser(); ap.add_argument("--out", default=str(ROOT / "results/baselines/secp256k1_facts.json"))
23 + args = ap.parse_args()
24 + t0 = time.time()
25 + facts = k1.verify_parameters()
26 + p, n, beta, lam = k1.p, k1.n, k1.beta, k1.lam
27 + twist = facts["twist_order"]
28 + script = f"""
29 +p={p}; n={n}; E=ellinit([0,7],p); G=[{k1.Gx},{k1.Gy}];
30 +print("p_is_prime=",isprime(p)); print("n_is_prime=",isprime(n)); print("ellcard=",ellcard(E));
31 +print("G_on_curve=",ellisoncurve(E,G)); print("nG_is_infinity=",ellmul(E,G,n)==[0]);
32 +print("trace_t=",ellap(E)); print("j_invariant=",lift(E.j));
33 +print("beta_cubed_is_1_mod_p=",Mod({beta},p)^3==1); print("lambda_cubed_is_1_mod_n=",Mod({lam},n)^3==1);
34 +print("phi_G_equals_lambda_G=",ellmul(E,G,{lam})==[Mod({beta},p)*G[1], G[2]]);
35 +tw={twist}; f=factor(tw,10^7); print("twist_small_primes=",f[,1]~); print("twist_small_exponents=",f[,2]~);
36 +c=tw; for(i=1,#f[,1], if(f[i,1]<10^7, c=c/f[i,1]^f[i,2])); print("twist_cofactor_is_prime=",isprime(c)); print("twist_cofactor_bits=",#binary(c));
37 +print("embedding_degree=",znorder(Mod(p,n)));
38 +print("n_minus_1_small_primes=",factor(n-1,10^7)[,1]~); print("p_minus_1_small_primes=",factor(p-1,10^7)[,1]~);
39 +t=ellap(E); d=t^2-4*p; print("frobenius_discriminant=",d); print("disc_is_minus3_times_square=",issquare(-d/3));
40 +z=sqrtn(Mod(1,p),3,&zz); print("cube_roots_unity_mod_p=",[lift(zz),lift(zz^2)]);
41 +y=sqrtn(Mod(1,n),3,&yy); print("cube_roots_unity_mod_n=",[lift(yy),lift(yy^2)]);
42 +print("kronecker_7_p=",kronecker(7,p)); print("num_roots_x3_plus_7=",#polrootsmod(x^3+7, p));
43 +"""
44 + g = {}
45 + for line in gp(script):
46 + if "=" in line:
47 + key, val = line.split("=", 1)
48 + g[key.strip()] = val.strip()
49 + def I(k): return int(g[k])
50 + def B(k): return g[k] == "1"
51 + gp_facts = {
52 + "p_is_prime": B("p_is_prime"), "n_is_prime": B("n_is_prime"), "ellcard": I("ellcard"), "G_on_curve": B("G_on_curve"),
53 + "nG_is_infinity": B("nG_is_infinity"), "trace_t": I("trace_t"), "j_invariant": I("j_invariant"),
54 + "beta_cubed_is_1_mod_p": B("beta_cubed_is_1_mod_p"), "lambda_cubed_is_1_mod_n": B("lambda_cubed_is_1_mod_n"),
55 + "phi_G_equals_lambda_G": B("phi_G_equals_lambda_G"),
56 + "twist_small_primes": g["twist_small_primes"], "twist_small_exponents": g["twist_small_exponents"],
57 + "twist_cofactor_is_prime": B("twist_cofactor_is_prime"), "twist_cofactor_bits": I("twist_cofactor_bits"),
58 + "embedding_degree": I("embedding_degree"),
59 + "n_minus_1_small_primes": g["n_minus_1_small_primes"], "p_minus_1_small_primes": g["p_minus_1_small_primes"],
60 + "frobenius_discriminant": I("frobenius_discriminant"), "disc_is_minus3_times_square": B("disc_is_minus3_times_square"),
61 + "cube_roots_unity_mod_p": [int(v) for v in g["cube_roots_unity_mod_p"].strip("[]").split(",")],
62 + "cube_roots_unity_mod_n": [int(v) for v in g["cube_roots_unity_mod_n"].strip("[]").split(",")],
63 + "kronecker_7_p": I("kronecker_7_p"), "num_roots_x3_plus_7": I("num_roots_x3_plus_7"),
64 + }
65 + agree = {
66 + "p_prime": facts["p_is_prime"] == gp_facts["p_is_prime"] == True,
67 + "n_prime": facts["n_is_prime"] == gp_facts["n_is_prime"] == True,
68 + "order_equals_n": gp_facts["ellcard"] == n,
69 + "trace": facts["trace_t"] == gp_facts["trace_t"],
70 + "j": facts["j_invariant"] == gp_facts["j_invariant"] == 0,
71 + "phi_lambda": facts["phi_G_equals_lambda_G"] == gp_facts["phi_G_equals_lambda_G"] == True,
72 + "twist_cofactor_prime": facts["twist_cofactor_is_prime"] == gp_facts["twist_cofactor_is_prime"],
73 + "twist_cofactor_bits": facts["twist_cofactor_bits"] == gp_facts["twist_cofactor_bits"],
74 + "embedding_degree_gt_100": facts["embedding_degree_le_100"] is None and gp_facts["embedding_degree"] > 100,
75 + "beta_in_gp_roots": beta in gp_facts["cube_roots_unity_mod_p"],
76 + "lambda_in_gp_roots": lam in gp_facts["cube_roots_unity_mod_n"],
77 + "x0_points": facts["x0_points_exist"] == (gp_facts["kronecker_7_p"] == 1),
78 + "cm_discriminant": facts["disc_is_minus3_times_square"] == gp_facts["disc_is_minus3_times_square"] == True,
79 + "frobenius_discriminant": facts["frobenius_discriminant"] == gp_facts["frobenius_discriminant"],
80 + }
81 + # The GLV basis can be re-derived from (n, λ) by the extended Euclidean algorithm (GLV 2001, §4).
82 + r0, r1 = n, lam
83 + s0, s1 = 1, 0
84 + t0_, t1 = 0, 1
85 + seq = []
86 + while r1 != 0:
87 + q = r0 // r1
88 + r0, r1 = r1, r0 - q * r1
89 + s0, s1 = s1, s0 - q * s1
90 + t0_, t1 = t1, t0_ - q * t1
91 + seq.append((r0, t0_))
92 + import math
93 + sq = math.isqrt(n)
94 + idx = next(i for i, (r, t) in enumerate(seq) if r < sq)
95 + # candidate vectors (r_i, -t_i) and (r_{i+1}, -t_{i+1}) or (r_{i-1}, -t_{i-1})
96 + cands = [(seq[idx - 1][0], -seq[idx - 1][1]), (seq[idx][0], -seq[idx][1]), (seq[idx + 1][0], -seq[idx + 1][1])]
97 + pub = {(k1.a1, k1.b1), (k1.a2, k1.b2)}
98 + derived = set()
99 + for a, b in cands:
100 + for sgn in (1, -1):
101 + if (sgn * a, sgn * b) in pub:
102 + derived.add((sgn * a, sgn * b))
103 + facts["glv_basis_rederived_from_euclid"] = len(derived) == 2
104 + facts["glv_euclid_candidates"] = [(str(a), str(b)) for a, b in cands]
105 + measurements = {"python": {k: (str(v) if isinstance(v, int) and abs(v) > 2**63 else v) for k, v in facts.items()},
106 + "pari_gp": {k: (str(v) if isinstance(v, int) and abs(v) > 2**63 else v) for k, v in gp_facts.items()},
107 + "agreement": agree, "all_agree": all(agree.values())}
108 + record("secp256k1_parameter_verification", {"gp_version": subprocess.run(["gp", "--version-short"], capture_output=True, text=True).stdout.strip() or "2.19"},
109 + measurements, out=args.out, started=t0)
110 + print(json.dumps(agree, indent=1)); print("ALL AGREE:", all(agree.values()))
111 + print("twist cofactor bits", facts["twist_cofactor_bits"], "prime", facts["twist_cofactor_is_prime"], "| small factors", facts["twist_small_factors"])
112 + print("embedding degree (gp):", gp_facts["embedding_degree"] if gp_facts["embedding_degree"] < 10**6 else "huge")
113 + print("trace t =", facts["trace_t"], "conductor f =", facts["conductor_f"])
114 + print("GLV basis re-derived from Euclid:", facts["glv_basis_rederived_from_euclid"])
115 + print("x=0 points exist:", facts["x0_points_exist"], "| #roots of x^3+7:", gp_facts["num_roots_x3_plus_7"])
116 + print("n-1 small factors", facts["n_minus_1_small_factors"], "| p-1 small factors", facts["p_minus_1_small_factors"])
117 +
118 +
119 +if __name__ == "__main__":
120 + main()
added experiments/reduced_round_sha256/README.md +56 −0
@@ -0,0 +1,56 @@
1 +# Reduced-round SHA-256 experiments (WP34 Axis C)
2 +
3 +All scripts are re-runnable from the repository root with `PYTHONPATH=src .venv/bin/python
4 +experiments/reduced_round_sha256/<script>.py --seed S --out results/reduced_round_sha256/<name>.json`. Every result JSON
5 +is written with `wp34.provenance.record` (host, CPU, RAM, OS, library versions, seed, command, timestamp). The reference
6 +`src/wp34/sha256.py` is never modified; all helpers live here.
7 +
8 +| file | role |
9 +|---|---|
10 +| `common.py` | path setup, peak-RSS (`resource.getrusage`), random-oracle control (hashlib over a secret salt), bootstrap CI |
11 +| `diffusion.py` | avalanche of the compression function, steps 1..64, single-bit difference in W_0 / W_5 / W_15 / IV |
12 +| `bias_test.py` | bit bias, input/output linear correlations, byte chi-square for r in {1,...,64} + random-oracle control |
13 +| `cnf_sha256.py` | Tseitin CNF encoder (words = 32 literals, free rotations, Ch/Maj/XOR gates, ripple-carry adders); `--selftest` |
14 +| `solvers.py` | CaDiCaL (binary, `-t`), Kissat (binary, `--time`), z3 bit-vector model; uniform statistics dict |
15 +| `preimage_sat.py` | 3a: preimage cells (r, k, seed, solver, placement); `--list-cells`, `--cell`, `--aggregate` |
16 +| `collision_sat.py` | 3b: collision cells (r, seed, solver, iv fixed/free, feed-forward); same modes |
17 +| `local_collision.py` | 3c: 9-step local collision — GF(2) verification, empirical conformance, SAT conforming pairs |
18 +| `cluster_driver.py` | cell queue runner: one fresh subprocess per cell, N workers, seed-0 phase first, deadline, kill guard |
19 +| `run_cluster.sh` | `setup` / `launch` / `status` / `fetch` on cluster node M3U96b |
20 +| `make_summary.py` | aggregates everything into `results/reduced_round_sha256/summary.json` |
21 +| `../../figures/make_sha256_figures.py` | regenerates `figures/sha256_{diffusion,bias,sat_scaling,collision_scaling,local_collision}.pdf` |
22 +| `../../tests/test_sha256_cnf.py`, `test_sha256_local_collision.py` | gates: encoder == reference for r in {1,4,16,64}; pattern cancels; vectorised step == reference |
23 +
24 +## What ran, where, how long
25 +
26 +| experiment | host | command | runs | wall |
27 +|---|---|---|---|---|
28 +| diffusion | laptop (MacBook Pro M5 Max, 18 c) | `diffusion.py --seed {1,2,3} --n 2000` | 3 seeds x 4 configs x 2000 msgs | 11.5 s per seed |
29 +| bias | laptop | `bias_test.py --seed {1,2,3} --n 20000` | 3 seeds x 14 step counts | 7.6 s per seed |
30 +| local collision | laptop | `local_collision.py --seed 1 --n 100000 --sat-bits all --sat-timeout 120` and `--seed 2 --sat-bits 31,0 --sat-timeout 900` | 32 x 100 000 samples; 32 + 2 SAT runs | 943 s + 70 s |
31 +| preimage SAT grid | **M3U96b** (Mac Studio M3 Ultra, 28 c / 96 GB, macOS 26.4, python 3.12 venv via uv) | `run_cluster.sh launch` -> `cluster_driver.py --script preimage_sat.py --workers 14 --deadline 4800` | see below | see below |
32 +| collision SAT grid | M3U96b | `cluster_driver.py --script collision_sat.py --workers 10 --deadline 4800` | see below | see below |
33 +
34 +Solver binaries copied to `M3U96b:~/wp34/bin/` (kissat 4.0.4 and cadical 3.0.1 from Homebrew, self-contained arm64;
35 +checked with `--version` and a 2-clause instance). Results come back with `run_cluster.sh fetch` into
36 +`results/reduced_round_sha256/m3u96b/` (per-cell JSONs + driver logs) and the two aggregated files are copied to
37 +`results/reduced_round_sha256/{preimage_sat_scaling,collision_sat}.json`.
38 +
39 +GRID-PLACEHOLDER
40 +
41 +## Design notes and gotchas
42 +
43 +* **Preimage free bits.** Placement `first` (as specified: the first k message bits, W_0 LSB-first, then W_1) makes every
44 + r <= 16 cell solvable by unit propagation alone (0 conflicts): the step function is a bijection and with all other words
45 + fixed the solver simply inverts the steps from the known target. This is a property of the compression function
46 + without unknown IV, not a solver feat. A second placement `spread` (bit i -> word i mod 16, CaDiCaL only) was added so
47 + that the difficulty does not collapse; both are reported.
48 +* **Collisions.** M != M' is restricted to the words an r-step compression reads (W_0..W_{min(r,16)-1}); otherwise a
49 + difference in an unused word is a trivial "collision" (this happened in the first smoke test). With a shared IV,
50 + feed-forward does not change the collision event; the two CNFs differ only by the extra adders.
51 +* **Timeouts.** pysat's `interrupt()` does not stop CaDiCaL 1.9.5 in-process; the first launch had to be killed and
52 + CaDiCaL is now the standalone binary with a real-time limit. CaDiCaL omits statistics lines whose value is 0, which the
53 + parser maps to 0 (only when the status is SAT/UNSAT).
54 +* **Seeds.** CaDiCaL/Kissat receive `--seed`; the instance (message, target) also depends on the seed for preimage cells.
55 + Collision cells have no instance randomness (the seed only changes the solver seed).
56 +* Peak RSS is measured per cell (fresh process; `ru_maxrss` of self and children).
added experiments/reduced_round_sha256/bias_test.py +98 −0
@@ -0,0 +1,98 @@
1 +"""Statistical regularity tests on r-step SHA-256 (single block, standard IV, with feed-forward), C1.
2 +
3 +For N random 512-bit messages (seeded) and r in R = {1,2,3,4,5,6,8,10,12,16,20,24,32,64}:
4 + (i) bias of each of the 256 output bits, |mean - 0.5|; summary = max and number above the 4-sigma level
5 + 4 * 0.5/sqrt(N);
6 + (ii) linear correlation c(i,j) = E[(-1)^(x_i xor y_j)] between each of 64 randomly sampled input bits
7 + (from W_0..W_15) and every output bit; count of pairs with |c| > 4/sqrt(N) (under H0: c ~ N(0, 1/N), so the
8 + expected count under uniformity is 64*256*P(|Z|>4) ~ 1.04);
9 + (iii) chi-square uniformity test on each of the 32 output bytes (255 d.f.), min p-value and number of bytes
10 + rejected at alpha = 0.01 / 32 (Bonferroni).
11 +Controls: r = 64 (full compression) and a random oracle (hashlib SHA-256 over a secret salt and the message).
12 +One 64-step trace per message gives every r at once (state after r steps + IV feed-forward).
13 +
14 +Usage: PYTHONPATH=src .venv/bin/python experiments/reduced_round_sha256/bias_test.py --seed 1 --n 20000 \
15 + --out results/reduced_round_sha256/bias.json
16 +"""
17 +from __future__ import annotations
18 +import argparse, os, random, sys, time
19 +import numpy as np
20 +from scipy import stats as sps
21 +from common import S, provenance, random_block, state_to_int, random_oracle, peak_rss_mb, MASK
22 +
23 +R_LIST = [1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24, 32, 64]
24 +
25 +
26 +def bits_matrix(ints, nbits=256) -> np.ndarray:
27 + """N x nbits uint8 matrix, column 0 = most significant bit."""
28 + arr = np.array([[(v >> (nbits - 1 - i)) & 1 for i in range(nbits)] for v in ints], dtype=np.uint8)
29 + return arr
30 +
31 +
32 +def analyse(Y: np.ndarray, X: np.ndarray, n: int, label: str):
33 + thr = 4 * 0.5 / n ** 0.5
34 + bias = np.abs(Y.mean(axis=0) - 0.5)
35 + Xs = 1.0 - 2.0 * X.astype(np.float64)
36 + Ys = 1.0 - 2.0 * Y.astype(np.float64)
37 + C = Xs.T @ Ys / n # 64 x 256
38 + cthr = 4 / n ** 0.5
39 + big = int((np.abs(C) > cthr).sum())
40 + # chi-square per output byte
41 + pvals, chis = [], []
42 + for byte in range(32):
43 + cols = Y[:, byte * 8:(byte + 1) * 8].astype(np.int64)
44 + vals = (cols * (1 << np.arange(7, -1, -1))).sum(axis=1)
45 + counts = np.bincount(vals, minlength=256)
46 + chi, p = sps.chisquare(counts)
47 + pvals.append(float(p)); chis.append(float(chi))
48 + alpha = 0.01 / 32
49 + res = {
50 + "max_bit_bias": float(bias.max()), "mean_bit_bias": float(bias.mean()),
51 + "bias_threshold_4sigma": thr, "n_bits_bias_over_4sigma": int((bias > thr).sum()),
52 + "bit_bias": bias.round(6).tolist(),
53 + "max_abs_corr": float(np.abs(C).max()), "corr_threshold_4sigma": cthr,
54 + "n_pairs_corr_over_4sigma": big, "expected_pairs_under_null": float(64 * 256 * 2 * sps.norm.sf(4)),
55 + "argmax_corr_pair": [int(x) for x in np.unravel_index(np.abs(C).argmax(), C.shape)],
56 + "chi2_min_p": float(min(pvals)), "chi2_n_bytes_rejected_bonferroni": int(sum(p < alpha for p in pvals)),
57 + "chi2_p_by_byte": pvals, "chi2_stat_by_byte": chis,
58 + }
59 + print(f"[{label}] max bias {res['max_bit_bias']:.4f} (#>4s {res['n_bits_bias_over_4sigma']}), "
60 + f"max|corr| {res['max_abs_corr']:.4f} (#>4s {big}), chi2 min p {res['chi2_min_p']:.3g} "
61 + f"(rejected {res['chi2_n_bytes_rejected_bonferroni']}/32)", file=sys.stderr)
62 + return res
63 +
64 +
65 +def main():
66 + ap = argparse.ArgumentParser()
67 + ap.add_argument("--seed", type=int, default=1)
68 + ap.add_argument("--n", type=int, default=20000)
69 + ap.add_argument("--n-input-bits", type=int, default=64)
70 + ap.add_argument("--out", default="results/reduced_round_sha256/bias.json")
71 + a = ap.parse_args()
72 + t0 = time.time()
73 + rng = random.Random(a.seed)
74 + salt = os.urandom(32)
75 + in_bits = sorted(rng.sample(range(512), a.n_input_bits))
76 + blocks = [random_block(rng) for _ in range(a.n)]
77 + traces = []
78 + for blk in blocks:
79 + tr = []
80 + S.compress(S.H0, blk, 64, feedforward=False, trace=tr)
81 + traces.append(tr)
82 + msg_ints = [int.from_bytes(b, "big") for b in blocks]
83 + X = bits_matrix(msg_ints, 512)[:, in_bits]
84 + meas = {"input_bits_sampled": in_bits, "by_r": {}}
85 + for r in R_LIST:
86 + outs = [state_to_int([(x + y) & MASK for x, y in zip(S.H0, tr[r - 1])]) for tr in traces]
87 + meas["by_r"][str(r)] = analyse(bits_matrix(outs), X, a.n, f"r={r}")
88 + ro = [random_oracle(salt, b) for b in blocks]
89 + meas["random_oracle_control"] = analyse(bits_matrix(ro), X, a.n, "RO")
90 + stats = {"compress_calls": a.n, "wall_s": round(time.time() - t0, 3), "peak_rss_mb": peak_rss_mb()}
91 + provenance.record("sha256_bias_test", {"n": a.n, "r_list": R_LIST, "n_input_bits": a.n_input_bits,
92 + "feedforward": True, "iv": "standard H0"},
93 + meas, seed=a.seed, extra={"op_stats": stats}, out=a.out, started=t0)
94 + print("wrote", a.out, stats)
95 +
96 +
97 +if __name__ == "__main__":
98 + main()
added experiments/reduced_round_sha256/cluster_driver.py +95 −0
@@ -0,0 +1,95 @@
1 +"""Cell-queue job runner: runs every cell of an experiment script as a fresh subprocess (one solver thread each),
2 +at most --workers concurrent, in phases (seed 0 for all cells first, then the remaining seeds) with cells ordered by
3 +expected cost inside a phase, so that a --deadline cuts the number of seeds of the largest cells rather than whole
4 +rows. Per-cell JSONs go to --cells-dir; cells already present are skipped (re-runnable). Finally --aggregate.
5 +
6 +Usage: python cluster_driver.py --script preimage_sat.py --cells-dir results/cells_preimage \
7 + --out results/preimage_sat_scaling.json --workers 24 --deadline 7200 [--timeout 300]
8 +"""
9 +from __future__ import annotations
10 +import argparse, json, os, pathlib, subprocess, sys, time
11 +from concurrent.futures import ThreadPoolExecutor, as_completed
12 +
13 +HERE = pathlib.Path(__file__).resolve().parent
14 +
15 +
16 +def cell_name(c: dict) -> str:
17 + return "_".join(f"{k}-{c[k]}" for k in sorted(c))
18 +
19 +
20 +def cost(c: dict) -> tuple:
21 + return (c.get("placement", "first") == "spread", c["r"], c.get("k", 0), c.get("iv") == "free", int(c.get("feedforward", 0)))
22 +
23 +
24 +def main():
25 + ap = argparse.ArgumentParser()
26 + ap.add_argument("--script", required=True)
27 + ap.add_argument("--cells-dir", required=True)
28 + ap.add_argument("--out", required=True)
29 + ap.add_argument("--workers", type=int, default=24)
30 + ap.add_argument("--deadline", type=float, default=1e9, help="seconds after start; no new cell is launched after it")
31 + ap.add_argument("--timeout", type=float, default=None)
32 + ap.add_argument("--list-args", default="")
33 + ap.add_argument("--python", default=sys.executable)
34 + a = ap.parse_args()
35 + script = str(HERE / a.script)
36 + cells_dir = pathlib.Path(a.cells_dir); cells_dir.mkdir(parents=True, exist_ok=True)
37 + lst = subprocess.run([a.python, script, "--list-cells"] + a.list_args.split(), capture_output=True, text=True, check=True)
38 + cells = [json.loads(l) for l in lst.stdout.splitlines() if l.strip()]
39 + # phases: seed 0 first (all cells), then seeds 1, 2, ...; inside a phase by cost
40 + cells.sort(key=lambda c: (c["seed"], cost(c)))
41 + todo = [c for c in cells if not (cells_dir / (cell_name(c) + ".json")).exists()]
42 + print(f"{len(cells)} cells, {len(todo)} to run, workers={a.workers}, deadline={a.deadline}s", flush=True)
43 + t0 = time.time()
44 + log = open(cells_dir / "driver.log", "a")
45 + skipped = []
46 +
47 + def run(c):
48 + outp = cells_dir / (cell_name(c) + ".json")
49 + cmd = [a.python, script, "--cell", json.dumps(c), "--out", str(outp)]
50 + if a.timeout:
51 + cmd += ["--timeout", str(a.timeout)]
52 + env = dict(os.environ, OMP_NUM_THREADS="1", PYTHONPATH=str(HERE.parents[1] / "src"))
53 + t = time.time()
54 + try:
55 + p = subprocess.run(cmd, capture_output=True, text=True, env=env, timeout=(a.timeout or 600) + 120)
56 + except subprocess.TimeoutExpired:
57 + outp.write_text(json.dumps({"cell": c, "status": "timeout", "killed_by_driver": True,
58 + "wall_s": time.time() - t, "hostname": os.uname().nodename}))
59 + p = subprocess.CompletedProcess(cmd, -9, "", "killed by driver guard")
60 + status = "?"
61 + if outp.exists():
62 + try:
63 + status = json.loads(outp.read_text()).get("status")
64 + except Exception:
65 + status = "bad-json"
66 + else:
67 + status = "failed: " + p.stderr[-300:].replace("\n", " | ")
68 + line = f"{time.strftime('%H:%M:%S')} {cell_name(c)} -> {status} ({time.time()-t:.1f}s) [elapsed {time.time()-t0:.0f}s]"
69 + print(line, flush=True); log.write(line + "\n"); log.flush()
70 + return c, status
71 +
72 + with ThreadPoolExecutor(max_workers=a.workers) as ex:
73 + futs = []
74 + it = iter(todo)
75 + # submit lazily so the deadline can stop launches
76 + pending = set()
77 + for c in it:
78 + while len(pending) >= a.workers:
79 + done = next(as_completed(pending)); pending.discard(done)
80 + if time.time() - t0 > a.deadline:
81 + skipped.append(c)
82 + continue
83 + pending.add(ex.submit(run, c))
84 + for f in as_completed(pending):
85 + pass
86 + if skipped:
87 + (cells_dir / "skipped_cells.json").write_text(json.dumps(skipped, indent=1))
88 + print(f"deadline reached: {len(skipped)} cells not started (see skipped_cells.json)")
89 + subprocess.run([a.python, script, "--aggregate", str(cells_dir), "--out", a.out], check=True,
90 + env=dict(os.environ, PYTHONPATH=str(HERE.parents[1] / "src")))
91 + print("done in %.0f s" % (time.time() - t0))
92 +
93 +
94 +if __name__ == "__main__":
95 + main()
added experiments/reduced_round_sha256/cnf_sha256.py +313 −0
@@ -0,0 +1,313 @@
1 +"""Tseitin CNF encoder for r-step SHA-256 compression (research question C2).
2 +
3 +Representation. A *bit* is either a DIMACS literal (non-zero int) or a Python bool constant; a *word* is a list of 32
4 +bits, index 0 = least significant bit. Rotations and shifts are free (list re-indexing; shifted-in bits are the
5 +constant False). Gates with constant inputs are folded. Gates:
6 + xor(a,b) : 4 clauses and(a,b) : 3 clauses not : literal negation
7 + ch(e,f,g) : 4 clauses (z = e ? f : g) maj(a,b,c) : 6 clauses
8 + full adder : sum = xor(xor(x,y),c) (8 clauses), carry = maj(x,y,c) (6 clauses) -> 14 clauses per bit
9 + add(x, y) : 32-bit ripple-carry (carry out of bit 31 dropped = mod 2^32); multi-operand sums are chained
10 + 2-operand adders.
11 +The step and schedule follow `wp34.sha256` exactly (T1 = h + S1(e) + Ch(e,f,g) + K_t + W_t; T2 = S0(a) + Maj(a,b,c);
12 +a' = T1 + T2, e' = d + T1). Message words are always fresh variables (fixing bits = unit clauses), so the
13 +variable/clause counts of an instance do not depend on how many bits are fixed. The IV is either the constant H0
14 +(folded) or 8 free words (semi-free-start).
15 +
16 +`build_compression(r, feedforward, free_iv)` returns (cnf, W, IV, OUT) where W = 16 message words, IV = 8 words
17 +(constants or variables) and OUT = 8 output words (after r steps, with or without the IV feed-forward).
18 +`decode_word(model, word)` reads a word back from a solver model. The self-test (`python cnf_sha256.py --selftest`)
19 +fixes all 512 message bits + IV and checks the solved output against `wp34.sha256.compress`.
20 +"""
21 +from __future__ import annotations
22 +import argparse, random, sys, time
23 +from common import S, MASK
24 +
25 +K = S.K
26 +H0 = S.H0
27 +
28 +
29 +class CNF:
30 + def __init__(self):
31 + self.nv = 0
32 + self.clauses: list[list[int]] = []
33 + self._true = None
34 +
35 + # ---- variables -------------------------------------------------------------------------------------------
36 + def var(self) -> int:
37 + self.nv += 1
38 + return self.nv
39 +
40 + def word(self) -> list[int]:
41 + return [self.var() for _ in range(32)]
42 +
43 + @staticmethod
44 + def const_word(v: int) -> list[bool]:
45 + return [bool((v >> i) & 1) for i in range(32)]
46 +
47 + def true_lit(self) -> int:
48 + if self._true is None:
49 + self._true = self.var()
50 + self.clauses.append([self._true])
51 + return self._true
52 +
53 + def lit(self, b) -> int:
54 + """Any bit -> literal (constants mapped onto a pinned TRUE variable)."""
55 + if b is True:
56 + return self.true_lit()
57 + if b is False:
58 + return -self.true_lit()
59 + return b
60 +
61 + def add(self, *lits):
62 + self.clauses.append(list(lits))
63 +
64 + # ---- gates -------------------------------------------------------------------------------------------------
65 + @staticmethod
66 + def neg(a):
67 + if isinstance(a, bool):
68 + return not a
69 + return -a
70 +
71 + def xor(self, a, b):
72 + if isinstance(a, bool):
73 + return self.neg(b) if a else b
74 + if isinstance(b, bool):
75 + return self.neg(a) if b else a
76 + if a == b:
77 + return False
78 + if a == -b:
79 + return True
80 + z = self.var()
81 + self.add(-a, -b, -z); self.add(a, b, -z); self.add(a, -b, z); self.add(-a, b, z)
82 + return z
83 +
84 + def and_(self, a, b):
85 + if isinstance(a, bool):
86 + return b if a else False
87 + if isinstance(b, bool):
88 + return a if b else False
89 + if a == b:
90 + return a
91 + if a == -b:
92 + return False
93 + z = self.var()
94 + self.add(-z, a); self.add(-z, b); self.add(z, -a, -b)
95 + return z
96 +
97 + def or_(self, a, b):
98 + return self.neg(self.and_(self.neg(a), self.neg(b)))
99 +
100 + def ch(self, e, f, g):
101 + """z = (e and f) xor (not e and g) = e ? f : g."""
102 + if isinstance(e, bool):
103 + return f if e else g
104 + if isinstance(f, bool) and isinstance(g, bool):
105 + if f == g:
106 + return f
107 + return e if f else self.neg(e)
108 + if not isinstance(f, bool) and not isinstance(g, bool) and f == g:
109 + return f # (never compare a literal with a bool: in Python 1 == True)
110 + f, g = self.lit(f), self.lit(g)
111 + z = self.var()
112 + self.add(-e, -f, z); self.add(-e, f, -z); self.add(e, -g, z); self.add(e, g, -z)
113 + return z
114 +
115 + def maj(self, a, b, c):
116 + for x, y, w in ((a, b, c), (b, c, a), (c, a, b)):
117 + if isinstance(x, bool):
118 + return self.or_(y, w) if x else self.and_(y, w)
119 + if a == b or a == c:
120 + return a
121 + if b == c:
122 + return b
123 + if a == -b:
124 + return c
125 + if a == -c:
126 + return b
127 + if b == -c:
128 + return a
129 + z = self.var()
130 + self.add(-a, -b, z); self.add(-a, -c, z); self.add(-b, -c, z)
131 + self.add(a, b, -z); self.add(a, c, -z); self.add(b, c, -z)
132 + return z
133 +
134 + # ---- words -------------------------------------------------------------------------------------------------
135 + def xor_w(self, x, y):
136 + return [self.xor(p, q) for p, q in zip(x, y)]
137 +
138 + def and_w(self, x, y):
139 + return [self.and_(p, q) for p, q in zip(x, y)]
140 +
141 + def not_w(self, x):
142 + return [self.neg(p) for p in x]
143 +
144 + @staticmethod
145 + def rotr(x, k):
146 + return [x[(i + k) % 32] for i in range(32)]
147 +
148 + @staticmethod
149 + def shr(x, k):
150 + return [x[i + k] if i + k < 32 else False for i in range(32)]
151 +
152 + def Sigma0(self, x):
153 + return self.xor_w(self.xor_w(self.rotr(x, 2), self.rotr(x, 13)), self.rotr(x, 22))
154 +
155 + def Sigma1(self, x):
156 + return self.xor_w(self.xor_w(self.rotr(x, 6), self.rotr(x, 11)), self.rotr(x, 25))
157 +
158 + def sigma0(self, x):
159 + return self.xor_w(self.xor_w(self.rotr(x, 7), self.rotr(x, 18)), self.shr(x, 3))
160 +
161 + def sigma1(self, x):
162 + return self.xor_w(self.xor_w(self.rotr(x, 17), self.rotr(x, 19)), self.shr(x, 10))
163 +
164 + def ch_w(self, e, f, g):
165 + return [self.ch(p, q, s) for p, q, s in zip(e, f, g)]
166 +
167 + def maj_w(self, a, b, c):
168 + return [self.maj(p, q, s) for p, q, s in zip(a, b, c)]
169 +
170 + def add_w(self, x, y):
171 + """Ripple-carry modular adder."""
172 + out, carry = [], False
173 + for i in range(32):
174 + s = self.xor(self.xor(x[i], y[i]), carry)
175 + if i < 31:
176 + carry = self.maj(x[i], y[i], carry)
177 + out.append(s)
178 + return out
179 +
180 + def sum_w(self, *words):
181 + acc = words[0]
182 + for w in words[1:]:
183 + acc = self.add_w(acc, w)
184 + return acc
185 +
186 + def eq_w(self, x, y):
187 + """Assert x == y bitwise."""
188 + for p, q in zip(x, y):
189 + self.assert_eq(p, q)
190 +
191 + def assert_eq(self, p, q):
192 + if isinstance(p, bool) and isinstance(q, bool):
193 + if p != q:
194 + self.add(-self.true_lit())
195 + return
196 + if isinstance(p, bool):
197 + p, q = q, p
198 + if isinstance(q, bool):
199 + self.add(p if q else -p)
200 + return
201 + self.add(-p, q); self.add(p, -q)
202 +
203 + def fix_word(self, w, value: int):
204 + for i in range(32):
205 + self.assert_eq(w[i], bool((value >> i) & 1))
206 +
207 + def neq_w_any(self, pairs):
208 + """Assert that at least one bit differs among the given (word, word) pairs: OR of XORs."""
209 + diffs = []
210 + for x, y in pairs:
211 + for p, q in zip(x, y):
212 + d = self.xor(p, q)
213 + if d is True:
214 + return
215 + if d is not False:
216 + diffs.append(d)
217 + if not diffs:
218 + self.add(-self.true_lit())
219 + else:
220 + self.add(*diffs)
221 +
222 + # ---- SHA-256 -----------------------------------------------------------------------------------------------
223 + def step(self, state, k: int, w):
224 + a, b, c, d, e, f, g, h = state
225 + T1 = self.sum_w(h, self.Sigma1(e), self.ch_w(e, f, g), self.const_word(k), w)
226 + T2 = self.add_w(self.Sigma0(a), self.maj_w(a, b, c))
227 + return (self.add_w(T1, T2), a, b, c, self.add_w(d, T1), e, f, g)
228 +
229 + def schedule(self, W16, rounds: int):
230 + W = list(W16)
231 + for t in range(16, rounds):
232 + W.append(self.sum_w(self.sigma1(W[t - 2]), W[t - 7], self.sigma0(W[t - 15]), W[t - 16]))
233 + return W
234 +
235 + def compress(self, IV, W16, rounds: int, feedforward: bool, trace=None):
236 + W = self.schedule(W16, rounds)
237 + s = tuple(IV)
238 + for t in range(rounds):
239 + s = self.step(s, K[t], W[t])
240 + if trace is not None:
241 + trace.append(s)
242 + if not feedforward:
243 + return list(s)
244 + return [self.add_w(x, y) for x, y in zip(IV, s)]
245 +
246 + def stats(self):
247 + return {"n_vars": self.nv, "n_clauses": len(self.clauses)}
248 +
249 + def to_dimacs(self) -> str:
250 + lines = ["p cnf %d %d" % (self.nv, len(self.clauses))]
251 + lines += [" ".join(map(str, c)) + " 0" for c in self.clauses]
252 + return "\n".join(lines) + "\n"
253 +
254 +
255 +def build_compression(rounds: int, feedforward: bool = False, free_iv: bool = False, cnf: CNF | None = None):
256 + cnf = cnf or CNF()
257 + W = [cnf.word() for _ in range(16)]
258 + IV = [cnf.word() for _ in range(8)] if free_iv else [CNF.const_word(h) for h in H0]
259 + OUT = cnf.compress(IV, W, rounds, feedforward)
260 + return cnf, W, IV, OUT
261 +
262 +
263 +def decode_word(model_set, word) -> int:
264 + """model_set: set of true literals (positive ints)."""
265 + v = 0
266 + for i, b in enumerate(word):
267 + if b is True or (not isinstance(b, bool) and ((b > 0 and b in model_set) or (b < 0 and -b not in model_set))):
268 + v |= 1 << i
269 + return v
270 +
271 +
272 +def model_to_set(model) -> set:
273 + return {l for l in model if l > 0}
274 +
275 +
276 +def selftest(rounds_list=(1, 4, 16, 64), n_msgs=3, seed=0, verbose=True):
277 + from pysat.solvers import Cadical195
278 + rng = random.Random(seed)
279 + ok = True
280 + for r in rounds_list:
281 + for ff in (False, True):
282 + for _ in range(n_msgs):
283 + block = bytes(rng.getrandbits(8) for _ in range(64))
284 + words = S.bytes_to_words(block)
285 + cnf, W, IV, OUT = build_compression(r, feedforward=ff)
286 + for w, v in zip(W, words):
287 + cnf.fix_word(w, v)
288 + t0 = time.time()
289 + with Cadical195(bootstrap_with=cnf.clauses) as s:
290 + assert s.solve(), "unexpected UNSAT"
291 + m = model_to_set(s.get_model())
292 + got = [decode_word(m, w) for w in OUT]
293 + ref = S.compress(S.H0, block, r, feedforward=ff)
294 + good = got == ref
295 + ok &= good
296 + if verbose:
297 + print(f"r={r:2d} ff={ff!s:5} vars={cnf.nv:7d} clauses={len(cnf.clauses):8d} "
298 + f"solve={time.time()-t0:.3f}s match={good}")
299 + return ok
300 +
301 +
302 +if __name__ == "__main__":
303 + ap = argparse.ArgumentParser()
304 + ap.add_argument("--selftest", action="store_true")
305 + ap.add_argument("--dimacs", help="write the r-step CNF (free message, fixed IV) to this file")
306 + ap.add_argument("--rounds", type=int, default=64)
307 + a = ap.parse_args()
308 + if a.selftest:
309 + sys.exit(0 if selftest() else 1)
310 + if a.dimacs:
311 + cnf, *_ = build_compression(a.rounds)
312 + open(a.dimacs, "w").write(cnf.to_dimacs())
313 + print(cnf.stats())
added experiments/reduced_round_sha256/collision_sat.py +137 −0
@@ -0,0 +1,137 @@
1 +"""3b. SAT collision search on r-step SHA-256 compression (question C2/C4).
2 +
3 +Instance: two message blocks M, M' (fresh variables each) with M != M' (OR of the XOR differences over the words
4 +W_0..W_{min(r,16)-1} that an r-step compression reads; unused words are pinned to 0), equal r-step output. Variants: feedforward in {False, True} x iv in {standard, free} (semi-free-start: IV is 8 free
5 +words shared by both messages). Note that with a *shared* IV the feed-forward adds the same value to both sides,
6 +so a collision with feed-forward is mathematically the same event as without; the two CNFs nevertheless differ
7 +(extra adders), which is what the SAT comparison measures. Found collisions are verified with
8 +wp34.sha256.compress and saved in hex. Solvers: CaDiCaL (pysat) and Kissat; timeout 600 s per cell.
9 +Modes as in preimage_sat.py (--list-cells / --cell / --aggregate).
10 +"""
11 +from __future__ import annotations
12 +import argparse, json, pathlib, random, time
13 +from common import S, provenance, peak_rss_mb, MASK
14 +import cnf_sha256 as C
15 +import solvers as SV
16 +
17 +R_LIST = [4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24]
18 +TIMEOUT = 600.0
19 +VARIANTS = [("fixed", False), ("fixed", True), ("free", False), ("free", True)] # (iv, feedforward)
20 +
21 +
22 +def list_cells(seeds, solvers):
23 + cells = []
24 + for iv, ff in VARIANTS:
25 + for r in R_LIST:
26 + for seed in seeds:
27 + for sol in solvers:
28 + cells.append({"r": r, "seed": seed, "solver": sol, "iv": iv, "feedforward": ff})
29 + return cells
30 +
31 +
32 +def build(r: int, free_iv: bool, ff: bool):
33 + cnf = C.CNF()
34 + W1 = [cnf.word() for _ in range(16)]
35 + W2 = [cnf.word() for _ in range(16)]
36 + IV = [cnf.word() for _ in range(8)] if free_iv else [C.CNF.const_word(h) for h in S.H0]
37 + O1 = cnf.compress(IV, W1, r, ff)
38 + O2 = cnf.compress(IV, W2, r, ff)
39 + for x, y in zip(O1, O2):
40 + cnf.eq_w(x, y)
41 + # M != M' restricted to the words an r-step compression actually reads (W_0..W_{min(r,16)-1}); otherwise a
42 + # difference in an unused word is a trivial "collision".
43 + used = min(r, 16)
44 + cnf.neq_w_any(list(zip(W1[:used], W2[:used])))
45 + for j in range(used, 16): # unused words are forced equal (and to zero) to remove the useless freedom
46 + cnf.fix_word(W1[j], 0); cnf.fix_word(W2[j], 0)
47 + return cnf, W1, W2, IV
48 +
49 +
50 +def run_cell(cell: dict, timeout: float) -> dict:
51 + r, seed, sol = cell["r"], cell["seed"], cell["solver"]
52 + free_iv, ff = cell["iv"] == "free", bool(cell["feedforward"])
53 + t0 = time.time()
54 + cnf, W1, W2, IV = build(r, free_iv, ff)
55 + # seed-dependent symmetry breaking is deliberately NOT added; the seed only changes the solver's random seed
56 + # (Kissat --seed) and is recorded for repeated runs of CaDiCaL (deterministic => identical runs).
57 + res = {"cell": cell, "timeout_s": timeout, **cnf.stats(), "encode_s": round(time.time() - t0, 3)}
58 + if sol == "cadical":
59 + out = SV.run_cadical(cnf.clauses, timeout, n_vars=cnf.nv, seed=seed)
60 + else:
61 + out = SV.run_kissat(cnf.clauses, cnf.nv, timeout, seed=seed)
62 + model = out.pop("model", None)
63 + res.update(out)
64 + if out["status"] == "sat":
65 + m1 = [C.decode_word(model, w) for w in W1]
66 + m2 = [C.decode_word(model, w) for w in W2]
67 + iv = [C.decode_word(model, w) for w in IV]
68 + b1, b2 = S.words_to_bytes(m1), S.words_to_bytes(m2)
69 + h1 = S.compress(iv, b1, r, feedforward=ff)
70 + h2 = S.compress(iv, b2, r, feedforward=ff)
71 + res.update({"message1_hex": b1.hex(), "message2_hex": b2.hex(), "iv_hex": "".join("%08x" % v for v in iv),
72 + "output_hex": "".join("%08x" % v for v in h1),
73 + "verified": (h1 == h2 and b1 != b2),
74 + "message_xor_difference_hex": (int.from_bytes(b1, "big") ^ int.from_bytes(b2, "big")).to_bytes(64, "big").hex(),
75 + "differing_words": [j for j in range(16) if m1[j] != m2[j]]})
76 + res["total_wall_s"] = round(time.time() - t0, 3)
77 + res["peak_rss_mb"] = max(peak_rss_mb(), peak_rss_mb(children=True))
78 + res["hostname"] = provenance.host_info()["hostname"]
79 + return res
80 +
81 +
82 +def aggregate(d: str, out: str, started: float):
83 + cells = [json.loads(p.read_text()) for p in sorted(pathlib.Path(d).glob("*.json"))]
84 + summary = {}
85 + for iv, ff in VARIANTS:
86 + for sol in ("cadical", "kissat"):
87 + for r in R_LIST:
88 + cs = [c for c in cells if c["cell"]["solver"] == sol and c["cell"]["r"] == r
89 + and c["cell"]["iv"] == iv and bool(c["cell"]["feedforward"]) == ff]
90 + if not cs:
91 + continue
92 + solved = [c for c in cs if c["status"] == "sat"]
93 + times = sorted(c["wall_s"] for c in cs)
94 + conf = sorted(c["conflicts"] for c in solved if c.get("conflicts") is not None)
95 + summary[f"{sol}:iv_{iv}:ff{int(ff)}:r{r}"] = {
96 + "solver": sol, "iv": iv, "feedforward": ff, "r": r, "n_runs": len(cs), "n_sat": len(solved),
97 + "n_timeout": sum(c["status"] == "timeout" for c in cs), "solved_fraction": len(solved) / len(cs),
98 + "n_unsat": sum(c["status"] == "unsat" for c in cs),
99 + "median_wall_s_unsat": sorted(c["wall_s"] for c in cs if c["status"] == "unsat")[sum(c["status"] == "unsat" for c in cs) // 2] if any(c["status"] == "unsat" for c in cs) else None,
100 + "median_wall_s": times[len(times) // 2],
101 + "median_wall_s_solved": sorted(c["wall_s"] for c in solved)[len(solved) // 2] if solved else None,
102 + "median_conflicts_solved": conf[len(conf) // 2] if conf else None,
103 + "all_verified": all(c.get("verified") for c in solved) if solved else None,
104 + "n_vars": cs[0]["n_vars"], "n_clauses": cs[0]["n_clauses"],
105 + "max_peak_rss_mb": max(c["peak_rss_mb"] for c in cs), "hosts": sorted({c["hostname"] for c in cs}),
106 + }
107 + provenance.record("sha256_collision_sat", {"r_list": R_LIST, "timeout_s": TIMEOUT, "variants": VARIANTS,
108 + "solvers": ["cadical (pysat Cadical195)", "kissat 4.0.4"]},
109 + {"summary": summary, "cells": cells}, seed="per-cell", extra={"n_cells": len(cells)},
110 + out=out, started=started)
111 + print("aggregated", len(cells), "cells ->", out)
112 +
113 +
114 +if __name__ == "__main__":
115 + ap = argparse.ArgumentParser()
116 + ap.add_argument("--list-cells", action="store_true")
117 + ap.add_argument("--seeds", default="0,1,2")
118 + ap.add_argument("--solvers", default="cadical,kissat")
119 + ap.add_argument("--cell")
120 + ap.add_argument("--timeout", type=float, default=TIMEOUT)
121 + ap.add_argument("--aggregate")
122 + ap.add_argument("--out")
123 + ap.add_argument("--seed", type=int, default=0)
124 + a = ap.parse_args()
125 + t0 = time.time()
126 + if a.list_cells:
127 + for c in list_cells([int(x) for x in a.seeds.split(",")], a.solvers.split(",")):
128 + print(json.dumps(c))
129 + elif a.cell:
130 + cell = json.loads(a.cell); cell.setdefault("seed", a.seed)
131 + res = run_cell(cell, a.timeout)
132 + if a.out:
133 + pathlib.Path(a.out).parent.mkdir(parents=True, exist_ok=True)
134 + pathlib.Path(a.out).write_text(json.dumps(res, indent=1, default=str))
135 + print(json.dumps(res, default=str)[:1500])
136 + elif a.aggregate:
137 + aggregate(a.aggregate, a.out, t0)
added experiments/reduced_round_sha256/common.py +85 −0
@@ -0,0 +1,85 @@
1 +"""Shared helpers for the reduced-round SHA-256 experiments (Axis C of WP34).
2 +
3 +Nothing here modifies the reference implementation `wp34.sha256`; it is imported as ground truth.
4 +"""
5 +from __future__ import annotations
6 +import hashlib, os, pathlib, resource, struct, sys, time
7 +
8 +ROOT = pathlib.Path(__file__).resolve().parents[2]
9 +SRC = ROOT / "src"
10 +if str(SRC) not in sys.path:
11 + sys.path.insert(0, str(SRC))
12 +
13 +from wp34 import sha256 as S # noqa: E402
14 +from wp34 import provenance # noqa: E402
15 +
16 +MASK = 0xFFFFFFFF
17 +
18 +
19 +def peak_rss_mb(children: bool = False) -> float:
20 + """Peak resident set size in MiB (macOS reports bytes, Linux kilobytes)."""
21 + who = resource.RUSAGE_CHILDREN if children else resource.RUSAGE_SELF
22 + v = resource.getrusage(who).ru_maxrss
23 + if sys.platform == "darwin":
24 + return round(v / 2**20, 1)
25 + return round(v / 2**10, 1)
26 +
27 +
28 +def random_block(rng) -> bytes:
29 + return bytes(rng.getrandbits(8) for _ in range(64))
30 +
31 +
32 +def state_to_int(state) -> int:
33 + v = 0
34 + for w in state:
35 + v = (v << 32) | (w & MASK)
36 + return v
37 +
38 +
39 +def int_to_bits(v: int, n: int) -> list[int]:
40 + """Bit list, index 0 = most significant bit of the n-bit integer (so index 0 = bit 31 of word a)."""
41 + return [(v >> (n - 1 - i)) & 1 for i in range(n)]
42 +
43 +
44 +def random_oracle(salt: bytes, msg: bytes) -> int:
45 + """256-bit 'random oracle' negative control: SHA-256 (hashlib) over a secret salt and the input."""
46 + return int.from_bytes(hashlib.sha256(salt + msg).digest(), "big")
47 +
48 +
49 +def hamming(v: int) -> int:
50 + return bin(v).count("1")
51 +
52 +
53 +def words_hex(words) -> str:
54 + return "".join("%08x" % (w & MASK) for w in words)
55 +
56 +
57 +def block_hex(block: bytes) -> str:
58 + return block.hex()
59 +
60 +
61 +def bootstrap_ci(values, rng, n_boot: int = 2000, alpha: float = 0.05):
62 + """Bootstrap percentile CI of the mean (floats are fine: statistics only)."""
63 + import statistics
64 + vals = list(values)
65 + if len(vals) < 2:
66 + return [None, None]
67 + means = []
68 + n = len(vals)
69 + for _ in range(n_boot):
70 + means.append(sum(vals[rng.randrange(n)] for _ in range(n)) / n)
71 + means.sort()
72 + lo = means[int(alpha / 2 * n_boot)]
73 + hi = means[min(n_boot - 1, int((1 - alpha / 2) * n_boot))]
74 + return [lo, hi]
75 +
76 +
77 +def mean_sd(values):
78 + vals = list(values)
79 + if not vals:
80 + return None, None
81 + m = sum(vals) / len(vals)
82 + if len(vals) < 2:
83 + return m, 0.0
84 + var = sum((x - m) ** 2 for x in vals) / (len(vals) - 1)
85 + return m, var ** 0.5
added experiments/reduced_round_sha256/diffusion.py +113 −0
@@ -0,0 +1,113 @@
1 +"""Avalanche / diffusion analysis of the SHA-256 compression function, steps r = 1..64 (no feed-forward).
2 +
3 +Research question C1 (diffusion). For a single-bit input difference we measure, after every step t = 1..64,
4 +the fraction of the 256 state bits that flip, averaged over N random messages (seeded). Four configurations:
5 + W0, W5, W15 : one random bit of message word W_j is flipped (the difference is tracked through the message
6 + schedule, so for t >= 16 several expanded words differ);
7 + IV : one random bit of the 256-bit initial state is flipped (message unchanged).
8 +Negative control: a 256-bit random oracle (hashlib SHA-256 over a secret os.urandom salt and the input) evaluated on
9 +the same input pairs — its flip ratio must be ~0.5 with sd ~ 0.5/sqrt(256) = 0.031 from the start.
10 +
11 +Outputs (JSON via wp34.provenance.record): per configuration the [64 x 256] mean-flip matrix, the per-step mean and sd
12 +of the Hamming weight of the state difference, and `full_diffusion_step` = first step with mean flip ratio >= 0.49.
13 +Only one trace per message pair is needed: the state after r steps without feed-forward is trace[r-1].
14 +
15 +Usage: PYTHONPATH=src .venv/bin/python experiments/reduced_round_sha256/diffusion.py --seed 1 --n 2000 \
16 + --out results/reduced_round_sha256/diffusion.json
17 +"""
18 +from __future__ import annotations
19 +import argparse, os, random, sys, time
20 +import numpy as np
21 +from common import S, provenance, random_block, state_to_int, int_to_bits, random_oracle, peak_rss_mb, bootstrap_ci
22 +
23 +CONFIGS = ["W0", "W5", "W15", "IV"]
24 +
25 +
26 +def flip_block_bit(block: bytes, word: int, bit: int) -> bytes:
27 + words = S.bytes_to_words(block)
28 + words[word] ^= 1 << bit
29 + return S.words_to_bytes(words)
30 +
31 +
32 +def run(seed: int, n: int, threshold: float):
33 + rng = random.Random(seed)
34 + salt = os.urandom(32) # secret salt for the random-oracle control (never saved)
35 + out = {}
36 + stats = {"compress_calls": 0}
37 + for cfg in CONFIGS:
38 + flips = np.zeros((64, 256), dtype=np.int64)
39 + hw = np.zeros((n, 64), dtype=np.int64)
40 + ro_flips = np.zeros(n, dtype=np.int64)
41 + in_pos = []
42 + for i in range(n):
43 + block = random_block(rng)
44 + state = list(S.H0)
45 + if cfg == "IV":
46 + w, b = rng.randrange(8), rng.randrange(32)
47 + state2 = list(state)
48 + state2[w] ^= 1 << b
49 + block2 = block
50 + in_pos.append(("IV", w, b))
51 + x1, x2 = S.words_to_bytes(state) + block, S.words_to_bytes(state2) + block
52 + else:
53 + j = int(cfg[1:])
54 + b = rng.randrange(32)
55 + state2 = state
56 + block2 = flip_block_bit(block, j, b)
57 + in_pos.append(("W", j, b))
58 + x1, x2 = block, block2
59 + tr1, tr2 = [], []
60 + S.compress(state, block, 64, feedforward=False, trace=tr1)
61 + S.compress(state2, block2, 64, feedforward=False, trace=tr2)
62 + stats["compress_calls"] += 2
63 + for t in range(64):
64 + d = state_to_int(tr1[t]) ^ state_to_int(tr2[t])
65 + bits = int_to_bits(d, 256)
66 + flips[t] += bits
67 + hw[i, t] = sum(bits)
68 + ro_flips[i] = bin(random_oracle(salt, x1) ^ random_oracle(salt, x2)).count("1")
69 + mean_matrix = flips / n
70 + ratio = mean_matrix.mean(axis=1) # mean flip ratio per step
71 + full = next((t + 1 for t in range(64) if ratio[t] >= threshold), None)
72 + brng = random.Random(seed + 1000)
73 + out[cfg] = {
74 + "n": n,
75 + "mean_flip_matrix": mean_matrix.round(5).tolist(),
76 + "mean_flip_ratio_by_step": ratio.round(6).tolist(),
77 + "hamming_mean_by_step": hw.mean(axis=0).round(4).tolist(),
78 + "hamming_sd_by_step": hw.std(axis=0, ddof=1).round(4).tolist(),
79 + "hamming_ci95_step64": bootstrap_ci(hw[:, 63].tolist(), brng),
80 + "full_diffusion_step": full,
81 + "threshold": threshold,
82 + "random_oracle_control": {
83 + "flip_ratio_mean": float(ro_flips.mean() / 256),
84 + "flip_ratio_sd": float(ro_flips.std(ddof=1) / 256),
85 + "hamming_mean": float(ro_flips.mean()),
86 + "hamming_sd": float(ro_flips.std(ddof=1)),
87 + },
88 + "input_bit_positions_sample": in_pos[:20],
89 + }
90 + print(f"[{cfg}] full diffusion (>= {threshold}) at step {full}; ratio by step:",
91 + " ".join("%.3f" % x for x in ratio[:24]), "...", file=sys.stderr)
92 + return out, stats
93 +
94 +
95 +def main():
96 + ap = argparse.ArgumentParser()
97 + ap.add_argument("--seed", type=int, default=1)
98 + ap.add_argument("--n", type=int, default=2000)
99 + ap.add_argument("--threshold", type=float, default=0.49)
100 + ap.add_argument("--out", default="results/reduced_round_sha256/diffusion.json")
101 + a = ap.parse_args()
102 + t0 = time.time()
103 + meas, stats = run(a.seed, a.n, a.threshold)
104 + stats["wall_s"] = round(time.time() - t0, 3)
105 + stats["peak_rss_mb"] = peak_rss_mb()
106 + provenance.record("sha256_diffusion", {"n": a.n, "steps": 64, "threshold": a.threshold, "configs": CONFIGS,
107 + "feedforward": False, "iv": "standard H0"},
108 + meas, seed=a.seed, extra={"op_stats": stats}, out=a.out, started=t0)
109 + print("wrote", a.out, "in", stats["wall_s"], "s")
110 +
111 +
112 +if __name__ == "__main__":
113 + main()
added experiments/reduced_round_sha256/local_collision.py +232 −0
@@ -0,0 +1,232 @@
1 +"""3c. The 9-step local collision of SHA-256 (Chabaud-Joux style, Gilbert-Handschuh 2003 structure), C3/C4.
2 +
3 +Linearised model
4 +----------------
5 +Replace every modular addition of the step by XOR and assume the Boolean functions Ch and Maj *absorb* every
6 +difference (their output difference is 0 whenever only one input word carries a difference; this is a bit
7 +condition, e.g. for Ch(e,f,g) with a difference in e it requires f = g on the differing bits). The difference
8 +propagation then becomes GF(2)-linear in (state difference, message-word difference):
9 + da' = dh + S1(de) + dW + S0(da) de' = dd + dh + S1(de) + dW (db',dc',dd',df',dg',dh') = (da,db,dc,de,df,dg)
10 +with S0 = Sigma0, S1 = Sigma1 (both GF(2)-linear) and "+" = XOR.
11 +
12 +Derivation of the corrections for a perturbation d in W_i (state difference zero before step i):
13 + step i : dW_i = d -> state_{i+1}: a = d, e = d
14 + step i+1 : da' = S1(d) + dW + S0(d), de' = S1(d) + dW. Both cannot vanish (S0(d) != 0); choose dW_{i+1} =
15 + S0(d) + S1(d) so that the *a*-difference dies and e receives S0(d) -> state_{i+2}: b = d, e = S0(d), f = d
16 + step i+2 : dW_{i+2} = S1(S0(d)) -> state_{i+3}: c = d, f = S0(d), g = d
17 + step i+3 : dW_{i+3} = 0 -> state_{i+4}: d = d, g = S0(d), h = d
18 + step i+4 : da' = dh + dW = d + dW, de' = dd + dh + dW = dW. Choose dW_{i+4} = d (kills a, e gets d)
19 + -> state_{i+5}: e = d, h = S0(d)
20 + step i+5 : dW_{i+5} = S0(d) + S1(d) -> state_{i+6}: f = d
21 + step i+6 : dW_{i+6} = 0 -> state_{i+7}: g = d
22 + step i+7 : dW_{i+7} = 0 -> state_{i+8}: h = d
23 + step i+8 : dW_{i+8} = d -> state_{i+9}: 0.
24 +Pattern: (d, S0(d)+S1(d), S1(S0(d)), 0, d, S0(d)+S1(d), 0, 0, d). Register differences: a only at i+1; e at
25 +i+1 (d), i+2 (S0(d)) and i+5 (d). This is the 9-step structure of Gilbert & Handschuh (SAC 2003) under the
26 +"all Boolean-function differences absorbed" assumption; the table is derived here and verified below by linear
27 +algebra over GF(2) (numpy, 256x256 and 256x32 matrices) for all 32 single-bit d (and hence, by linearity, all d).
28 +
29 +Experiments
30 +-----------
31 +(i) linear-algebra verification of the pattern (exhaustive over the 32 single-bit perturbations);
32 +(ii) with the REAL step function: probability that the XOR pattern applied to W_i..W_{i+8} gives a state
33 + collision after 9 steps, over N random (state, W) samples per bit position (numpy uint32 vectorised,
34 + cross-checked against wp34.sha256.step on a subsample); also the step-by-step survival of the intended
35 + XOR difference;
36 +(iii) SAT (CaDiCaL): find a state and words W_i..W_{i+8} conforming to the local collision with the real step
37 + function for d at every bit position (reproduces that conforming pairs exist under message conditions);
38 + verified with the reference.
39 +"""
40 +from __future__ import annotations
41 +import argparse, json, random, sys, time
42 +import numpy as np
43 +from common import S, provenance, peak_rss_mb, MASK, bootstrap_ci
44 +import cnf_sha256 as C
45 +import solvers as SV
46 +
47 +# ------------------------------------------------------------------------------------------- GF(2) linear model
48 +def word_matrix(fn) -> np.ndarray:
49 + """32x32 GF(2) matrix of a GF(2)-linear word function (column j = fn(1<<j))."""
50 + M = np.zeros((32, 32), dtype=np.uint8)
51 + for j in range(32):
52 + v = fn(1 << j)
53 + for i in range(32):
54 + M[i, j] = (v >> i) & 1
55 + return M
56 +
57 +
58 +def vec(w: int) -> np.ndarray:
59 + return np.array([(w >> i) & 1 for i in range(32)], dtype=np.uint8)
60 +
61 +
62 +def unvec(v) -> int:
63 + return int(sum(int(b) << i for i, b in enumerate(v)))
64 +
65 +
66 +def linear_step_matrices():
67 + """A (256x256) and B (256x32): dstate' = A dstate + B dW over GF(2), registers ordered a,b,c,d,e,f,g,h."""
68 + I = np.eye(32, dtype=np.uint8); Z = np.zeros((32, 32), dtype=np.uint8)
69 + S0, S1 = word_matrix(S.Sigma0), word_matrix(S.Sigma1)
70 + rows = [
71 + [S0, Z, Z, Z, S1, Z, Z, I], # a' = S0(a) + S1(e) + h (+W)
72 + [I, Z, Z, Z, Z, Z, Z, Z], # b' = a
73 + [Z, I, Z, Z, Z, Z, Z, Z],
74 + [Z, Z, I, Z, Z, Z, Z, Z],
75 + [Z, Z, Z, I, S1, Z, Z, I], # e' = d + S1(e) + h (+W)
76 + [Z, Z, Z, Z, I, Z, Z, Z],
77 + [Z, Z, Z, Z, Z, I, Z, Z],
78 + [Z, Z, Z, Z, Z, Z, I, Z],
79 + ]
80 + A = np.block(rows) % 2
81 + B = np.vstack([I, Z, Z, Z, I, Z, Z, Z])
82 + return A, B
83 +
84 +
85 +def pattern(d: int) -> list[int]:
86 + s = S.Sigma0(d) ^ S.Sigma1(d)
87 + return [d, s, S.Sigma1(S.Sigma0(d)), 0, d, s, 0, 0, d]
88 +
89 +
90 +def verify_linear(d: int):
91 + A, B = linear_step_matrices()
92 + x = np.zeros(256, dtype=np.uint8)
93 + traj = []
94 + for dw in pattern(d):
95 + x = (A @ x + B @ vec(dw)) % 2
96 + traj.append([unvec(x[32 * r:32 * (r + 1)]) for r in range(8)])
97 + return bool(not x.any()), traj
98 +
99 +
100 +# ----------------------------------------------------------------------------------- vectorised real step (numpy)
101 +def np_rotr(x, k):
102 + return ((x >> np.uint32(k)) | (x << np.uint32(32 - k))).astype(np.uint32)
103 +
104 +
105 +def np_step(st, k, w):
106 + a, b, c, d, e, f, g, h = st
107 + S1 = np_rotr(e, 6) ^ np_rotr(e, 11) ^ np_rotr(e, 25)
108 + ch = (e & f) ^ (~e & g)
109 + T1 = (h + S1 + ch + np.uint32(k) + w).astype(np.uint32)
110 + S0 = np_rotr(a, 2) ^ np_rotr(a, 13) ^ np_rotr(a, 22)
111 + mj = (a & b) ^ (a & c) ^ (b & c)
112 + T2 = (S0 + mj).astype(np.uint32)
113 + return [(T1 + T2).astype(np.uint32), a, b, c, (d + T1).astype(np.uint32), e, f, g]
114 +
115 +
116 +def empirical(d_bit: int, n: int, rng: np.random.Generator, start: int, check_ref: int = 50):
117 + d = 1 << d_bit
118 + pat = pattern(d)
119 + st = [rng.integers(0, 2**32, n, dtype=np.uint32) for _ in range(8)]
120 + Ws = [rng.integers(0, 2**32, n, dtype=np.uint32) for _ in range(9)]
121 + _, lin = verify_linear(d)
122 + s1, s2 = list(st), list(st)
123 + survival = []
124 + alive = np.ones(n, dtype=bool)
125 + for j in range(9):
126 + k = S.K[start + j]
127 + s1 = np_step(s1, k, Ws[j])
128 + s2 = np_step(s2, k, (Ws[j] ^ np.uint32(pat[j])).astype(np.uint32))
129 + diff_ok = np.ones(n, dtype=bool)
130 + for r in range(8):
131 + diff_ok &= (s1[r] ^ s2[r]) == np.uint32(lin[j][r])
132 + alive &= diff_ok
133 + survival.append(int(alive.sum()))
134 + coll = np.ones(n, dtype=bool)
135 + for r in range(8):
136 + coll &= s1[r] == s2[r]
137 + n_coll = int(coll.sum())
138 + # cross-check the vectorised step against the reference on a subsample
139 + with np.errstate(over="ignore"):
140 + pass
141 + for idx in range(min(check_ref, n)):
142 + a = tuple(int(x[idx]) for x in st)
143 + b = a
144 + for j in range(9):
145 + a = S.step(a, S.K[start + j], int(Ws[j][idx]))
146 + b = S.step(b, S.K[start + j], int(Ws[j][idx]) ^ pat[j])
147 + assert a == tuple(int(x[idx]) for x in s1) and b == tuple(int(x[idx]) for x in s2), "vectorised step mismatch"
148 + return {"bit": d_bit, "n": n, "n_collisions": n_coll, "p_collision": n_coll / n,
149 + "p_upper95_rule_of_three": 3 / n if n_coll == 0 else None,
150 + "survival_by_step": survival, "survival_fraction_by_step": [s / n for s in survival],
151 + "pattern_hex": ["%08x" % w for w in pat], "pattern_weight": sum(bin(w).count("1") for w in pat)}
152 +
153 +
154 +# ------------------------------------------------------------------------------------------------------- SAT (iii)
155 +def sat_conforming(d_bit: int, start: int, timeout: float, seed: int):
156 + d = 1 << d_bit
157 + pat = pattern(d)
158 + cnf = C.CNF()
159 + st = [cnf.word() for _ in range(8)]
160 + W1 = [cnf.word() for _ in range(9)]
161 + W2 = [[(-b if (pat[j] >> i) & 1 else b) for i, b in enumerate(W1[j])] for j in range(9)]
162 + s1, s2 = tuple(st), tuple(st)
163 + for j in range(9):
164 + s1 = cnf.step(s1, S.K[start + j], W1[j])
165 + s2 = cnf.step(s2, S.K[start + j], W2[j])
166 + for x, y in zip(s1, s2):
167 + cnf.eq_w(x, y)
168 + t0 = time.time()
169 + out = SV.run_cadical(cnf.clauses, timeout, n_vars=cnf.nv, seed=seed)
170 + model = out.pop("model", None)
171 + res = {"bit": d_bit, **cnf.stats(), **out, "seed": seed}
172 + if out["status"] == "sat":
173 + state = [C.decode_word(model, w) for w in st]
174 + w1 = [C.decode_word(model, w) for w in W1]
175 + w2 = [w ^ p for w, p in zip(w1, pat)]
176 + a, b = tuple(state), tuple(state)
177 + for j in range(9):
178 + a = S.step(a, S.K[start + j], w1[j]); b = S.step(b, S.K[start + j], w2[j])
179 + res.update({"state_hex": "".join("%08x" % v for v in state), "W_hex": ["%08x" % v for v in w1],
180 + "W_prime_hex": ["%08x" % v for v in w2], "verified": a == b and w1 != w2,
181 + "final_state_hex": "".join("%08x" % v for v in a)})
182 + return res
183 +
184 +
185 +def main():
186 + ap = argparse.ArgumentParser()
187 + ap.add_argument("--seed", type=int, default=1)
188 + ap.add_argument("--n", type=int, default=100_000)
189 + ap.add_argument("--start", type=int, default=0, help="step index i of the perturbation (uses K[i..i+8])")
190 + ap.add_argument("--sat-bits", default="all", help="comma list of bit positions for the SAT search, or 'all'")
191 + ap.add_argument("--sat-timeout", type=float, default=300)
192 + ap.add_argument("--out", default="results/reduced_round_sha256/local_collision.json")
193 + a = ap.parse_args()
194 + t0 = time.time()
195 + # (i) linear verification
196 + lin = {}
197 + for b in range(32):
198 + ok, traj = verify_linear(1 << b)
199 + lin[b] = {"cancels_after_9_steps": ok, "pattern_hex": ["%08x" % w for w in pattern(1 << b)],
200 + "register_difference_trajectory_hex": [["%08x" % v for v in s] for s in traj]}
201 + # the generic (symbolic) check: the map d -> final difference is linear, so 32 basis vectors suffice;
202 + # also check a few random multi-bit d
203 + rng = random.Random(a.seed)
204 + multi_ok = all(verify_linear(rng.getrandbits(32))[0] for _ in range(20))
205 + print("(i) linear model: all 32 single-bit perturbations cancel:", all(v["cancels_after_9_steps"] for v in lin.values()),
206 + "; 20 random multi-bit:", multi_ok, file=sys.stderr)
207 + # (ii) empirical with real step
208 + nrng = np.random.default_rng(a.seed)
209 + emp = []
210 + for b in range(32):
211 + r = empirical(b, a.n, nrng, a.start)
212 + emp.append(r)
213 + print(f"(ii) bit {b:2d}: collisions {r['n_collisions']}/{a.n} survival by step {r['survival_by_step']}", file=sys.stderr)
214 + # (iii) SAT
215 + bits = list(range(32)) if a.sat_bits == "all" else [int(x) for x in a.sat_bits.split(",")]
216 + sat = []
217 + for b in bits:
218 + r = sat_conforming(b, a.start, a.sat_timeout, a.seed)
219 + sat.append(r)
220 + print(f"(iii) bit {b:2d}: {r['status']} in {r['wall_s']} s, conflicts {r.get('conflicts')}, verified {r.get('verified')}", file=sys.stderr)
221 + meas = {"linear_model": lin, "linear_model_random_multibit_ok": multi_ok, "empirical": emp, "sat": sat,
222 + "pattern_formula": "(d, S0(d)^S1(d), S1(S0(d)), 0, d, S0(d)^S1(d), 0, 0, d)"}
223 + stats = {"wall_s": round(time.time() - t0, 3), "peak_rss_mb": peak_rss_mb(),
224 + "step_evaluations_real": 2 * 9 * a.n * 32}
225 + provenance.record("sha256_local_collision_9step", {"n_per_bit": a.n, "start_step": a.start, "sat_bits": bits,
226 + "sat_timeout_s": a.sat_timeout, "model": "GH03 structure, absorbing Boolean functions"},
227 + meas, seed=a.seed, extra={"op_stats": stats}, out=a.out, started=t0)
228 + print("wrote", a.out, stats)
229 +
230 +
231 +if __name__ == "__main__":
232 + main()
added experiments/reduced_round_sha256/make_summary.py +141 −0
@@ -0,0 +1,141 @@
1 +"""Aggregate every reduced-round SHA-256 result into results/reduced_round_sha256/summary.json (numbers the paper quotes).
2 +
3 + PYTHONPATH=src .venv/bin/python experiments/reduced_round_sha256/make_summary.py
4 +"""
5 +from __future__ import annotations
6 +import json, pathlib, statistics, sys, time
7 +from common import provenance, ROOT
8 +
9 +RES = ROOT / "results" / "reduced_round_sha256"
10 +
11 +
12 +def load(name):
13 + p = RES / name
14 + return json.loads(p.read_text()) if p.exists() else None
15 +
16 +
17 +def main():
18 + t0 = time.time()
19 + out = {}
20 + # ---- diffusion
21 + docs = [json.loads(p.read_text()) for p in sorted(RES.glob("diffusion_seed*.json"))]
22 + if docs:
23 + diff = {}
24 + for cfg in docs[0]["params"]["configs"]:
25 + fds = [d["measurements"][cfg]["full_diffusion_step"] for d in docs]
26 + ratios = [d["measurements"][cfg]["mean_flip_ratio_by_step"] for d in docs]
27 + mean_ratio = [statistics.mean(r[t] for r in ratios) for t in range(64)]
28 + diff[cfg] = {"full_diffusion_step_by_seed": fds, "full_diffusion_step": max(fds),
29 + "mean_flip_ratio_by_step": [round(x, 4) for x in mean_ratio],
30 + "flip_ratio_step64": round(mean_ratio[63], 4),
31 + "hamming_mean_step64": round(statistics.mean(d["measurements"][cfg]["hamming_mean_by_step"][63] for d in docs), 2),
32 + "hamming_sd_step64": round(statistics.mean(d["measurements"][cfg]["hamming_sd_by_step"][63] for d in docs), 2),
33 + "random_oracle_flip_ratio": round(statistics.mean(d["measurements"][cfg]["random_oracle_control"]["flip_ratio_mean"] for d in docs), 4),
34 + "random_oracle_flip_sd": round(statistics.mean(d["measurements"][cfg]["random_oracle_control"]["flip_ratio_sd"] for d in docs), 4)}
35 + out["diffusion"] = {"n_per_seed": docs[0]["params"]["n"], "seeds": [d["seed"] for d in docs], "threshold": docs[0]["params"]["threshold"],
36 + "by_config": diff, "hosts": sorted({d["host"]["hostname"] for d in docs})}
37 + # ---- bias
38 + docs = [json.loads(p.read_text()) for p in sorted(RES.glob("bias_seed*.json"))]
39 + if docs:
40 + rs = docs[0]["params"]["r_list"]; by = {}
41 + for r in rs:
42 + cs = [d["measurements"]["by_r"][str(r)] for d in docs]
43 + by[str(r)] = {"max_bit_bias_by_seed": [round(c["max_bit_bias"], 4) for c in cs],
44 + "n_bits_bias_over_4sigma_by_seed": [c["n_bits_bias_over_4sigma"] for c in cs],
45 + "max_abs_corr_by_seed": [round(c["max_abs_corr"], 4) for c in cs],
46 + "n_pairs_corr_over_4sigma_by_seed": [c["n_pairs_corr_over_4sigma"] for c in cs],
47 + "chi2_bytes_rejected_by_seed": [c["chi2_n_bytes_rejected_bonferroni"] for c in cs],
48 + "chi2_min_p_by_seed": [c["chi2_min_p"] for c in cs]}
49 + ro = [d["measurements"]["random_oracle_control"] for d in docs]
50 + by["random_oracle"] = {"max_bit_bias_by_seed": [round(c["max_bit_bias"], 4) for c in ro],
51 + "n_pairs_corr_over_4sigma_by_seed": [c["n_pairs_corr_over_4sigma"] for c in ro],
52 + "chi2_bytes_rejected_by_seed": [c["chi2_n_bytes_rejected_bonferroni"] for c in ro]}
53 + first_clean = next((r for r in rs if all(by[str(r)]["n_bits_bias_over_4sigma_by_seed"][i] == 0 and by[str(r)]["chi2_bytes_rejected_by_seed"][i] == 0 for i in range(len(docs)))), None)
54 + first_corr_clean = next((r for r in rs if all(x <= 3 for x in by[str(r)]["n_pairs_corr_over_4sigma_by_seed"])), None)
55 + out["bias"] = {"n_per_seed": docs[0]["params"]["n"], "seeds": [d["seed"] for d in docs], "r_list": rs,
56 + "bias_threshold_4sigma": docs[0]["measurements"]["by_r"]["1"]["bias_threshold_4sigma"],
57 + "corr_threshold_4sigma": docs[0]["measurements"]["by_r"]["1"]["corr_threshold_4sigma"],
58 + "expected_pairs_under_null": docs[0]["measurements"]["by_r"]["1"]["expected_pairs_under_null"],
59 + "first_r_with_no_bit_bias_and_no_chi2_rejection": first_clean,
60 + "first_r_with_correlation_count_at_noise_level": first_corr_clean, "by_r": by}
61 + # ---- preimage
62 + d = load("preimage_sat_scaling.json")
63 + if d:
64 + S = d["measurements"]["summary"]
65 + largest = {}
66 + for key, c in S.items():
67 + if c["solved_fraction"] > 0:
68 + g = c["solver"] + ("" if c["placement"] == "first" else "_spread")
69 + best = largest.get(g)
70 + if best is None or (c["r"], c["k"]) > (best["r"], best["k"]):
71 + largest[g] = {"r": c["r"], "k": c["k"], "solved_fraction": c["solved_fraction"], "median_wall_s": c["median_wall_s"]}
72 + frontier = {}
73 + for sol in ("cadical", "kissat", "z3"):
74 + for pl in ("first", "spread"):
75 + rows = {}
76 + for r in d["params"]["r_list"]:
77 + ks = [c["k"] for c in S.values() if c["solver"] == sol and c["placement"] == pl and c["r"] == r and c["solved_fraction"] >= 0.5]
78 + rows[str(r)] = max(ks) if ks else None
79 + if any(v is not None for v in rows.values()):
80 + frontier[sol + ("" if pl == "first" else "_spread")] = rows
81 + out["preimage_sat"] = {"n_cells": d["n_cells"], "timeout_s": d["params"]["timeout_s"], "hosts": sorted({h for c in S.values() for h in c["hosts"]}),
82 + "largest_solved_cell_by_solver": largest, "max_k_solved_majority_by_r": frontier,
83 + "all_sat_answers_verified": all(c["all_verified"] in (True, None) for c in S.values()),
84 + "cells": S}
85 + # ---- collision
86 + d = load("collision_sat.json")
87 + if d:
88 + S = d["measurements"]["summary"]
89 + cells = d["measurements"]["cells"]
90 + largest = {}
91 + for c in S.values():
92 + g = f'{c["solver"]}:iv_{c["iv"]}:ff{int(c["feedforward"])}'
93 + if c["solved_fraction"] > 0 and (g not in largest or c["r"] > largest[g]["r"]):
94 + largest[g] = {"r": c["r"], "solved_fraction": c["solved_fraction"], "median_wall_s_solved": c["median_wall_s_solved"]}
95 + unsat = sorted({(c["cell"]["r"], c["cell"]["iv"], c["cell"]["feedforward"], c["cell"]["solver"]) for c in cells if c["status"] == "unsat"})
96 + examples = {}
97 + for c in cells:
98 + if c["status"] == "sat" and c.get("verified"):
99 + g = f'iv_{c["cell"]["iv"]}:ff{int(c["cell"]["feedforward"])}'
100 + if g not in examples or c["cell"]["r"] > examples[g]["r"]:
101 + examples[g] = {"r": c["cell"]["r"], "solver": c["cell"]["solver"], "message1_hex": c["message1_hex"],
102 + "message2_hex": c["message2_hex"], "iv_hex": c["iv_hex"], "output_hex": c["output_hex"],
103 + "differing_words": c["differing_words"]}
104 + out["collision_sat"] = {"n_cells": d["n_cells"], "timeout_s": d["params"]["timeout_s"], "hosts": sorted({h for c in S.values() for h in c["hosts"]}),
105 + "largest_solved_r_by_variant": largest,
106 + "proven_unsat_cells (r, iv, feedforward, solver)": unsat,
107 + "all_collisions_verified": all(c["all_verified"] in (True, None) for c in S.values()),
108 + "largest_verified_collision_examples": examples, "cells": S}
109 + # ---- local collision
110 + d = load("local_collision.json")
111 + if d:
112 + m = d["measurements"]
113 + emp = m["empirical"]; sat = m["sat"]
114 + out["local_collision"] = {
115 + "n_per_bit": d["params"]["n_per_bit"], "seed": d["seed"], "host": d["host"]["hostname"],
116 + "linear_model_all_32_single_bit_cancel": all(v["cancels_after_9_steps"] for v in m["linear_model"].values()),
117 + "pattern_formula": m["pattern_formula"],
118 + "pattern_bit31_hex": m["linear_model"]["31"]["pattern_hex"], "pattern_bit0_hex": m["linear_model"]["0"]["pattern_hex"],
119 + "empirical_collisions_total": sum(e["n_collisions"] for e in emp),
120 + "empirical_p_collision_by_bit": [e["p_collision"] for e in emp],
121 + "rule_of_three_upper_bound": 3 / d["params"]["n_per_bit"],
122 + "survival_after_step_i_by_bit": [e["survival_fraction_by_step"][0] for e in emp],
123 + "survival_after_step_i+1_by_bit": [e["survival_fraction_by_step"][1] for e in emp],
124 + "survival_after_step_i+2_by_bit": [e["survival_fraction_by_step"][2] for e in emp],
125 + "sat_timeout_s": d["params"]["sat_timeout_s"],
126 + "sat_found_verified_bits": [s["bit"] for s in sat if s.get("verified") is True],
127 + "sat_timeout_bits": [s["bit"] for s in sat if s["status"] != "sat"],
128 + "sat_wall_s_by_bit": {str(s["bit"]): s["wall_s"] for s in sat},
129 + "sat_conflicts_by_bit": {str(s["bit"]): s.get("conflicts") for s in sat},
130 + }
131 + d2 = load("local_collision_bits31_0_seed2.json")
132 + if d2:
133 + out["local_collision"]["second_run_bits31_0"] = {"seed": d2["seed"], "sat": [{k: s.get(k) for k in ("bit", "status", "wall_s", "conflicts", "verified")} for s in d2["measurements"]["sat"]],
134 + "empirical_collisions_total": sum(e["n_collisions"] for e in d2["measurements"]["empirical"])}
135 + provenance.record("sha256_summary", {"sources": sorted(p.name for p in RES.glob("*.json") if p.name != "summary.json")}, out,
136 + out=RES / "summary.json", started=t0)
137 + print(json.dumps({k: {kk: vv for kk, vv in v.items() if kk not in ("cells", "by_r", "by_config")} for k, v in out.items()}, indent=1, default=str)[:6000])
138 +
139 +
140 +if __name__ == "__main__":
141 + main()
added experiments/reduced_round_sha256/preimage_sat.py +176 −0
@@ -0,0 +1,176 @@
1 +"""3a. SAT/SMT preimage scaling on r-step SHA-256 compression (standard IV, no feed-forward), question C2.
2 +
3 +Cell = (r, k, seed, solver). A seeded random 512-bit message M* is drawn; target = compress(H0, M*, r, ff=False),
4 +so a preimage exists. The k *free* bits are the first k message bits in (word, LSB-first) order: W_0 bits 0..31,
5 +then W_1, ... ; all other message bits are fixed to M* by unit clauses. Solvers: CaDiCaL 1.9.5 (pysat Cadical195),
6 +Kissat 4.0.4 (subprocess, --time), and z3 (bit-vector theory) on r <= 16, k <= 32. Every SAT answer is verified
7 +by recomputing the hash with wp34.sha256.compress.
8 +
9 +Modes:
10 + --list-cells print one JSON cell spec per line (used by cluster_driver.py)
11 + --cell '<json>' --out f run one cell in this process (fresh process => peak RSS is per cell)
12 + --aggregate DIR --out f gather per-cell JSONs into the experiment result with provenance
13 +"""
14 +from __future__ import annotations
15 +import argparse, json, os, pathlib, random, sys, time
16 +from common import S, provenance, peak_rss_mb, MASK, mean_sd
17 +import cnf_sha256 as C
18 +import solvers as SV
19 +
20 +R_LIST = [4, 8, 12, 16, 20, 24, 28, 32, 40, 48, 64]
21 +K_LIST = [16, 24, 32, 40, 48, 56, 64]
22 +TIMEOUT = 300.0
23 +
24 +
25 +def free_positions(k: int, placement: str):
26 + """'first': the first k message bits (W_0 LSB-first, then W_1, ...).
27 + 'spread': k bits spread evenly over the 16 words (bit i -> word i mod 16, position i // 16), so that no single
28 + word carries all the freedom (for r <= 16 a single free word makes the instance invertible step by step)."""
29 + if placement == "first":
30 + return [(i // 32, i % 32) for i in range(k)]
31 + if placement == "spread":
32 + return [(i % 16, i // 16) for i in range(k)]
33 + raise ValueError(placement)
34 +
35 +
36 +def instance(r: int, k: int, seed: int, placement: str = "first"):
37 + rng = random.Random(1_000_003 * seed + 1_000 * r + k)
38 + block = bytes(rng.getrandbits(8) for _ in range(64))
39 + words = S.bytes_to_words(block)
40 + target = S.compress(S.H0, block, r, feedforward=False)
41 + return block, words, target, free_positions(k, placement)
42 +
43 +
44 +def list_cells(seeds, solvers, placements=("first", "spread")):
45 + cells = []
46 + for placement in placements:
47 + for r in R_LIST:
48 + for k in K_LIST:
49 + for seed in seeds:
50 + for sol in solvers:
51 + if sol == "z3" and not (r <= 16 and k <= 32):
52 + continue
53 + if placement == "spread" and sol != "cadical":
54 + continue
55 + cells.append({"r": r, "k": k, "seed": seed, "solver": sol, "placement": placement})
56 + return cells
57 +
58 +
59 +def run_cell(cell: dict, timeout: float) -> dict:
60 + r, k, seed, sol = cell["r"], cell["k"], cell["seed"], cell["solver"]
61 + placement = cell.setdefault("placement", "first")
62 + block, words, target, free = instance(r, k, seed, placement)
63 + freeset = set(free)
64 + t0 = time.time()
65 + res = {"cell": cell, "timeout_s": timeout, "target_hex": "".join("%08x" % v for v in target),
66 + "fixed_message_hex": block.hex(), "free_bits": "%d bits, placement=%s" % (k, placement)}
67 + if sol in ("cadical", "kissat"):
68 + cnf, W, IV, OUT = C.build_compression(r, feedforward=False)
69 + for j, w in enumerate(W):
70 + for i in range(32):
71 + if (j, i) not in freeset:
72 + cnf.assert_eq(w[i], bool((words[j] >> i) & 1))
73 + for o, v in zip(OUT, target):
74 + cnf.fix_word(o, v)
75 + res.update(cnf.stats())
76 + res["encode_s"] = round(time.time() - t0, 3)
77 + if sol == "cadical":
78 + out = SV.run_cadical(cnf.clauses, timeout, n_vars=cnf.nv, seed=seed)
79 + else:
80 + out = SV.run_kissat(cnf.clauses, cnf.nv, timeout, seed=seed)
81 + model = out.pop("model", None)
82 + res.update(out)
83 + if out["status"] == "sat":
84 + found = [C.decode_word(model, w) for w in W]
85 + res["preimage_hex"] = "".join("%08x" % v for v in found)
86 + res["verified"] = S.compress(S.H0, S.words_to_bytes(found), r, feedforward=False) == target
87 + elif sol == "z3":
88 + def build(z3, s):
89 + Wv = [z3.BitVec("W%d" % j, 32) for j in range(16)]
90 + for j in range(16):
91 + for i in range(32):
92 + if (j, i) not in freeset:
93 + s.add(z3.Extract(i, i, Wv[j]) == z3.BitVecVal((words[j] >> i) & 1, 1))
94 + OUT = SV.z3_compress(z3, list(S.H0), Wv, r, False)
95 + for o, v in zip(OUT, target):
96 + s.add(o == z3.BitVecVal(v, 32))
97 + return {"W%d" % j: Wv[j] for j in range(16)}
98 + res["encode_s"] = None
99 + out = SV.run_z3(build, timeout, seed=seed)
100 + model = out.pop("model", None)
101 + res.update(out)
102 + if out["status"] == "sat":
103 + found = [model["W%d" % j] for j in range(16)]
104 + res["preimage_hex"] = "".join("%08x" % v for v in found)
105 + res["verified"] = S.compress(S.H0, S.words_to_bytes(found), r, feedforward=False) == target
106 + res["total_wall_s"] = round(time.time() - t0, 3)
107 + res["peak_rss_mb"] = max(peak_rss_mb(), peak_rss_mb(children=True))
108 + res["hostname"] = provenance.host_info()["hostname"]
109 + return res
110 +
111 +
112 +def aggregate(d: str, out: str, started: float):
113 + cells = []
114 + for p in sorted(pathlib.Path(d).glob("*.json")):
115 + cells.append(json.loads(p.read_text()))
116 + summary = {}
117 + for placement in ("first", "spread"):
118 + for sol in ("cadical", "kissat", "z3"):
119 + for r in R_LIST:
120 + for k in K_LIST:
121 + cs = [c for c in cells if c["cell"]["solver"] == sol and c["cell"]["r"] == r and c["cell"]["k"] == k
122 + and c["cell"].get("placement", "first") == placement]
123 + if not cs:
124 + continue
125 + times = sorted(c["wall_s"] for c in cs)
126 + solved = [c for c in cs if c["status"] == "sat"]
127 + conf = sorted(c["conflicts"] for c in solved if c.get("conflicts") is not None)
128 + key = f"{sol}:r{r}:k{k}" + ("" if placement == "first" else ":spread")
129 + summary[key] = {
130 + "solver": sol, "placement": placement, "r": r, "k": k, "n_runs": len(cs), "n_sat": len(solved),
131 + "n_timeout": sum(c["status"] == "timeout" for c in cs),
132 + "solved_fraction": len(solved) / len(cs),
133 + "median_wall_s": times[len(times) // 2],
134 + "median_wall_s_solved": sorted(c["wall_s"] for c in solved)[len(solved) // 2] if solved else None,
135 + "median_conflicts_solved": conf[len(conf) // 2] if conf else None,
136 + "all_verified": all(c.get("verified") for c in solved) if solved else None,
137 + "n_vars": cs[0].get("n_vars"), "n_clauses": cs[0].get("n_clauses"),
138 + "max_peak_rss_mb": max(c["peak_rss_mb"] for c in cs),
139 + "hosts": sorted({c["hostname"] for c in cs}),
140 + }
141 + provenance.record("sha256_preimage_sat_scaling",
142 + {"r_list": R_LIST, "k_list": K_LIST, "timeout_s": TIMEOUT, "feedforward": False,
143 + "iv": "standard H0", "free_bits": {"first": "first k message bits, W_0 LSB-first", "spread": "bit i -> word i mod 16, position i//16 (cadical only)"},
144 + "solvers": {"cadical": "pysat Cadical195 (CaDiCaL 1.9.5)", "kissat": "kissat 4.0.4 binary",
145 + "z3": "z3 bit-vector, r<=16 & k<=32"}},
146 + {"summary": summary, "cells": cells}, seed="per-cell (see cells)",
147 + extra={"n_cells": len(cells)}, out=out, started=started)
148 + print("aggregated", len(cells), "cells ->", out)
149 +
150 +
151 +if __name__ == "__main__":
152 + ap = argparse.ArgumentParser()
153 + ap.add_argument("--list-cells", action="store_true")
154 + ap.add_argument("--seeds", default="0,1,2")
155 + ap.add_argument("--solvers", default="cadical,kissat,z3")
156 + ap.add_argument("--placements", default="first,spread")
157 + ap.add_argument("--cell")
158 + ap.add_argument("--timeout", type=float, default=TIMEOUT)
159 + ap.add_argument("--aggregate")
160 + ap.add_argument("--out")
161 + ap.add_argument("--seed", type=int, default=0, help="(single-cell convenience) seed when --cell omits it")
162 + a = ap.parse_args()
163 + t0 = time.time()
164 + if a.list_cells:
165 + for c in list_cells([int(x) for x in a.seeds.split(",")], a.solvers.split(","), a.placements.split(",")):
166 + print(json.dumps(c))
167 + elif a.cell:
168 + cell = json.loads(a.cell)
169 + cell.setdefault("seed", a.seed)
170 + res = run_cell(cell, a.timeout)
171 + if a.out:
172 + pathlib.Path(a.out).parent.mkdir(parents=True, exist_ok=True)
173 + pathlib.Path(a.out).write_text(json.dumps(res, indent=1, default=str))
174 + print(json.dumps({k: v for k, v in res.items() if k not in ("fixed_message_hex",)}, default=str))
175 + elif a.aggregate:
176 + aggregate(a.aggregate, a.out, t0)
added experiments/reduced_round_sha256/run_cluster.sh +47 −0
@@ -0,0 +1,47 @@
1 +#!/bin/bash
2 +# Run the SAT scaling grids (3a preimage, 3b collision) on cluster node M3U96b and bring the results back.
3 +# ./run_cluster.sh setup # venv (uv, python 3.12) + copy kissat/cadical binaries + smoke test
4 +# ./run_cluster.sh launch # rsync src/ + experiments/reduced_round_sha256/ and start both drivers (nohup)
5 +# ./run_cluster.sh status # tail the driver logs
6 +# ./run_cluster.sh fetch # rsync results back into results/reduced_round_sha256/
7 +# Workers: 14 (preimage) + 10 (collision) = 24 concurrent single-thread solver processes. Deadline: no new cell
8 +# after DEADLINE seconds (cells are ordered seed-0-first then by cost, so the largest cells lose seeds first).
9 +set -euo pipefail
10 +NODE=${NODE:-M3U96b}
11 +REMOTE=~/wp34
12 +HERE="$(cd "$(dirname "$0")" && pwd)"
13 +ROOT="$(cd "$HERE/../.." && pwd)"
14 +DEADLINE=${DEADLINE:-7200}
15 +PY='~/wp34/venv/bin/python'
16 +case "${1:-}" in
17 + setup)
18 + ssh $NODE "mkdir -p $REMOTE/bin $REMOTE/results && cd $REMOTE && uv venv venv --python 3.12 -q && uv pip install --python venv/bin/python -q python-sat numpy z3-solver scipy"
19 + scp -q /opt/homebrew/bin/kissat /opt/homebrew/bin/cadical $NODE:$REMOTE/bin/
20 + ssh $NODE "$REMOTE/bin/kissat --version && $REMOTE/bin/cadical --version && $PY -c 'import pysat, z3, numpy; print(\"venv ok\")'"
21 + ;;
22 + launch)
23 + rsync -az --delete --exclude '__pycache__' "$ROOT/src/" $NODE:$REMOTE/src/
24 + rsync -az --delete --exclude '__pycache__' "$HERE/" $NODE:$REMOTE/experiments/reduced_round_sha256/
25 + ssh $NODE "cd $REMOTE && PYTHONPATH=$REMOTE/src $PY experiments/reduced_round_sha256/cnf_sha256.py --selftest | tail -2"
26 + ssh $NODE "cd $REMOTE && export PYTHONPATH=$REMOTE/src KISSAT_BIN=$REMOTE/bin/kissat; \
27 + nohup $PY experiments/reduced_round_sha256/cluster_driver.py --script preimage_sat.py \
28 + --cells-dir results/cells_preimage --out results/preimage_sat_scaling.json --workers ${W_PRE:-14} --deadline $DEADLINE \
29 + > results/driver_preimage.log 2>&1 & \
30 + nohup $PY experiments/reduced_round_sha256/cluster_driver.py --script collision_sat.py \
31 + --cells-dir results/cells_collision --out results/collision_sat.json --workers ${W_COL:-10} --deadline $DEADLINE \
32 + > results/driver_collision.log 2>&1 & \
33 + sleep 2; echo launched; pgrep -fl cluster_driver | head"
34 + ;;
35 + status)
36 + ssh $NODE "cd $REMOTE/results && tail -n 4 driver_preimage.log driver_collision.log; echo; ls cells_preimage | wc -l; ls cells_collision | wc -l; uptime"
37 + ;;
38 + fetch)
39 + mkdir -p "$ROOT/results/reduced_round_sha256"
40 + rsync -az $NODE:$REMOTE/results/ "$ROOT/results/reduced_round_sha256/m3u96b/"
41 + for f in preimage_sat_scaling.json collision_sat.json; do
42 + [ -f "$ROOT/results/reduced_round_sha256/m3u96b/$f" ] && cp "$ROOT/results/reduced_round_sha256/m3u96b/$f" "$ROOT/results/reduced_round_sha256/$f"
43 + done
44 + ls -la "$ROOT/results/reduced_round_sha256/"
45 + ;;
46 + *) echo "usage: $0 setup|launch|status|fetch"; exit 1;;
47 +esac
added experiments/reduced_round_sha256/solvers.py +177 −0
@@ -0,0 +1,177 @@
1 +"""Uniform solver front-ends: CaDiCaL (pysat Cadical195, in-process), Kissat (subprocess), z3 (bit-vector SMT).
2 +
3 +Each runner returns a dict with: status in {sat, unsat, timeout, error}, wall_s, conflicts, decisions, propagations
4 +(when available), solver-reported extras, and `model` = set of true literals (SAT) / dict of word values (z3).
5 +Peak RSS is measured by the caller (one fresh process per cell; see cluster_driver.py).
6 +"""
7 +from __future__ import annotations
8 +import os, re, shutil, subprocess, sys, tempfile, threading, time
9 +from common import S, MASK
10 +
11 +KISSAT_CANDIDATES = [os.environ.get("KISSAT_BIN", ""), os.path.expanduser("~/wp34/bin/kissat"), "/opt/homebrew/bin/kissat"]
12 +CADICAL_CANDIDATES = [os.environ.get("CADICAL_BIN", ""), os.path.expanduser("~/wp34/bin/cadical"), "/opt/homebrew/bin/cadical"]
13 +
14 +
15 +def _find(cands, name):
16 + for c in cands:
17 + if c and os.path.exists(c):
18 + return c
19 + return shutil.which(name)
20 +
21 +
22 +def kissat_bin() -> str | None:
23 + return _find(KISSAT_CANDIDATES, "kissat")
24 +
25 +
26 +def cadical_bin() -> str | None:
27 + return _find(CADICAL_CANDIDATES, "cadical")
28 +
29 +
30 +def _write_dimacs(clauses, n_vars):
31 + with tempfile.NamedTemporaryFile("w", suffix=".cnf", delete=False) as f:
32 + f.write("p cnf %d %d\n" % (n_vars, len(clauses)))
33 + for c in clauses:
34 + f.write(" ".join(map(str, c)) + " 0\n")
35 + return f.name
36 +
37 +
38 +_STAT_RE = re.compile(r"^c (conflicts|decisions|propagations|restarts|maximum-resident-set-size|process-time|maximum resident set size of process|total real time since initialization):\s+([0-9.]+)")
39 +_STAT_KEYS = {"maximum-resident-set-size": "kissat_rss_bytes", "process-time": "solver_process_s",
40 + "maximum resident set size of process": "cadical_rss_mb", "total real time since initialization": "solver_real_s"}
41 +
42 +
43 +def _run_binary(cmd, timeout):
44 + """Run a DIMACS solver binary; returns (status, model, stats, wall)."""
45 + t0 = time.time()
46 + try:
47 + p = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout + 60)
48 + txt = p.stdout
49 + except subprocess.TimeoutExpired as e:
50 + txt = e.stdout if isinstance(e.stdout, str) else (e.stdout or b"").decode(errors="replace")
51 + txt += "\ns UNKNOWN\n"
52 + wall = time.time() - t0
53 + status, model, stats = "timeout", set(), {}
54 + for line in txt.splitlines():
55 + if line.startswith("s "):
56 + status = {"s SATISFIABLE": "sat", "s UNSATISFIABLE": "unsat"}.get(line.strip(), "timeout")
57 + elif line.startswith("v "):
58 + model.update(int(x) for x in line[2:].split() if int(x) > 0)
59 + else:
60 + m = _STAT_RE.match(line)
61 + if m:
62 + key = _STAT_KEYS.get(m.group(1), m.group(1))
63 + val = float(m.group(2))
64 + stats[key] = int(val) if val.is_integer() and key not in ("cadical_rss_mb",) else val
65 + return status, model, stats, wall
66 +
67 +
68 +def run_cadical(clauses, timeout: float, n_vars: int | None = None, seed: int = 0) -> dict:
69 + """CaDiCaL as the standalone binary (`cadical -t <s> --seed=<seed>`): pysat's in-process `interrupt()` does
70 + not stop CaDiCaL 1.9.5 (tested: a 5 s limit ran for minutes), so wall-clock limits are enforced by the
71 + binary's real-time limit and a subprocess kill guard."""
72 + binp = cadical_bin()
73 + if binp is None:
74 + return {"status": "error", "error": "cadical binary not found", "wall_s": 0.0}
75 + if n_vars is None:
76 + n_vars = max(abs(l) for c in clauses for l in c)
77 + path = _write_dimacs(clauses, n_vars)
78 + try:
79 + status, model, st, wall = _run_binary([binp, "-t", str(int(timeout)), "--seed=%d" % seed, path], timeout)
80 + finally:
81 + os.unlink(path)
82 + # the CaDiCaL binary omits a statistics line when its value is 0 (e.g. an instance solved by propagation)
83 + dflt = 0 if status in ("sat", "unsat") else None
84 + out = {"status": status, "wall_s": round(wall, 3), "conflicts": st.get("conflicts", dflt), "decisions": st.get("decisions", dflt),
85 + "propagations": st.get("propagations", dflt), "restarts": st.get("restarts", dflt), "model": model if status == "sat" else None}
86 + out.update({k: v for k, v in st.items() if k.startswith("cadical_") or k.startswith("solver_")})
87 + return out
88 +
89 +
90 +def run_cadical_pysat(clauses, timeout: float) -> dict:
91 + """In-process pysat Cadical195 (used only where the instance is known to be fast; no reliable timeout)."""
92 + from pysat.solvers import Cadical195
93 + t0 = time.time()
94 + with Cadical195(bootstrap_with=clauses) as s:
95 + timer = threading.Timer(timeout, s.interrupt)
96 + timer.start()
97 + try:
98 + res = s.solve_limited(expect_interrupt=True)
99 + finally:
100 + timer.cancel()
101 + wall = time.time() - t0
102 + st = s.accum_stats() or {}
103 + out = {"status": "sat" if res is True else ("unsat" if res is False else "timeout"),
104 + "wall_s": round(wall, 3), "conflicts": st.get("conflicts"), "decisions": st.get("decisions"),
105 + "propagations": st.get("propagations"), "restarts": st.get("restarts"), "model": None}
106 + if res is True:
107 + out["model"] = {l for l in s.get_model() if l > 0}
108 + return out
109 +
110 +
111 +def run_kissat(clauses, n_vars: int, timeout: float, seed: int = 0) -> dict:
112 + binp = kissat_bin()
113 + if binp is None:
114 + return {"status": "error", "error": "kissat binary not found", "wall_s": 0.0}
115 + path = _write_dimacs(clauses, n_vars)
116 + try:
117 + status, model, st, wall = _run_binary([binp, "--time=%d" % int(timeout), "--seed=%d" % seed, "-s", path], timeout)
118 + finally:
119 + os.unlink(path)
120 + out = {"status": status, "wall_s": round(wall, 3), "conflicts": st.get("conflicts"), "decisions": st.get("decisions"),
121 + "propagations": st.get("propagations"), "restarts": st.get("restarts"), "model": model if status == "sat" else None}
122 + out.update({k: v for k, v in st.items() if k.startswith("kissat_") or k.startswith("solver_")})
123 + return out
124 +
125 +
126 +# ---------------------------------------------------------------------------------------------------------- z3 ---
127 +def z3_compress(z3, IV, W16, rounds: int, feedforward: bool):
128 + """Bit-vector model of the reference compression (W16/IV are z3 BitVec(32) or python ints)."""
129 + def rotr(x, k): return z3.RotateRight(x, k)
130 + def S0(x): return rotr(x, 2) ^ rotr(x, 13) ^ rotr(x, 22)
131 + def S1(x): return rotr(x, 6) ^ rotr(x, 11) ^ rotr(x, 25)
132 + def s0(x): return rotr(x, 7) ^ rotr(x, 18) ^ z3.LShR(x, 3)
133 + def s1(x): return rotr(x, 17) ^ rotr(x, 19) ^ z3.LShR(x, 10)
134 + bv = lambda v: z3.BitVecVal(v, 32) if isinstance(v, int) else v
135 + W = [bv(w) for w in W16]
136 + for t in range(16, rounds):
137 + W.append(s1(W[t - 2]) + W[t - 7] + s0(W[t - 15]) + W[t - 16])
138 + a, b, c, d, e, f, g, h = [bv(x) for x in IV]
139 + for t in range(rounds):
140 + T1 = h + S1(e) + ((e & f) ^ (~e & g)) + z3.BitVecVal(S.K[t], 32) + W[t]
141 + T2 = S0(a) + ((a & b) ^ (a & c) ^ (b & c))
142 + a, b, c, d, e, f, g, h = T1 + T2, a, b, c, d + T1, e, f, g
143 + out = [a, b, c, d, e, f, g, h]
144 + if feedforward:
145 + out = [x + bv(y) for x, y in zip(out, IV)]
146 + return out
147 +
148 +
149 +def run_z3(build, timeout: float, seed: int = 0) -> dict:
150 + """build(z3, solver) -> dict name -> BitVec to read back on SAT."""
151 + import z3
152 + z3.set_param("sat.random_seed", seed)
153 + z3.set_param("smt.random_seed", seed)
154 + s = z3.Solver()
155 + s.set("timeout", int(timeout * 1000))
156 + names = build(z3, s)
157 + t0 = time.time()
158 + r = s.check()
159 + wall = time.time() - t0
160 + st = {}
161 + try:
162 + stats = s.statistics()
163 + for k in stats.keys():
164 + try:
165 + st[k] = stats.get_key_value(k)
166 + except Exception:
167 + pass
168 + except Exception:
169 + pass
170 + status = "sat" if r == z3.sat else ("unsat" if r == z3.unsat else "timeout")
171 + out = {"status": status, "wall_s": round(wall, 3),
172 + "conflicts": st.get("conflicts", st.get("sat conflicts")), "decisions": st.get("decisions", st.get("sat decisions")),
173 + "propagations": st.get("propagations", st.get("sat propagations")), "z3_stats": st, "model": None}
174 + if r == z3.sat:
175 + m = s.model()
176 + out["model"] = {k: m.eval(v, model_completion=True).as_long() for k, v in names.items()}
177 + return out
added experiments/signature_benchmarks/README.md +79 −0
@@ -0,0 +1,79 @@
1 +# experiments/signature_benchmarks — Research Axis B (ECDSA / BIP340 Schnorr)
2 +
3 +Benchmarks of legitimate secp256k1 signature implementations on synthetic keys (mandate §5, §9;
4 +research questions B2 and B5). Nothing here touches real keys: every secret key, message, aux-rand
5 +value and batch weight is drawn from a ChaCha20 RNG seeded with `--seed` (default 34).
6 +
7 +## Layout
8 +
9 +| Path | Role |
10 +|---|---|
11 +| `sigbench/` | Rust crate (binary). Dependencies: `secp256k1` 0.33 (rust-secp256k1 → vendored libsecp256k1 C, features `rand, global-context, std`), `k256` 0.14 (RustCrypto, features `ecdsa, schnorr, arithmetic, precomputed-tables, std, alloc`), `sha2`, `rand`/`rand_chacha` 0.9, `serde(_json)`, `clap`. Release profile: opt-level 3, fat LTO, 1 codegen unit. |
12 +| `sigbench/src/msm.rs` | Own multi-scalar multiplication on `k256::{AffinePoint, ProjectivePoint, Scalar}`: **Straus** (interleaved width-5 wNAF, per-term odd-multiple tables) and **Pippenger** (bucket method, c-bit windows, mixed additions, running-sum trick), both instrumented with counters (projective adds, mixed adds, doublings, identity adds). Also `lift_x`, the BIP340 challenge hash and `batch_verify`. |
13 +| `sigbench/src/vectors.rs` | Parser + checker for `data/vectors/bip340_test_vectors.csv` against both libraries (sign where a secret key is given, verify everywhere, compare with the expected column). |
14 +| `sigbench/src/stats.rs` | Median / quartiles / IQR / mean / sd and a bootstrap (percentile) 95 % CI for a ratio of medians. |
15 +| `run_bench.py` | Thin wrapper: `cargo build --release`, run the binary, run `python_crosscheck.py` on the dumped triples, run `python_baselines.py`, add the `wp34.provenance.record()` block plus rustc/cargo/crate versions (`cargo tree`), write `results/signature_benchmarks/bench_<hostname>.json`. Standard library only (runs with a bare `python3` on cluster nodes). |
16 +| `python_crosscheck.py` | Verifies Rust-produced (seckey, msg, sig) triples with the exact-arithmetic references `src/wp34/ecdsa.py` and `src/wp34/schnorr.py`, re-signs in Python and compares bytes, and rejects a tampered signature. Used by `tests/test_sig_crosscheck.py`. |
17 +| `python_baselines.py` | Pure-Python `src/wp34`, pure-Python `ecdsa` package, and `coincurve` (CFFI → libsecp256k1) timings, 20 ops × 7 reps. |
18 +| `summarize.py` | Collects every `bench_*.json` into `results/signature_benchmarks/summary.json` and prints a Markdown digest. |
19 +| `../../figures/make_sig_figures.py` | Regenerates `figures/sig_batch_verification.pdf` and `figures/sig_implementations.pdf` from the JSON files. |
20 +
21 +## Running
22 +
23 +```bash
24 +# laptop (repository root)
25 +PYTHONPATH=src .venv/bin/python experiments/signature_benchmarks/run_bench.py --seed 34 --label "Laptop (Apple M5 Max)"
26 +
27 +# cluster node (crate + src/wp34 + data/vectors rsync'ed to ~/wp34-sigbench, cargo in PATH)
28 +ssh M3U96a 'cd ~/wp34-sigbench && PYTHONPATH=src python3 experiments/signature_benchmarks/run_bench.py --seed 34 --label "M3U96a (Apple M3 Ultra)"'
29 +rsync -a M3U96a:~/wp34-sigbench/results/signature_benchmarks/ results/signature_benchmarks/
30 +
31 +.venv/bin/python experiments/signature_benchmarks/summarize.py
32 +.venv/bin/python figures/make_sig_figures.py
33 +.venv/bin/python -m pytest tests/test_sig_crosscheck.py -q
34 +```
35 +
36 +Binary options (`sigbench --help`): `--seed`, `--reps` (7), `--batch-ops` (2000 operations per repetition),
37 +`--batch-min-sigs` (≥ 2000 signatures per repetition in the batch benchmark), `--batch-sizes`,
38 +`--crosscheck-n` (1000), `--python-crosscheck-n` (200), `--bootstrap` (2000 resamples), `--pippenger-c`
39 +(0 = size heuristic), `--ct-lincomb-max-batch` (256). Everything runs on the main thread; macOS has no
40 +hard CPU pinning from user space, so "pinned single thread" means single-threaded with no parallelism.
41 +
42 +## What is measured
43 +
44 +1. **Correctness gates** (all recorded in the JSON under `measurements.correctness_gates`):
45 + (a) 19/19 BIP340 vectors with both libraries (verification column + byte-identical signing for the
46 + 8 vectors with a secret key); (b) 1000 cross-library checks in each direction for ECDSA
47 + (RFC 6979, SHA-256 prehash, low-S) and Schnorr (with aux_rand) — signatures are also byte-identical
48 + across libraries because both nonce derivations are deterministic; (c) 200 triples verified and
49 + re-signed by the pure-Python references (`python_reference_crosscheck`); (d) own wNAF scalar
50 + multiplication agrees with k256 on 64 cases; (e) batch negative controls (one signature with
51 + s+1, a flipped message bit, or r+1 inside a batch of 64) rejected by all four MSM back-ends.
52 +2. **Sign / verify** ns per op (median, IQR over 7 reps × 2000 ops): ECDSA and Schnorr, libsecp256k1 and k256.
53 +3. **Batch verification** Σ a_i s_i G = Σ a_i R_i + Σ a_i e_i P_i (a_1 = 1, a_i random 128-bit), merged
54 + into one MSM with 2b + 1 terms (points negated instead of scalars so the weights stay 128-bit),
55 + b ∈ {1, …, 2048}, with four back-ends: own Straus, own Pippenger, k256 `lincomb_vartime`
56 + (GLV + wNAF), k256 `lincomb` (constant time, up to b = 256). Reported per signature: wall-clock,
57 + MSM-only time, operation counts, and speed-up S(b) relative to single verification (k256
58 + `verify_raw` and libsecp256k1) with bootstrap 95 % CI.
59 +4. **Scalar multiplication**: k256 constant-time (GLV, generic point and generator), k256
60 + `mul_by_generator` (precomputed table), k256 vartime (GLV + wNAF), own wNAF-5 double-and-add
61 + without GLV (with op counts), libsecp256k1 `ecmult_gen` (`PublicKey::from_secret_key`) and
62 + `ecmult_const` (`PublicKey::mul_tweak`). GLV cannot be toggled from either library's public API
63 + (see `scalar_mult.glv_note` in the JSON); the only non-GLV data point is our own implementation.
64 +5. **Python baselines** for the implementation spread (pure Python ≈ milliseconds, CFFI ≈ C).
65 +
66 +## Headline numbers (seed 34, 2026-11-01)
67 +
68 +| | Laptop, Apple M5 Max | M3U96a, Apple M3 Ultra |
69 +|---|---|---|
70 +| ECDSA sign / verify, libsecp256k1 | 23.6 / 12.6 µs | 29.2 / 16.7 µs |
71 +| ECDSA sign / verify, k256 | 15.3 / 25.0 µs | 18.8 / 33.5 µs |
72 +| Schnorr sign / verify, libsecp256k1 | 20.5 / 13.2 µs | 24.7 / 16.9 µs |
73 +| Schnorr sign / verify, k256 | 11.6 / 24.5 µs | 14.8 / 32.5 µs |
74 +| Batch verify, own Pippenger, b = 2048 | 7.9 µs/sig, S = 3.10 [3.05, 3.16] vs k256, 1.67 vs libsecp256k1 | 10.7 µs/sig, S = 3.03 [3.00, 3.04] vs k256, 1.58 vs libsecp256k1 |
75 +| Batch verify, own Straus (plateau b ≥ 32) | ≈ 12.1–12.7 µs/sig, S ≈ 2.0 | ≈ 16.1 µs/sig, S ≈ 2.0 |
76 +| Pure Python `src/wp34` Schnorr verify | 7.4 ms | 9.2 ms |
77 +
78 +Full digest: `results/signature_benchmarks/summary.json`; interpretation and classification:
79 +`research_log/2026-11-01-signatures.md`.
added experiments/signature_benchmarks/python_baselines.py +119 −0
@@ -0,0 +1,119 @@
1 +"""Python baselines for the implementation-choice spread (WP34 Axis B, research question B5):
2 +pure-Python `src/wp34` (exact integer arithmetic, no tables), the pure-Python `ecdsa` package,
3 +and `coincurve` (CFFI bindings to libsecp256k1). Small samples (default 20 ops x 7 reps),
4 +median / IQR reported in ns per op. Synthetic keys only (random.Random(seed)).
5 +
6 +Usage: PYTHONPATH=src python experiments/signature_benchmarks/python_baselines.py [--seed 34] [--ops 20] [--reps 7]
7 +Prints a JSON document on stdout. Libraries that are not importable are reported as null.
8 +"""
9 +from __future__ import annotations
10 +import argparse, hashlib, json, random, statistics, sys, time
11 +from pathlib import Path
12 +
13 +ROOT = Path(__file__).resolve().parents[2]
14 +sys.path.insert(0, str(ROOT / "src"))
15 +
16 +
17 +def quantile(sorted_xs, q):
18 + pos = q * (len(sorted_xs) - 1)
19 + lo, hi = int(pos // 1), min(int(pos // 1) + 1, len(sorted_xs) - 1)
20 + return sorted_xs[lo] + (sorted_xs[hi] - sorted_xs[lo]) * (pos - lo)
21 +
22 +
23 +def timing(reps, ops, fn):
24 + fn() # warm-up
25 + samples = []
26 + for _ in range(reps):
27 + t = time.perf_counter_ns()
28 + fn()
29 + samples.append((time.perf_counter_ns() - t) / ops)
30 + v = sorted(samples)
31 + med = quantile(v, 0.5)
32 + return {
33 + "reps": reps, "ops_per_rep": ops, "ns_per_op_samples": samples,
34 + "median_ns": med, "q1_ns": quantile(v, 0.25), "q3_ns": quantile(v, 0.75),
35 + "iqr_ns": quantile(v, 0.75) - quantile(v, 0.25), "min_ns": v[0],
36 + "mean_ns": statistics.fmean(v), "sd_ns": statistics.stdev(v) if len(v) > 1 else 0.0,
37 + "ops_per_s": 1e9 / med,
38 + }
39 +
40 +
41 +def main():
42 + ap = argparse.ArgumentParser()
43 + ap.add_argument("--seed", type=int, default=34)
44 + ap.add_argument("--ops", type=int, default=20)
45 + ap.add_argument("--reps", type=int, default=7)
46 + ap.add_argument("--batch", type=int, default=16, help="batch size for the pure-Python batch verification")
47 + a = ap.parse_args()
48 + rng = random.Random(a.seed)
49 + from wp34 import secp256k1 as k1
50 + n_ord = k1.n
51 + keys = [rng.randrange(1, n_ord) for _ in range(a.ops)]
52 + msgs = [bytes(rng.getrandbits(8) for _ in range(32)) for _ in range(a.ops)]
53 + auxs = [bytes(rng.getrandbits(8) for _ in range(32)) for _ in range(a.ops)]
54 + out = {"seed": a.seed, "ops": a.ops, "reps": a.reps, "python": sys.version.split()[0], "results": {}, "versions": {}}
55 + R = out["results"]
56 +
57 + # ---- pure Python src/wp34 (exact arithmetic, affine double-and-add, no precomputation)
58 + from wp34 import ecdsa as D, schnorr as B
59 + Qs = [D.pubkey(d) for d in keys]
60 + sigs = [D.sign(d, m) for d, m in zip(keys, msgs)]
61 + R["wp34_pure_python_ecdsa_sign"] = timing(a.reps, a.ops, lambda: [D.sign(d, m) for d, m in zip(keys, msgs)])
62 + R["wp34_pure_python_ecdsa_verify"] = timing(a.reps, a.ops, lambda: [D.verify(Q, m, s) for Q, m, s in zip(Qs, msgs, sigs)])
63 + assert all(D.verify(Q, m, s) for Q, m, s in zip(Qs, msgs, sigs))
64 + R["wp34_pure_python_ecdsa_verify_shamir"] = timing(a.reps, a.ops, lambda: [D.verify_shamir(Q, m, s) for Q, m, s in zip(Qs, msgs, sigs)])
65 + skb = [d.to_bytes(32, "big") for d in keys]
66 + pks = [B.pubkey_gen(s) for s in skb]
67 + ssigs = [B.sign(m, s, x) for m, s, x in zip(msgs, skb, auxs)]
68 + R["wp34_pure_python_schnorr_sign"] = timing(a.reps, a.ops, lambda: [B.sign(m, s, x) for m, s, x in zip(msgs, skb, auxs)])
69 + R["wp34_pure_python_schnorr_verify"] = timing(a.reps, a.ops, lambda: [B.verify(m, p, s) for m, p, s in zip(msgs, pks, ssigs)])
70 + assert all(B.verify(m, p, s) for m, p, s in zip(msgs, pks, ssigs))
71 + b = min(a.batch, a.ops)
72 + items = list(zip(msgs[:b], pks[:b], ssigs[:b]))
73 + brng = random.Random(a.seed + 1)
74 + R["wp34_pure_python_schnorr_batch_verify_per_sig"] = timing(a.reps, b, lambda: B.batch_verify(items, brng))
75 + R["wp34_pure_python_schnorr_batch_verify_per_sig"]["batch_size"] = b
76 + assert B.batch_verify(items, brng)
77 + bad = list(items)
78 + bad[1] = (bad[1][0], bad[1][1], bad[1][2][:63] + bytes([bad[1][2][63] ^ 1]))
79 + out["pure_python_batch_negative_control_rejected"] = not B.batch_verify(bad, brng)
80 +
81 + # ---- python-ecdsa package (pure Python too, but with its own arithmetic)
82 + try:
83 + import ecdsa as E
84 + out["versions"]["ecdsa"] = E.__version__
85 + sk = [E.SigningKey.from_string(s, curve=E.SECP256k1, hashfunc=hashlib.sha256) for s in skb]
86 + vk = [k.get_verifying_key() for k in sk]
87 + es = [k.sign_digest_deterministic(m, hashfunc=hashlib.sha256) for k, m in zip(sk, msgs)]
88 + R["python_ecdsa_pkg_ecdsa_sign"] = timing(a.reps, a.ops, lambda: [k.sign_digest_deterministic(m, hashfunc=hashlib.sha256) for k, m in zip(sk, msgs)])
89 + R["python_ecdsa_pkg_ecdsa_verify"] = timing(a.reps, a.ops, lambda: [v.verify_digest(s, m) for v, s, m in zip(vk, es, msgs)])
90 + except Exception as e: # pragma: no cover
91 + out["versions"]["ecdsa"] = None
92 + out["python_ecdsa_pkg_error"] = repr(e)
93 +
94 + # ---- coincurve (libsecp256k1 through CFFI)
95 + try:
96 + import coincurve as C
97 + out["versions"]["coincurve"] = C.__version__
98 + ck = [C.PrivateKey(s) for s in skb]
99 + cpk = [k.public_key for k in ck]
100 + cs = [k.sign(m, hasher=None) for k, m in zip(ck, msgs)] # DER, low-S, RFC 6979
101 + R["coincurve_ecdsa_sign_der"] = timing(a.reps, a.ops, lambda: [k.sign(m, hasher=None) for k, m in zip(ck, msgs)])
102 + R["coincurve_ecdsa_verify_der"] = timing(a.reps, a.ops, lambda: [p.verify(s, m, hasher=None) for p, s, m in zip(cpk, cs, msgs)])
103 + assert all(p.verify(s, m, hasher=None) for p, s, m in zip(cpk, cs, msgs))
104 + xo = [C.PublicKeyXOnly.from_secret(s) for s in skb]
105 + css = [k.sign_schnorr(m, x) for k, m, x in zip(ck, msgs, auxs)]
106 + R["coincurve_schnorr_sign"] = timing(a.reps, a.ops, lambda: [k.sign_schnorr(m, x) for k, m, x in zip(ck, msgs, auxs)])
107 + R["coincurve_schnorr_verify"] = timing(a.reps, a.ops, lambda: [p.verify(s, m) for p, s, m in zip(xo, css, msgs)])
108 + assert all(p.verify(s, m) for p, s, m in zip(xo, css, msgs))
109 + # agreement with the pure-Python reference (bytes must be identical: same aux_rand)
110 + out["coincurve_schnorr_identical_to_wp34"] = sum(x == y for x, y in zip(css, ssigs))
111 + except Exception as e: # pragma: no cover
112 + out["versions"]["coincurve"] = None
113 + out["coincurve_error"] = repr(e)
114 +
115 + print(json.dumps(out))
116 +
117 +
118 +if __name__ == "__main__":
119 + main()
added experiments/signature_benchmarks/python_crosscheck.py +72 −0
@@ -0,0 +1,72 @@
1 +"""Cross-check Rust-produced (seckey, msg, sig) triples against the pure-Python references
2 +`wp34.ecdsa` and `wp34.schnorr` (exact arithmetic). Used by run_bench.py (pass counts go into the
3 +results JSON) and by tests/test_sig_crosscheck.py (pytest gate).
4 +
5 +Usage: PYTHONPATH=src python experiments/signature_benchmarks/python_crosscheck.py <triples.json>
6 +"""
7 +from __future__ import annotations
8 +import json, sys, time
9 +from pathlib import Path
10 +
11 +ROOT = Path(__file__).resolve().parents[2]
12 +sys.path.insert(0, str(ROOT / "src"))
13 +from wp34 import ecdsa as D, schnorr as B # noqa: E402
14 +
15 +
16 +def crosscheck(path: str | Path, resign: bool = True) -> dict:
17 + doc = json.loads(Path(path).read_text())
18 + items = doc["items"]
19 + n = len(items)
20 + c = {
21 + "n": n, "source": str(path),
22 + "ecdsa_k256_sig_verified_by_python": 0, "ecdsa_libsecp_sig_verified_by_python": 0,
23 + "ecdsa_python_resign_identical": 0,
24 + "schnorr_k256_sig_verified_by_python": 0, "schnorr_libsecp_sig_verified_by_python": 0,
25 + "schnorr_python_resign_identical": 0,
26 + "tampered_sig_rejected_by_python": 0,
27 + }
28 + t0 = time.time()
29 + for it in items:
30 + d = int(it["seckey"], 16)
31 + msg = bytes.fromhex(it["msg"])
32 + aux = bytes.fromhex(it["aux_rand"])
33 + pub = bytes.fromhex(it["pubkey_uncompressed"])
34 + Q = (int.from_bytes(pub[1:33], "big"), int.from_bytes(pub[33:65], "big"))
35 + for key, cnt in (("ecdsa_sig_k256", "ecdsa_k256_sig_verified_by_python"),
36 + ("ecdsa_sig_libsecp256k1", "ecdsa_libsecp_sig_verified_by_python")):
37 + sig = bytes.fromhex(it[key])
38 + r, s = int.from_bytes(sig[:32], "big"), int.from_bytes(sig[32:], "big")
39 + if D.verify(Q, msg, (r, s)):
40 + c[cnt] += 1
41 + if resign:
42 + r, s = D.sign(d, msg) # RFC 6979, low-S
43 + if r.to_bytes(32, "big") + s.to_bytes(32, "big") == bytes.fromhex(it["ecdsa_sig_k256"]):
44 + c["ecdsa_python_resign_identical"] += 1
45 + pkx = bytes.fromhex(it["pubkey_xonly"])
46 + for key, cnt in (("schnorr_sig_k256", "schnorr_k256_sig_verified_by_python"),
47 + ("schnorr_sig_libsecp256k1", "schnorr_libsecp_sig_verified_by_python")):
48 + sig = bytes.fromhex(it[key])
49 + if B.verify(msg, pkx, sig):
50 + c[cnt] += 1
51 + if resign:
52 + sig = B.sign(msg, d.to_bytes(32, "big"), aux)
53 + if sig == bytes.fromhex(it["schnorr_sig_k256"]):
54 + c["schnorr_python_resign_identical"] += 1
55 + # negative control: flip one bit of s -> Python must reject
56 + sig = bytearray(bytes.fromhex(it["schnorr_sig_libsecp256k1"]))
57 + sig[63] ^= 0x01
58 + if not B.verify(msg, pkx, bytes(sig)):
59 + c["tampered_sig_rejected_by_python"] += 1
60 + c["elapsed_s"] = round(time.time() - t0, 2)
61 + c["resign_checked"] = resign
62 + keys = ["ecdsa_k256_sig_verified_by_python", "ecdsa_libsecp_sig_verified_by_python",
63 + "schnorr_k256_sig_verified_by_python", "schnorr_libsecp_sig_verified_by_python",
64 + "tampered_sig_rejected_by_python"]
65 + if resign:
66 + keys += ["ecdsa_python_resign_identical", "schnorr_python_resign_identical"]
67 + c["all_pass"] = all(c[k] == n for k in keys)
68 + return c
69 +
70 +
71 +if __name__ == "__main__":
72 + print(json.dumps(crosscheck(sys.argv[1]), indent=1))
added experiments/signature_benchmarks/run_bench.py +134 −0
@@ -0,0 +1,134 @@
1 +"""Thin wrapper around the `sigbench` Rust binary (WP34 Research Axis B).
2 +
3 +Builds the crate (unless --no-build), runs it single-threaded, collects its JSON stdout, runs the
4 +Python cross-check against src/wp34 and the Python baselines, and writes
5 +results/signature_benchmarks/bench_<hostname>.json with the wp34.provenance block
6 +(host, cpu, ram, os, versions, seed, command, timestamp) plus rustc/cargo/crate versions.
7 +
8 +Usage (from the repository root):
9 + PYTHONPATH=src .venv/bin/python experiments/signature_benchmarks/run_bench.py --seed 34
10 +Needs only the standard library + src/wp34 (so it also runs with a bare python3 on cluster nodes).
11 +"""
12 +from __future__ import annotations
13 +import argparse, json, os, platform, re, subprocess, sys, time
14 +from pathlib import Path
15 +
16 +HERE = Path(__file__).resolve().parent
17 +ROOT = HERE.parents[1]
18 +sys.path.insert(0, str(ROOT / "src"))
19 +from wp34 import provenance # noqa: E402
20 +
21 +CRATE = HERE / "sigbench"
22 +
23 +
24 +def run(cmd, **kw):
25 + return subprocess.run(cmd, capture_output=True, text=True, check=True, **kw).stdout
26 +
27 +
28 +def crate_versions() -> dict:
29 + tree = run(["cargo", "tree", "--edges", "normal", "--prefix", "depth"], cwd=CRATE)
30 + wanted = {"secp256k1", "secp256k1-sys", "k256", "elliptic-curve", "ecdsa", "primeorder", "wnaf", "sha2",
31 + "rand", "rand_chacha", "serde", "serde_json", "clap", "crypto-bigint"}
32 + out = {}
33 + for line in tree.splitlines():
34 + m = re.match(r"(\d+)(\S+) v(\S+)", line.strip())
35 + if m and m.group(2) in wanted and m.group(2) not in out:
36 + out[m.group(2)] = m.group(3)
37 + return out
38 +
39 +
40 +def main():
41 + ap = argparse.ArgumentParser()
42 + ap.add_argument("--seed", type=int, default=34)
43 + ap.add_argument("--reps", type=int, default=7)
44 + ap.add_argument("--batch-ops", type=int, default=2000)
45 + ap.add_argument("--batch-min-sigs", type=int, default=2000)
46 + ap.add_argument("--batch-sizes", default="1,2,4,8,16,32,64,128,256,512,1024,2048")
47 + ap.add_argument("--crosscheck-n", type=int, default=1000)
48 + ap.add_argument("--python-crosscheck-n", type=int, default=200)
49 + ap.add_argument("--bootstrap", type=int, default=2000)
50 + ap.add_argument("--label", default=None, help="human label for figures (e.g. 'Laptop M5 Max')")
51 + ap.add_argument("--out", default=None, help="default results/signature_benchmarks/bench_<hostname>.json")
52 + ap.add_argument("--no-build", action="store_true")
53 + ap.add_argument("--no-python-baselines", action="store_true")
54 + ap.add_argument("--python-ops", type=int, default=20)
55 + a = ap.parse_args()
56 + started = time.time()
57 +
58 + host = platform.node().split(".")[0]
59 + out = Path(a.out) if a.out else ROOT / "results" / "signature_benchmarks" / f"bench_{host}.json"
60 + out.parent.mkdir(parents=True, exist_ok=True)
61 + cc_path = out.parent / f"crosscheck_triples_{host}.json"
62 +
63 + if not a.no_build:
64 + subprocess.run(["cargo", "build", "--release"], cwd=CRATE, check=True)
65 + binary = CRATE / "target" / "release" / "sigbench"
66 + vectors = ROOT / "data" / "vectors" / "bip340_test_vectors.csv"
67 + cmd = [str(binary), "--seed", str(a.seed), "--reps", str(a.reps), "--batch-ops", str(a.batch_ops),
68 + "--batch-min-sigs", str(a.batch_min_sigs), "--batch-sizes", a.batch_sizes,
69 + "--crosscheck-n", str(a.crosscheck_n), "--python-crosscheck-n", str(a.python_crosscheck_n),
70 + "--bootstrap", str(a.bootstrap), "--vectors", str(vectors), "--crosscheck-out", str(cc_path)]
71 + print("[run_bench] " + " ".join(cmd), file=sys.stderr)
72 + t0 = time.time()
73 + proc = subprocess.run(cmd, capture_output=True, text=True, check=True)
74 + sys.stderr.write(proc.stderr)
75 + rust = json.loads(proc.stdout)
76 + rust_elapsed = time.time() - t0
77 +
78 + # Python cross-check of the Rust triples against src/wp34 (exact-arithmetic reference)
79 + from python_crosscheck import crosscheck
80 + py_cc = crosscheck(cc_path, resign=True)
81 + print(f"[run_bench] python crosscheck all_pass={py_cc['all_pass']} n={py_cc['n']} ({py_cc['elapsed_s']} s)", file=sys.stderr)
82 +
83 + baselines = None
84 + if not a.no_python_baselines:
85 + try:
86 + bl = run([sys.executable, str(HERE / "python_baselines.py"), "--seed", str(a.seed), "--ops", str(a.python_ops),
87 + "--reps", str(a.reps)], env={**os.environ, "PYTHONPATH": str(ROOT / "src")})
88 + baselines = json.loads(bl)
89 + for k, v in baselines["results"].items():
90 + print(f"[python-baseline] {k:<48} {v['median_ns']:>14.1f} ns/op", file=sys.stderr)
91 + except subprocess.CalledProcessError as e:
92 + baselines = {"error": e.stderr[-2000:]}
93 +
94 + gates = {
95 + "bip340_vectors_both_libraries": rust["bip340_vectors"]["both_all_pass"],
96 + "cross_library_1000": rust["cross_library_checks"]["all_pass"],
97 + "python_reference_200": py_cc["all_pass"],
98 + "own_scalar_mult_agrees_with_k256": rust["scalar_mult"]["own_wnaf5_agrees_with_k256_64_cases"],
99 + "batch_negative_controls_rejected": rust["batch_verification"]["negative_controls"]["all_invalid_batches_rejected"],
100 + }
101 + gates["all"] = all(gates.values())
102 + print(f"[run_bench] correctness gates: {gates}", file=sys.stderr)
103 +
104 + toolchain = {
105 + "rustc": run(["rustc", "--version"]).strip(),
106 + "cargo": run(["cargo", "--version"]).strip(),
107 + "crates": crate_versions(),
108 + "profile": "release (opt-level 3, lto fat, codegen-units 1)",
109 + "target": run(["rustc", "-vV"]).split("host:")[1].split()[0],
110 + }
111 + params = {"seed": a.seed, "reps": a.reps, "batch_ops": a.batch_ops, "batch_min_sigs": a.batch_min_sigs,
112 + "batch_sizes": [int(x) for x in a.batch_sizes.split(",")], "crosscheck_n": a.crosscheck_n,
113 + "python_crosscheck_n": a.python_crosscheck_n, "bootstrap": a.bootstrap, "python_ops": a.python_ops,
114 + "rust_command": " ".join(cmd)}
115 + measurements = {
116 + "correctness_gates": gates,
117 + "bip340_vectors": rust["bip340_vectors"],
118 + "cross_library_checks": rust["cross_library_checks"],
119 + "python_reference_crosscheck": py_cc,
120 + "sign_verify": rust["sign_verify"],
121 + "scalar_mult": rust["scalar_mult"],
122 + "batch_verification": rust["batch_verification"],
123 + "python_baselines": baselines,
124 + "rust_elapsed_s": round(rust_elapsed, 2),
125 + "threading": rust["threading"],
126 + }
127 + doc = provenance.record("signature_benchmarks", params, measurements, seed=a.seed,
128 + extra={"label": a.label or host, "toolchain": toolchain, "crate": "experiments/signature_benchmarks/sigbench"},
129 + out=out, started=started)
130 + print(f"[run_bench] wrote {out} ({doc['elapsed_s']} s total)", file=sys.stderr)
131 +
132 +
133 +if __name__ == "__main__":
134 + main()
added experiments/signature_benchmarks/sigbench/Cargo.lock +784 −0
@@ -0,0 +1,784 @@
1 +# This file is automatically @generated by Cargo.
2 +# It is not intended for manual editing.
3 +version = 4
4 +
5 +[[package]]
6 +name = "anstream"
7 +version = "1.0.0"
8 +source = "registry+https://github.com/rust-lang/crates.io-index"
9 +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
10 +dependencies = [
11 + "anstyle",
12 + "anstyle-parse",
13 + "anstyle-query",
14 + "anstyle-wincon",
15 + "colorchoice",
16 + "is_terminal_polyfill",
17 + "utf8parse",
18 +]
19 +
20 +[[package]]
21 +name = "anstyle"
22 +version = "1.0.14"
23 +source = "registry+https://github.com/rust-lang/crates.io-index"
24 +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
25 +
26 +[[package]]
27 +name = "anstyle-parse"
28 +version = "1.0.0"
29 +source = "registry+https://github.com/rust-lang/crates.io-index"
30 +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
31 +dependencies = [
32 + "utf8parse",
33 +]
34 +
35 +[[package]]
36 +name = "anstyle-query"
37 +version = "1.1.5"
38 +source = "registry+https://github.com/rust-lang/crates.io-index"
39 +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
40 +dependencies = [
41 + "windows-sys",
42 +]
43 +
44 +[[package]]
45 +name = "anstyle-wincon"
46 +version = "3.0.11"
47 +source = "registry+https://github.com/rust-lang/crates.io-index"
48 +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
49 +dependencies = [
50 + "anstyle",
51 + "once_cell_polyfill",
52 + "windows-sys",
53 +]
54 +
55 +[[package]]
56 +name = "autocfg"
57 +version = "1.5.1"
58 +source = "registry+https://github.com/rust-lang/crates.io-index"
59 +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
60 +
61 +[[package]]
62 +name = "base16ct"
63 +version = "1.0.0"
64 +source = "registry+https://github.com/rust-lang/crates.io-index"
65 +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6"
66 +
67 +[[package]]
68 +name = "base64ct"
69 +version = "1.8.3"
70 +source = "registry+https://github.com/rust-lang/crates.io-index"
71 +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
72 +
73 +[[package]]
74 +name = "block-buffer"
75 +version = "0.12.1"
76 +source = "registry+https://github.com/rust-lang/crates.io-index"
77 +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
78 +dependencies = [
79 + "hybrid-array",
80 +]
81 +
82 +[[package]]
83 +name = "cc"
84 +version = "1.10.0"
85 +source = "registry+https://github.com/rust-lang/crates.io-index"
86 +checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0"
87 +dependencies = [
88 + "find-msvc-tools",
89 + "shlex",
90 +]
91 +
92 +[[package]]
93 +name = "cfg-if"
94 +version = "1.0.5"
95 +source = "registry+https://github.com/rust-lang/crates.io-index"
96 +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
97 +
98 +[[package]]
99 +name = "clap"
100 +version = "4.6.7"
101 +source = "registry+https://github.com/rust-lang/crates.io-index"
102 +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
103 +dependencies = [
104 + "clap_builder",
105 + "clap_derive",
106 +]
107 +
108 +[[package]]
109 +name = "clap_builder"
110 +version = "4.6.7"
111 +source = "registry+https://github.com/rust-lang/crates.io-index"
112 +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
113 +dependencies = [
114 + "anstream",
115 + "anstyle",
116 + "clap_lex",
117 + "strsim",
118 +]
119 +
120 +[[package]]
121 +name = "clap_derive"
122 +version = "4.6.7"
123 +source = "registry+https://github.com/rust-lang/crates.io-index"
124 +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
125 +dependencies = [
126 + "heck",
127 + "proc-macro2",
128 + "quote",
129 + "syn 3.0.9",
130 +]
131 +
132 +[[package]]
133 +name = "clap_lex"
134 +version = "1.1.1"
135 +source = "registry+https://github.com/rust-lang/crates.io-index"
136 +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
137 +
138 +[[package]]
139 +name = "cmov"
140 +version = "0.5.4"
141 +source = "registry+https://github.com/rust-lang/crates.io-index"
142 +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
143 +
144 +[[package]]
145 +name = "colorchoice"
146 +version = "1.0.5"
147 +source = "registry+https://github.com/rust-lang/crates.io-index"
148 +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
149 +
150 +[[package]]
151 +name = "const-oid"
152 +version = "0.10.2"
153 +source = "registry+https://github.com/rust-lang/crates.io-index"
154 +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
155 +
156 +[[package]]
157 +name = "cpubits"
158 +version = "0.1.1"
159 +source = "registry+https://github.com/rust-lang/crates.io-index"
160 +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae"
161 +
162 +[[package]]
163 +name = "cpufeatures"
164 +version = "0.3.1"
165 +source = "registry+https://github.com/rust-lang/crates.io-index"
166 +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
167 +dependencies = [
168 + "libc",
169 +]
170 +
171 +[[package]]
172 +name = "crypto-bigint"
173 +version = "0.7.10"
174 +source = "registry+https://github.com/rust-lang/crates.io-index"
175 +checksum = "9c035cb64cba03dad2fb921f3e218785e8cf9a41790da5eb3a1400f62fa2be11"
176 +dependencies = [
177 + "cpubits",
178 + "ctutils",
179 + "getrandom 0.4.3",
180 + "hybrid-array",
181 + "num-traits",
182 + "rand_core 0.10.1",
183 + "subtle",
184 + "zeroize",
185 +]
186 +
187 +[[package]]
188 +name = "crypto-common"
189 +version = "0.2.2"
190 +source = "registry+https://github.com/rust-lang/crates.io-index"
191 +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
192 +dependencies = [
193 + "getrandom 0.4.3",
194 + "hybrid-array",
195 + "rand_core 0.10.1",
196 +]
197 +
198 +[[package]]
199 +name = "ctutils"
200 +version = "0.4.7"
201 +source = "registry+https://github.com/rust-lang/crates.io-index"
202 +checksum = "a2376ffd6f96d8a9db59875b4b6b0237d6900b5baf64279714e05493ce64828a"
203 +dependencies = [
204 + "cmov",
205 + "subtle",
206 +]
207 +
208 +[[package]]
209 +name = "der"
210 +version = "0.8.3"
211 +source = "registry+https://github.com/rust-lang/crates.io-index"
212 +checksum = "6a996a108feb8b427a6e12fcf6af4aa83409b79d72c15337d7741368131ee004"
213 +dependencies = [
214 + "const-oid",
215 + "zeroize",
216 +]
217 +
218 +[[package]]
219 +name = "digest"
220 +version = "0.11.3"
221 +source = "registry+https://github.com/rust-lang/crates.io-index"
222 +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
223 +dependencies = [
224 + "block-buffer",
225 + "const-oid",
226 + "crypto-common",
227 + "ctutils",
228 +]
229 +
230 +[[package]]
231 +name = "ecdsa"
232 +version = "0.17.0"
233 +source = "registry+https://github.com/rust-lang/crates.io-index"
234 +checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0"
235 +dependencies = [
236 + "der",
237 + "digest",
238 + "elliptic-curve",
239 + "rfc6979",
240 + "signature",
241 + "spki",
242 + "zeroize",
243 +]
244 +
245 +[[package]]
246 +name = "elliptic-curve"
247 +version = "0.14.3"
248 +source = "registry+https://github.com/rust-lang/crates.io-index"
249 +checksum = "900f37e7e18d74a2347389af6cd69e3a30d5cc9e697f40004865679aa75e9c7c"
250 +dependencies = [
251 + "base16ct",
252 + "crypto-bigint",
253 + "crypto-common",
254 + "digest",
255 + "ff",
256 + "group",
257 + "hybrid-array",
258 + "pkcs8",
259 + "rand_core 0.10.1",
260 + "sec1",
261 + "subtle",
262 + "zeroize",
263 +]
264 +
265 +[[package]]
266 +name = "ff"
267 +version = "0.14.0"
268 +source = "registry+https://github.com/rust-lang/crates.io-index"
269 +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f"
270 +dependencies = [
271 + "rand_core 0.10.1",
272 + "subtle",
273 +]
274 +
275 +[[package]]
276 +name = "find-msvc-tools"
277 +version = "0.1.15"
278 +source = "registry+https://github.com/rust-lang/crates.io-index"
279 +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48"
280 +
281 +[[package]]
282 +name = "getrandom"
283 +version = "0.3.4"
284 +source = "registry+https://github.com/rust-lang/crates.io-index"
285 +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
286 +dependencies = [
287 + "cfg-if",
288 + "libc",
289 + "r-efi 5.3.0",
290 + "wasip2",
291 +]
292 +
293 +[[package]]
294 +name = "getrandom"
295 +version = "0.4.3"
296 +source = "registry+https://github.com/rust-lang/crates.io-index"
297 +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
298 +dependencies = [
299 + "cfg-if",
300 + "libc",
301 + "r-efi 6.0.0",
302 + "rand_core 0.10.1",
303 +]
304 +
305 +[[package]]
306 +name = "group"
307 +version = "0.14.0"
308 +source = "registry+https://github.com/rust-lang/crates.io-index"
309 +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4"
310 +dependencies = [
311 + "ff",
312 + "rand_core 0.10.1",
313 + "subtle",
314 +]
315 +
316 +[[package]]
317 +name = "heck"
318 +version = "0.5.0"
319 +source = "registry+https://github.com/rust-lang/crates.io-index"
320 +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
321 +
322 +[[package]]
323 +name = "hmac"
324 +version = "0.13.0"
325 +source = "registry+https://github.com/rust-lang/crates.io-index"
326 +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f"
327 +dependencies = [
328 + "digest",
329 +]
330 +
331 +[[package]]
332 +name = "hybrid-array"
333 +version = "0.4.15"
334 +source = "registry+https://github.com/rust-lang/crates.io-index"
335 +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17"
336 +dependencies = [
337 + "subtle",
338 + "typenum",
339 + "zeroize",
340 +]
341 +
342 +[[package]]
343 +name = "is_terminal_polyfill"
344 +version = "1.70.2"
345 +source = "registry+https://github.com/rust-lang/crates.io-index"
346 +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
347 +
348 +[[package]]
349 +name = "itoa"
350 +version = "1.0.18"
351 +source = "registry+https://github.com/rust-lang/crates.io-index"
352 +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
353 +
354 +[[package]]
355 +name = "k256"
356 +version = "0.14.0"
357 +source = "registry+https://github.com/rust-lang/crates.io-index"
358 +checksum = "93f50113171a713f4a4231ef82eb26703607139b35dcb56241f0ceab2ae1f7d8"
359 +dependencies = [
360 + "cpubits",
361 + "ecdsa",
362 + "elliptic-curve",
363 + "primeorder",
364 + "sha2",
365 + "signature",
366 + "wnaf",
367 +]
368 +
369 +[[package]]
370 +name = "libc"
371 +version = "0.2.191"
372 +source = "registry+https://github.com/rust-lang/crates.io-index"
373 +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
374 +
375 +[[package]]
376 +name = "memchr"
377 +version = "2.8.3"
378 +source = "registry+https://github.com/rust-lang/crates.io-index"
379 +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
380 +
381 +[[package]]
382 +name = "num-traits"
383 +version = "0.2.19"
384 +source = "registry+https://github.com/rust-lang/crates.io-index"
385 +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
386 +dependencies = [
387 + "autocfg",
388 +]
389 +
390 +[[package]]
391 +name = "once_cell"
392 +version = "1.21.4"
393 +source = "registry+https://github.com/rust-lang/crates.io-index"
394 +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
395 +
396 +[[package]]
397 +name = "once_cell_polyfill"
398 +version = "1.70.2"
399 +source = "registry+https://github.com/rust-lang/crates.io-index"
400 +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
401 +
402 +[[package]]
403 +name = "pkcs8"
404 +version = "0.11.0"
405 +source = "registry+https://github.com/rust-lang/crates.io-index"
406 +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7"
407 +dependencies = [
408 + "der",
409 + "spki",
410 +]
411 +
412 +[[package]]
413 +name = "ppv-lite86"
414 +version = "0.2.21"
415 +source = "registry+https://github.com/rust-lang/crates.io-index"
416 +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
417 +dependencies = [
418 + "zerocopy",
419 +]
420 +
421 +[[package]]
422 +name = "primefield"
423 +version = "0.14.0"
424 +source = "registry+https://github.com/rust-lang/crates.io-index"
425 +checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4"
426 +dependencies = [
427 + "crypto-bigint",
428 + "crypto-common",
429 + "ff",
430 + "rand_core 0.10.1",
431 + "subtle",
432 + "zeroize",
433 +]
434 +
435 +[[package]]
436 +name = "primeorder"
437 +version = "0.14.0"
438 +source = "registry+https://github.com/rust-lang/crates.io-index"
439 +checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06"
440 +dependencies = [
441 + "elliptic-curve",
442 + "once_cell",
443 + "primefield",
444 + "serdect",
445 + "wnaf",
446 +]
447 +
448 +[[package]]
449 +name = "proc-macro2"
450 +version = "1.0.107"
451 +source = "registry+https://github.com/rust-lang/crates.io-index"
452 +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
453 +dependencies = [
454 + "unicode-ident",
455 +]
456 +
457 +[[package]]
458 +name = "quote"
459 +version = "1.0.47"
460 +source = "registry+https://github.com/rust-lang/crates.io-index"
461 +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
462 +dependencies = [
463 + "proc-macro2",
464 +]
465 +
466 +[[package]]
467 +name = "r-efi"
468 +version = "5.3.0"
469 +source = "registry+https://github.com/rust-lang/crates.io-index"
470 +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
471 +
472 +[[package]]
473 +name = "r-efi"
474 +version = "6.0.0"
475 +source = "registry+https://github.com/rust-lang/crates.io-index"
476 +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
477 +
478 +[[package]]
479 +name = "rand"
480 +version = "0.9.5"
481 +source = "registry+https://github.com/rust-lang/crates.io-index"
482 +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
483 +dependencies = [
484 + "rand_chacha",
485 + "rand_core 0.9.5",
486 +]
487 +
488 +[[package]]
489 +name = "rand_chacha"
490 +version = "0.9.0"
491 +source = "registry+https://github.com/rust-lang/crates.io-index"
492 +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
493 +dependencies = [
494 + "ppv-lite86",
495 + "rand_core 0.9.5",
496 +]
497 +
498 +[[package]]
499 +name = "rand_core"
500 +version = "0.9.5"
501 +source = "registry+https://github.com/rust-lang/crates.io-index"
502 +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
503 +dependencies = [
504 + "getrandom 0.3.4",
505 +]
506 +
507 +[[package]]
508 +name = "rand_core"
509 +version = "0.10.1"
510 +source = "registry+https://github.com/rust-lang/crates.io-index"
511 +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
512 +
513 +[[package]]
514 +name = "rfc6979"
515 +version = "0.6.0"
516 +source = "registry+https://github.com/rust-lang/crates.io-index"
517 +checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b"
518 +dependencies = [
519 + "crypto-bigint",
520 + "hmac",
521 +]
522 +
523 +[[package]]
524 +name = "sec1"
525 +version = "0.8.1"
526 +source = "registry+https://github.com/rust-lang/crates.io-index"
527 +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d"
528 +dependencies = [
529 + "base16ct",
530 + "ctutils",
531 + "der",
532 + "hybrid-array",
533 + "subtle",
534 + "zeroize",
535 +]
536 +
537 +[[package]]
538 +name = "secp256k1"
539 +version = "0.33.1"
540 +source = "registry+https://github.com/rust-lang/crates.io-index"
541 +checksum = "d7f404a8dab7a7a5a631e741d8699aa9c8e1d689fc26ccf897c7187565490b69"
542 +dependencies = [
543 + "rand",
544 + "secp256k1-sys",
545 +]
546 +
547 +[[package]]
548 +name = "secp256k1-sys"
549 +version = "0.14.1"
550 +source = "registry+https://github.com/rust-lang/crates.io-index"
551 +checksum = "f6b2992d4a3cd244539a7d5d0966aadbe5ca7fb868a5d7e38c29499b9e709bbd"
552 +dependencies = [
553 + "cc",
554 +]
555 +
556 +[[package]]
557 +name = "serde"
558 +version = "1.0.229"
559 +source = "registry+https://github.com/rust-lang/crates.io-index"
560 +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
561 +dependencies = [
562 + "serde_core",
563 + "serde_derive",
564 +]
565 +
566 +[[package]]
567 +name = "serde_core"
568 +version = "1.0.229"
569 +source = "registry+https://github.com/rust-lang/crates.io-index"
570 +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
571 +dependencies = [
572 + "serde_derive",
573 +]
574 +
575 +[[package]]
576 +name = "serde_derive"
577 +version = "1.0.229"
578 +source = "registry+https://github.com/rust-lang/crates.io-index"
579 +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
580 +dependencies = [
581 + "proc-macro2",
582 + "quote",
583 + "syn 3.0.9",
584 +]
585 +
586 +[[package]]
587 +name = "serde_json"
588 +version = "1.0.154"
589 +source = "registry+https://github.com/rust-lang/crates.io-index"
590 +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
591 +dependencies = [
592 + "itoa",
593 + "memchr",
594 + "serde",
595 + "serde_core",
596 + "zmij",
597 +]
598 +
599 +[[package]]
600 +name = "serdect"
601 +version = "0.4.3"
602 +source = "registry+https://github.com/rust-lang/crates.io-index"
603 +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e"
604 +dependencies = [
605 + "base16ct",
606 + "serde",
607 +]
608 +
609 +[[package]]
610 +name = "sha2"
611 +version = "0.11.1"
612 +source = "registry+https://github.com/rust-lang/crates.io-index"
613 +checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19"
614 +dependencies = [
615 + "cfg-if",
616 + "cpufeatures",
617 + "digest",
618 +]
619 +
620 +[[package]]
621 +name = "shlex"
622 +version = "2.0.1"
623 +source = "registry+https://github.com/rust-lang/crates.io-index"
624 +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
625 +
626 +[[package]]
627 +name = "sigbench"
628 +version = "0.1.0"
629 +dependencies = [
630 + "clap",
631 + "k256",
632 + "rand",
633 + "rand_chacha",
634 + "secp256k1",
635 + "serde",
636 + "serde_json",
637 + "sha2",
638 +]
639 +
640 +[[package]]
641 +name = "signature"
642 +version = "3.0.0"
643 +source = "registry+https://github.com/rust-lang/crates.io-index"
644 +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5"
645 +dependencies = [
646 + "digest",
647 + "rand_core 0.10.1",
648 +]
649 +
650 +[[package]]
651 +name = "spki"
652 +version = "0.8.1"
653 +source = "registry+https://github.com/rust-lang/crates.io-index"
654 +checksum = "7ef958a98b9d5da290cfc78946e9f3e61e1e62a18db0d92cac0b83cc161491a9"
655 +dependencies = [
656 + "base64ct",
657 + "der",
658 +]
659 +
660 +[[package]]
661 +name = "strsim"
662 +version = "0.11.1"
663 +source = "registry+https://github.com/rust-lang/crates.io-index"
664 +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
665 +
666 +[[package]]
667 +name = "subtle"
668 +version = "2.6.1"
669 +source = "registry+https://github.com/rust-lang/crates.io-index"
670 +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
671 +
672 +[[package]]
673 +name = "syn"
674 +version = "2.0.119"
675 +source = "registry+https://github.com/rust-lang/crates.io-index"
676 +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
677 +dependencies = [
678 + "proc-macro2",
679 + "quote",
680 + "unicode-ident",
681 +]
682 +
683 +[[package]]
684 +name = "syn"
685 +version = "3.0.9"
686 +source = "registry+https://github.com/rust-lang/crates.io-index"
687 +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
688 +dependencies = [
689 + "proc-macro2",
690 + "quote",
691 + "unicode-ident",
692 +]
693 +
694 +[[package]]
695 +name = "typenum"
696 +version = "1.20.1"
697 +source = "registry+https://github.com/rust-lang/crates.io-index"
698 +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
699 +
700 +[[package]]
701 +name = "unicode-ident"
702 +version = "1.0.27"
703 +source = "registry+https://github.com/rust-lang/crates.io-index"
704 +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
705 +
706 +[[package]]
707 +name = "utf8parse"
708 +version = "0.2.2"
709 +source = "registry+https://github.com/rust-lang/crates.io-index"
710 +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
711 +
712 +[[package]]
713 +name = "wasip2"
714 +version = "1.0.4+wasi-0.2.12"
715 +source = "registry+https://github.com/rust-lang/crates.io-index"
716 +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
717 +dependencies = [
718 + "wit-bindgen",
719 +]
720 +
721 +[[package]]
722 +name = "windows-link"
723 +version = "0.2.1"
724 +source = "registry+https://github.com/rust-lang/crates.io-index"
725 +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
726 +
727 +[[package]]
728 +name = "windows-sys"
729 +version = "0.61.2"
730 +source = "registry+https://github.com/rust-lang/crates.io-index"
731 +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
732 +dependencies = [
733 + "windows-link",
734 +]
735 +
736 +[[package]]
737 +name = "wit-bindgen"
738 +version = "0.57.1"
739 +source = "registry+https://github.com/rust-lang/crates.io-index"
740 +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
741 +
742 +[[package]]
743 +name = "wnaf"
744 +version = "0.14.1"
745 +source = "registry+https://github.com/rust-lang/crates.io-index"
746 +checksum = "795ca18b3fdb5e62bf982199278341ddcf7ebf7d32e25e212ad05d496e95f6fa"
747 +dependencies = [
748 + "ff",
749 + "group",
750 + "hybrid-array",
751 + "primefield",
752 +]
753 +
754 +[[package]]
755 +name = "zerocopy"
756 +version = "0.8.63"
757 +source = "registry+https://github.com/rust-lang/crates.io-index"
758 +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8"
759 +dependencies = [
760 + "zerocopy-derive",
761 +]
762 +
763 +[[package]]
764 +name = "zerocopy-derive"
765 +version = "0.8.63"
766 +source = "registry+https://github.com/rust-lang/crates.io-index"
767 +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f"
768 +dependencies = [
769 + "proc-macro2",
770 + "quote",
771 + "syn 2.0.119",
772 +]
773 +
774 +[[package]]
775 +name = "zeroize"
776 +version = "1.9.1"
777 +source = "registry+https://github.com/rust-lang/crates.io-index"
778 +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879"
779 +
780 +[[package]]
781 +name = "zmij"
782 +version = "1.0.23"
783 +source = "registry+https://github.com/rust-lang/crates.io-index"
784 +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
added experiments/signature_benchmarks/sigbench/Cargo.toml +22 −0
@@ -0,0 +1,22 @@
1 +[package]
2 +name = "sigbench"
3 +version = "0.1.0"
4 +edition = "2021"
5 +description = "WP34 Axis B: ECDSA / BIP340 Schnorr benchmarks (libsecp256k1 via rust-secp256k1, RustCrypto k256, own Straus/Pippenger MSM)"
6 +publish = false
7 +
8 +[dependencies]
9 +secp256k1 = { version = "0.33", features = ["rand", "global-context", "std"] }
10 +k256 = { version = "0.14", features = ["ecdsa", "schnorr", "arithmetic", "precomputed-tables", "std", "alloc"] }
11 +sha2 = "0.11"
12 +rand = "0.9"
13 +rand_chacha = "0.9"
14 +serde = { version = "1", features = ["derive"] }
15 +serde_json = "1"
16 +clap = { version = "4", features = ["derive"] }
17 +
18 +[profile.release]
19 +opt-level = 3
20 +lto = "fat"
21 +codegen-units = 1
22 +debug = false
added experiments/signature_benchmarks/sigbench/src/main.rs +584 −0
@@ -0,0 +1,584 @@
1 +//! sigbench — WP34 Research Axis B benchmark binary.
2 +//! Everything is synthetic: keys, messages and aux randomness come from a ChaCha20 RNG seeded
3 +//! with `--seed`. Output: one JSON document on stdout (provenance is added by run_bench.py).
4 +
5 +mod msm;
6 +mod stats;
7 +mod vectors;
8 +
9 +use clap::Parser;
10 +use k256::elliptic_curve::ff::PrimeField;
11 +use k256::elliptic_curve::ops::MulVartime;
12 +use k256::{FieldBytes, ProjectivePoint, Scalar};
13 +use msm::{MsmAlgo, OpCounts, SigItem};
14 +use rand::{RngCore, SeedableRng};
15 +use rand_chacha::ChaCha20Rng;
16 +use secp256k1::{
17 + ecdsa as secp_ecdsa, schnorr as secp_schnorr, Keypair, Message, PublicKey, SecretKey, XOnlyPublicKey,
18 +};
19 +use serde_json::{json, Value};
20 +use stats::Timing;
21 +use std::hint::black_box;
22 +use std::time::Instant;
23 +use vectors::to_hex;
24 +
25 +#[derive(Parser, Debug)]
26 +#[command(name = "sigbench", about = "ECDSA / BIP340 Schnorr benchmarks (WP34 Axis B)")]
27 +struct Args {
28 + /// RNG seed (ChaCha20) for all synthetic keys, messages, aux randomness and batch weights
29 + #[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 benchmarks
35 + #[arg(long, default_value_t = 2000)]
36 + batch_ops: usize,
37 + /// minimum signatures processed per repetition in the batch-verification benchmark
38 + #[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.csv
43 + #[arg(long)]
44 + vectors: String,
45 + /// write (seckey, msg, sig) triples for the Python cross-check here
46 + #[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 +}
61 +
62 +fn rand32(rng: &mut ChaCha20Rng) -> [u8; 32] {
63 + let mut b = [0u8; 32];
64 + rng.fill_bytes(&mut b);
65 + b
66 +}
67 +
68 +/// Draw a valid secret key (1 <= d < n) from the seeded RNG.
69 +fn 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 +}
77 +
78 +/// 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.
80 +fn 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 +}
90 +
91 +struct 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 +}
104 +
105 +fn 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 + p
140 +}
141 +
142 +fn timing_json(t: &Timing) -> Value {
143 + serde_json::to_value(t).unwrap()
144 +}
145 +
146 +fn 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)"));
163 +
164 + // ------------------------------------------------------------ 1a. BIP340 vectors
165 + 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_cases
173 + );
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 + );
183 +
184 + // ------------------------------------------------------------ 1b. cross-library checks
185 + 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 verify
198 + 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 verify
205 + 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 verify
215 + 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 verify
223 + 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_identical
251 + );
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_cc
263 + && 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 + }
278 +
279 + // ------------------------------------------------------------ 2. sign / verify benchmarks
280 + let n = args.batch_ops;
281 + let pool = make_pool(&mut rng, n);
282 + let mut sv = serde_json::Map::new();
283 +
284 + 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));
299 +
300 + 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));
314 +
315 + 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));
334 +
335 + 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));
359 +
360 + // ------------------------------------------------------------ 4. scalar multiplication
361 + 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 counts
396 + 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));
433 +
434 + // ------------------------------------------------------------ 3. batch verification
435 + 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 + }
453 +
454 + 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 call
488 + 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, window
505 + );
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 + }
522 +
523 + // negative controls: one bad signature in a batch of 64 must make every algorithm reject
524 + 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_ns
576 + },
577 + "sizes": sizes, "results": batch_results,
578 + "negative_controls": {"batch_size": nb, "all_invalid_batches_rejected": all_rejected, "cases": Value::Object(neg)},
579 + }),
580 + );
581 +
582 + out.insert("elapsed_s".into(), json!(t_start.elapsed().as_secs_f64()));
583 + println!("{}", serde_json::to_string(&Value::Object(out)).unwrap());
584 +}
added experiments/signature_benchmarks/sigbench/src/msm.rs +362 −0
@@ -0,0 +1,362 @@
1 +//! Multi-scalar multiplication over secp256k1 using k256 group arithmetic
2 +//! (`ProjectivePoint` / `AffinePoint` / `Scalar`), instrumented with operation counters.
3 +//!
4 +//! Two algorithms are implemented here from scratch (not taken from k256):
5 +//! * `straus` — Straus/Shamir interleaving with width-w signed-digit (wNAF) recoding,
6 +//! one odd-multiple table per term, projective-projective additions.
7 +//! * `pippenger` — bucket method (Pippenger 1976), c-bit windows, bucket accumulation with
8 +//! mixed (projective + affine) additions and the running-sum trick.
9 +//! The k256 library routines `lincomb` (constant-time GLV + radix-16) and `lincomb_vartime`
10 +//! (GLV + wNAF multiscalar) are also exposed for comparison; they are not instrumented.
11 +//! Scalar recoding is done on `u64` limbs (exact integer arithmetic only).
12 +
13 +use k256::elliptic_curve::group::{Group, GroupEncoding};
14 +use k256::elliptic_curve::ops::{LinearCombination, Reduce};
15 +use k256::elliptic_curve::ff::PrimeField;
16 +use k256::{AffinePoint, CompressedPoint, FieldBytes, ProjectivePoint, Scalar};
17 +use serde::Serialize;
18 +use sha2::{Digest, Sha256};
19 +
20 +#[derive(Serialize, Clone, Copy, Debug, Default)]
21 +pub struct OpCounts {
22 + /// projective + projective additions (includes additions involving the identity)
23 + pub adds: u64,
24 + /// projective + affine (mixed) additions
25 + pub mixed_adds: u64,
26 + /// point doublings
27 + pub doublings: u64,
28 + /// additions where one operand was the identity (subset of adds + mixed_adds; cheap in math,
29 + /// full cost in k256's complete formulas)
30 + pub identity_adds: u64,
31 +}
32 +
33 +impl OpCounts {
34 + pub fn total(&self) -> u64 {
35 + self.adds + self.mixed_adds + self.doublings
36 + }
37 +}
38 +
39 +/// BIP340 `lift_x`: the point with the given x coordinate and even y, if any.
40 +pub fn lift_x(x: &[u8; 32]) -> Option<AffinePoint> {
41 + let mut c = [0u8; 33];
42 + c[0] = 2;
43 + c[1..].copy_from_slice(x);
44 + let cp = CompressedPoint::from(c);
45 + AffinePoint::from_bytes(&cp).into_option()
46 +}
47 +
48 +pub fn tagged_hash_prefix(tag: &str) -> [u8; 32] {
49 + let th = Sha256::digest(tag.as_bytes());
50 + let mut out = [0u8; 32];
51 + out.copy_from_slice(&th);
52 + out
53 +}
54 +
55 +/// e = int(tagged_hash("BIP0340/challenge", r || pk || msg)) mod n
56 +pub fn challenge(th: &[u8; 32], r: &[u8; 32], pk: &[u8; 32], msg: &[u8]) -> Scalar {
57 + let h = Sha256::new()
58 + .chain_update(th)
59 + .chain_update(th)
60 + .chain_update(r)
61 + .chain_update(pk)
62 + .chain_update(msg)
63 + .finalize();
64 + let mut b = [0u8; 32];
65 + b.copy_from_slice(&h);
66 + <Scalar as Reduce<FieldBytes>>::reduce(&FieldBytes::from(b))
67 +}
68 +
69 +pub fn scalar_from_bytes_strict(b: &[u8]) -> Option<Scalar> {
70 + let mut a = [0u8; 32];
71 + a.copy_from_slice(b);
72 + Scalar::from_repr(FieldBytes::from(a)).into_option()
73 +}
74 +
75 +/// Little-endian 64-bit limbs of a scalar (exact).
76 +pub fn limbs(s: &Scalar) -> [u64; 4] {
77 + let b = s.to_bytes();
78 + let mut l = [0u64; 4];
79 + for i in 0..4 {
80 + l[3 - i] = u64::from_be_bytes(b[i * 8..i * 8 + 8].try_into().unwrap());
81 + }
82 + l
83 +}
84 +
85 +fn is_zero(x: &[u64; 5]) -> bool {
86 + x.iter().all(|&v| v == 0)
87 +}
88 +
89 +fn sub_small(x: &mut [u64; 5], d: u64) {
90 + let mut borrow = d;
91 + for limb in x.iter_mut() {
92 + let (v, b) = limb.overflowing_sub(borrow);
93 + *limb = v;
94 + borrow = b as u64;
95 + if borrow == 0 {
96 + break;
97 + }
98 + }
99 +}
100 +
101 +fn add_small(x: &mut [u64; 5], d: u64) {
102 + let mut carry = d;
103 + for limb in x.iter_mut() {
104 + let (v, c) = limb.overflowing_add(carry);
105 + *limb = v;
106 + carry = c as u64;
107 + if carry == 0 {
108 + break;
109 + }
110 + }
111 +}
112 +
113 +fn shr1(x: &mut [u64; 5]) {
114 + for i in 0..5 {
115 + let hi = if i + 1 < 5 { x[i + 1] << 63 } else { 0 };
116 + x[i] = (x[i] >> 1) | hi;
117 + }
118 +}
119 +
120 +/// Width-w non-adjacent form: digits d_i in {0, ±1, ±3, …, ±(2^{w-1}-1)}, least significant first.
121 +pub fn wnaf(k: &[u64; 4], w: u32) -> Vec<i8> {
122 + debug_assert!((2..=7).contains(&w));
123 + let mut x = [k[0], k[1], k[2], k[3], 0u64];
124 + let mask = (1u64 << w) - 1;
125 + let half = 1i64 << (w - 1);
126 + let mut out = Vec::with_capacity(258);
127 + while !is_zero(&x) {
128 + if x[0] & 1 == 1 {
129 + let mut d = (x[0] & mask) as i64;
130 + if d >= half {
131 + d -= 1 << w;
132 + }
133 + if d > 0 {
134 + sub_small(&mut x, d as u64);
135 + } else {
136 + add_small(&mut x, (-d) as u64);
137 + }
138 + out.push(d as i8);
139 + } else {
140 + out.push(0);
141 + }
142 + shr1(&mut x);
143 + }
144 + out
145 +}
146 +
147 +/// Straus interleaved wNAF multi-scalar multiplication: Σ k_i P_i.
148 +pub fn straus(terms: &[(ProjectivePoint, Scalar)], w: u32, cnt: &mut OpCounts) -> ProjectivePoint {
149 + let tsize = 1usize << (w - 2); // odd multiples 1,3,…,2^{w-1}-1
150 + let mut tables: Vec<Vec<ProjectivePoint>> = Vec::with_capacity(terms.len());
151 + let mut digits: Vec<Vec<i8>> = Vec::with_capacity(terms.len());
152 + for (p, k) in terms {
153 + let dbl = p.double();
154 + cnt.doublings += 1;
155 + let mut t = Vec::with_capacity(tsize);
156 + t.push(*p);
157 + for i in 1..tsize {
158 + t.push(t[i - 1] + dbl);
159 + cnt.adds += 1;
160 + }
161 + tables.push(t);
162 + digits.push(wnaf(&limbs(k), w));
163 + }
164 + let maxlen = digits.iter().map(|d| d.len()).max().unwrap_or(0);
165 + let mut acc = ProjectivePoint::IDENTITY;
166 + let mut started = false;
167 + for i in (0..maxlen).rev() {
168 + if started {
169 + acc = acc.double();
170 + cnt.doublings += 1;
171 + }
172 + for (j, d) in digits.iter().enumerate() {
173 + if i < d.len() && d[i] != 0 {
174 + let idx = (d[i].unsigned_abs() / 2) as usize;
175 + let pt = tables[j][idx];
176 + if !started {
177 + cnt.identity_adds += 1;
178 + }
179 + acc += if d[i] > 0 { pt } else { -pt };
180 + cnt.adds += 1;
181 + started = true;
182 + }
183 + }
184 + }
185 + acc
186 +}
187 +
188 +fn window(l: &[u64; 4], shift: u32, c: u32) -> usize {
189 + let li = (shift / 64) as usize;
190 + let off = shift % 64;
191 + let mut v = l[li] >> off;
192 + if off + c > 64 && li + 1 < 4 {
193 + v |= l[li + 1] << (64 - off);
194 + }
195 + (v & ((1u64 << c) - 1)) as usize
196 +}
197 +
198 +/// Window width heuristic for Pippenger as a function of the number of terms.
199 +pub fn pippenger_window(n_terms: usize) -> u32 {
200 + match n_terms {
201 + 0..=7 => 3,
202 + 8..=31 => 4,
203 + 32..=127 => 5,
204 + 128..=511 => 6,
205 + 512..=2047 => 7,
206 + 2048..=8191 => 8,
207 + _ => 9,
208 + }
209 +}
210 +
211 +/// Pippenger bucket method: Σ k_i P_i with affine inputs (mixed additions into buckets).
212 +pub fn pippenger(terms: &[(AffinePoint, Scalar)], c: u32, cnt: &mut OpCounts) -> ProjectivePoint {
213 + let n_windows = (256 + c - 1) / c;
214 + let lim: Vec<[u64; 4]> = terms.iter().map(|(_, k)| limbs(k)).collect();
215 + let nb = (1usize << c) - 1;
216 + let mut buckets = vec![ProjectivePoint::IDENTITY; nb];
217 + let mut touched = vec![false; nb];
218 + let mut acc = ProjectivePoint::IDENTITY;
219 + let mut started = false;
220 + for wi in (0..n_windows).rev() {
221 + if started {
222 + for _ in 0..c {
223 + acc = acc.double();
224 + cnt.doublings += 1;
225 + }
226 + }
227 + for (b, t) in buckets.iter_mut().zip(touched.iter_mut()) {
228 + *b = ProjectivePoint::IDENTITY;
229 + *t = false;
230 + }
231 + let shift = wi * c;
232 + let mut any = false;
233 + for (i, (p, _)) in terms.iter().enumerate() {
234 + let d = window(&lim[i], shift, c);
235 + if d != 0 {
236 + if !touched[d - 1] {
237 + cnt.identity_adds += 1;
238 + touched[d - 1] = true;
239 + }
240 + buckets[d - 1] += p;
241 + cnt.mixed_adds += 1;
242 + any = true;
243 + }
244 + }
245 + if !any {
246 + continue;
247 + }
248 + // running-sum trick: Σ_j j·B_j = Σ_j (Σ_{i ≥ j} B_i)
249 + let mut running = ProjectivePoint::IDENTITY;
250 + let mut sum = ProjectivePoint::IDENTITY;
251 + let mut run_started = false;
252 + for (j, b) in buckets.iter().enumerate().rev() {
253 + if touched[j] {
254 + if !run_started {
255 + cnt.identity_adds += 1;
256 + }
257 + running += b;
258 + cnt.adds += 1;
259 + run_started = true;
260 + }
261 + if run_started {
262 + sum += running;
263 + cnt.adds += 1;
264 + }
265 + }
266 + if started {
267 + acc += sum;
268 + cnt.adds += 1;
269 + } else {
270 + acc = sum;
271 + started = true;
272 + }
273 + }
274 + acc
275 +}
276 +
277 +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
278 +#[serde(rename_all = "snake_case")]
279 +pub enum MsmAlgo {
280 + Straus,
281 + Pippenger,
282 + K256LincombVartime,
283 + K256LincombCt,
284 +}
285 +
286 +impl MsmAlgo {
287 + pub fn name(&self) -> &'static str {
288 + match self {
289 + MsmAlgo::Straus => "straus_wnaf5",
290 + MsmAlgo::Pippenger => "pippenger",
291 + MsmAlgo::K256LincombVartime => "k256_lincomb_vartime",
292 + MsmAlgo::K256LincombCt => "k256_lincomb_ct",
293 + }
294 + }
295 +}
296 +
297 +/// One BIP340 signature to batch-verify (public key already lifted; `lift_x(R)` and the
298 +/// challenge hash are computed inside the batch routine, as in a real verifier).
299 +pub struct SigItem {
300 + pub pk_x: [u8; 32],
301 + pub p: AffinePoint,
302 + pub msg: [u8; 32],
303 + pub sig: [u8; 64],
304 +}
305 +
306 +pub struct BatchOutcome {
307 + pub valid: bool,
308 + pub counts: OpCounts,
309 + pub msm_ns: u128,
310 + pub n_terms: usize,
311 + pub window: u32,
312 +}
313 +
314 +/// Batch verification Σ a_i s_i G − Σ a_i R_i − Σ a_i e_i P_i = O with a_1 = 1,
315 +/// a_i random 128-bit (BIP340 §"Batch Verification"; Bernstein et al. 2012 for Ed25519).
316 +pub fn batch_verify(items: &[&SigItem], weights: &[[u8; 16]], algo: MsmAlgo, th: &[u8; 32], pip_c: u32) -> BatchOutcome {
317 + let n = items.len();
318 + let mut terms: Vec<(AffinePoint, Scalar)> = Vec::with_capacity(2 * n + 1);
319 + let mut s_g = Scalar::ZERO;
320 + let mut cnt = OpCounts::default();
321 + let fail = |n_terms| BatchOutcome { valid: false, counts: OpCounts::default(), msm_ns: 0, n_terms, window: 0 };
322 + for (i, it) in items.iter().enumerate() {
323 + let r: [u8; 32] = it.sig[..32].try_into().unwrap();
324 + let s = match scalar_from_bytes_strict(&it.sig[32..]) {
325 + Some(s) => s,
326 + None => return fail(0),
327 + };
328 + let rp = match lift_x(&r) {
329 + Some(p) => p,
330 + None => return fail(0),
331 + };
332 + let e = challenge(th, &r, &it.pk_x, &it.msg);
333 + let a = if i == 0 { Scalar::ONE } else { Scalar::from(u128::from_le_bytes(weights[i])) };
334 + s_g += a * s;
335 + // negate the points (free in affine) rather than the scalars, so a_i stays 128-bit
336 + terms.push((-rp, a));
337 + terms.push((-it.p, a * e));
338 + }
339 + terms.push((AffinePoint::GENERATOR, s_g));
340 + let n_terms = terms.len();
341 + let t0 = std::time::Instant::now();
342 + let (res, window) = match algo {
343 + MsmAlgo::Straus => {
344 + let proj: Vec<(ProjectivePoint, Scalar)> = terms.iter().map(|(p, k)| (ProjectivePoint::from(*p), *k)).collect();
345 + (straus(&proj, 5, &mut cnt), 5)
346 + }
347 + MsmAlgo::Pippenger => {
348 + let c = if pip_c == 0 { pippenger_window(n_terms) } else { pip_c };
349 + (pippenger(&terms, c, &mut cnt), c)
350 + }
351 + MsmAlgo::K256LincombVartime => {
352 + let proj: Vec<(ProjectivePoint, Scalar)> = terms.iter().map(|(p, k)| (ProjectivePoint::from(*p), *k)).collect();
353 + (<ProjectivePoint as LinearCombination<[(ProjectivePoint, Scalar)]>>::lincomb_vartime(proj.as_slice()), 0)
354 + }
355 + MsmAlgo::K256LincombCt => {
356 + let proj: Vec<(ProjectivePoint, Scalar)> = terms.iter().map(|(p, k)| (ProjectivePoint::from(*p), *k)).collect();
357 + (<ProjectivePoint as LinearCombination<[(ProjectivePoint, Scalar)]>>::lincomb(proj.as_slice()), 0)
358 + }
359 + };
360 + let msm_ns = t0.elapsed().as_nanos();
361 + BatchOutcome { valid: bool::from(res.is_identity()), counts: cnt, msm_ns, n_terms, window }
362 +}
added experiments/signature_benchmarks/sigbench/src/stats.rs +105 −0
@@ -0,0 +1,105 @@
1 +//! Descriptive statistics and bootstrap confidence intervals for timing samples.
2 +//! Floats are used here only for statistics (never for field arithmetic).
3 +
4 +use rand::{RngCore, SeedableRng};
5 +use rand_chacha::ChaCha20Rng;
6 +use serde::Serialize;
7 +
8 +/// Linear-interpolated quantile of a sorted slice (same convention as numpy `linear`).
9 +pub fn quantile_sorted(sorted: &[f64], q: f64) -> f64 {
10 + if sorted.is_empty() {
11 + return f64::NAN;
12 + }
13 + let pos = q * (sorted.len() as f64 - 1.0);
14 + let lo = pos.floor() as usize;
15 + let hi = pos.ceil() as usize;
16 + if lo == hi {
17 + sorted[lo]
18 + } else {
19 + sorted[lo] + (sorted[hi] - sorted[lo]) * (pos - lo as f64)
20 + }
21 +}
22 +
23 +pub fn median(xs: &[f64]) -> f64 {
24 + let mut v = xs.to_vec();
25 + v.sort_by(|a, b| a.partial_cmp(b).unwrap());
26 + quantile_sorted(&v, 0.5)
27 +}
28 +
29 +#[derive(Serialize, Clone, Debug)]
30 +pub struct Timing {
31 + pub reps: usize,
32 + pub ops_per_rep: usize,
33 + pub ns_per_op_samples: Vec<f64>,
34 + pub median_ns: f64,
35 + pub q1_ns: f64,
36 + pub q3_ns: f64,
37 + pub iqr_ns: f64,
38 + pub min_ns: f64,
39 + pub mean_ns: f64,
40 + pub sd_ns: f64,
41 + pub ops_per_s: f64,
42 +}
43 +
44 +impl Timing {
45 + pub fn from_samples(ops_per_rep: usize, samples: Vec<f64>) -> Timing {
46 + let mut v = samples.clone();
47 + v.sort_by(|a, b| a.partial_cmp(b).unwrap());
48 + let n = v.len() as f64;
49 + let mean = v.iter().sum::<f64>() / n;
50 + let sd = if v.len() > 1 {
51 + (v.iter().map(|x| (x - mean) * (x - mean)).sum::<f64>() / (n - 1.0)).sqrt()
52 + } else {
53 + 0.0
54 + };
55 + let med = quantile_sorted(&v, 0.5);
56 + let q1 = quantile_sorted(&v, 0.25);
57 + let q3 = quantile_sorted(&v, 0.75);
58 + Timing {
59 + reps: samples.len(),
60 + ops_per_rep,
61 + ns_per_op_samples: samples,
62 + median_ns: med,
63 + q1_ns: q1,
64 + q3_ns: q3,
65 + iqr_ns: q3 - q1,
66 + min_ns: v[0],
67 + mean_ns: mean,
68 + sd_ns: sd,
69 + ops_per_s: 1e9 / med,
70 + }
71 + }
72 +}
73 +
74 +#[derive(Serialize, Clone, Debug)]
75 +pub struct RatioCI {
76 + pub point: f64,
77 + pub ci95_low: f64,
78 + pub ci95_high: f64,
79 + pub bootstrap_resamples: usize,
80 +}
81 +
82 +/// Bootstrap 95 % CI for the ratio median(num) / median(den), resampling each sample set with
83 +/// replacement (percentile method). Deterministic given `seed`.
84 +pub fn bootstrap_ratio_of_medians(num: &[f64], den: &[f64], resamples: usize, seed: u64) -> RatioCI {
85 + let mut rng = ChaCha20Rng::seed_from_u64(seed);
86 + let mut ratios = Vec::with_capacity(resamples);
87 + let mut bn = vec![0.0; num.len()];
88 + let mut bd = vec![0.0; den.len()];
89 + for _ in 0..resamples {
90 + for x in bn.iter_mut() {
91 + *x = num[(rng.next_u64() % num.len() as u64) as usize];
92 + }
93 + for x in bd.iter_mut() {
94 + *x = den[(rng.next_u64() % den.len() as u64) as usize];
95 + }
96 + ratios.push(median(&bn) / median(&bd));
97 + }
98 + ratios.sort_by(|a, b| a.partial_cmp(b).unwrap());
99 + RatioCI {
100 + point: median(num) / median(den),
101 + ci95_low: quantile_sorted(&ratios, 0.025),
102 + ci95_high: quantile_sorted(&ratios, 0.975),
103 + bootstrap_resamples: resamples,
104 + }
105 +}
added experiments/signature_benchmarks/sigbench/src/vectors.rs +151 −0
@@ -0,0 +1,151 @@
1 +//! BIP340 official test vectors (data/vectors/bip340_test_vectors.csv) checked against both
2 +//! libsecp256k1 (rust-secp256k1) and k256.
3 +
4 +use serde::Serialize;
5 +
6 +#[derive(Debug, Clone)]
7 +pub struct Vector {
8 + pub index: u32,
9 + pub seckey: Option<[u8; 32]>,
10 + pub pubkey: [u8; 32],
11 + pub aux: Option<[u8; 32]>,
12 + pub msg: Vec<u8>,
13 + pub sig: [u8; 64],
14 + pub expected: bool,
15 + pub comment: String,
16 +}
17 +
18 +pub fn hex(s: &str) -> Vec<u8> {
19 + (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect()
20 +}
21 +
22 +pub fn to_hex(b: &[u8]) -> String {
23 + b.iter().map(|x| format!("{:02x}", x)).collect()
24 +}
25 +
26 +fn arr32(v: &[u8]) -> [u8; 32] {
27 + let mut a = [0u8; 32];
28 + a.copy_from_slice(v);
29 + a
30 +}
31 +
32 +pub fn parse_csv(text: &str) -> Vec<Vector> {
33 + let mut out = Vec::new();
34 + for (ln, line) in text.lines().enumerate() {
35 + if ln == 0 || line.trim().is_empty() {
36 + continue;
37 + }
38 + let f: Vec<&str> = line.split(',').collect();
39 + assert!(f.len() >= 7, "bad CSV line {}", ln);
40 + let sig = hex(f[5]);
41 + let mut s64 = [0u8; 64];
42 + s64.copy_from_slice(&sig);
43 + out.push(Vector {
44 + index: f[0].parse().unwrap(),
45 + seckey: if f[1].is_empty() { None } else { Some(arr32(&hex(f[1]))) },
46 + pubkey: arr32(&hex(f[2])),
47 + aux: if f[3].is_empty() { None } else { Some(arr32(&hex(f[3]))) },
48 + msg: hex(f[4]),
49 + sig: s64,
50 + expected: f[6].trim().eq_ignore_ascii_case("TRUE"),
51 + comment: f.get(7).map(|s| s.trim().to_string()).unwrap_or_default(),
52 + });
53 + }
54 + out
55 +}
56 +
57 +#[derive(Serialize, Debug, Clone)]
58 +pub struct VectorResult {
59 + pub index: u32,
60 + pub expected: bool,
61 + pub verify_ok: bool,
62 + pub sign_checked: bool,
63 + pub sign_ok: Option<bool>,
64 + pub comment: String,
65 +}
66 +
67 +#[derive(Serialize, Debug, Clone)]
68 +pub struct VectorSummary {
69 + pub library: String,
70 + pub n_vectors: usize,
71 + pub verify_pass: usize,
72 + pub sign_cases: usize,
73 + pub sign_pass: usize,
74 + pub all_pass: bool,
75 + pub details: Vec<VectorResult>,
76 +}
77 +
78 +pub fn check_secp(vs: &[Vector]) -> VectorSummary {
79 + use secp256k1::{schnorr, Keypair, SecretKey, XOnlyPublicKey};
80 + let mut details = Vec::new();
81 + for v in vs {
82 + let sig = schnorr::Signature::from_byte_array(v.sig);
83 + let verified = match XOnlyPublicKey::from_byte_array(v.pubkey) {
84 + Ok(pk) => schnorr::verify(&sig, &v.msg, &pk).is_ok(),
85 + Err(_) => false,
86 + };
87 + let mut sign_ok = None;
88 + if let (Some(sk), Some(aux)) = (v.seckey, v.aux) {
89 + let kp = Keypair::from_secret_key(&SecretKey::from_secret_bytes(sk).unwrap());
90 + let (xo, _) = kp.x_only_public_key();
91 + let s = schnorr::sign_with_aux_rand(&v.msg, &kp, &aux);
92 + sign_ok = Some(s.to_byte_array() == v.sig && xo.to_byte_array() == v.pubkey);
93 + }
94 + details.push(VectorResult {
95 + index: v.index,
96 + expected: v.expected,
97 + verify_ok: verified == v.expected,
98 + sign_checked: sign_ok.is_some(),
99 + sign_ok,
100 + comment: v.comment.clone(),
101 + });
102 + }
103 + summarize("libsecp256k1 (rust-secp256k1)", details)
104 +}
105 +
106 +pub fn check_k256(vs: &[Vector]) -> VectorSummary {
107 + use k256::schnorr::{Signature, SigningKey, VerifyingKey};
108 + use k256::FieldBytes;
109 + let mut details = Vec::new();
110 + for v in vs {
111 + let verified = match (VerifyingKey::from_bytes(&FieldBytes::from(v.pubkey)), Signature::from_slice(&v.sig)) {
112 + (Ok(pk), Ok(sig)) => pk.verify_raw(&v.msg, &sig).is_ok(),
113 + _ => false,
114 + };
115 + let mut sign_ok = None;
116 + if let (Some(sk), Some(aux)) = (v.seckey, v.aux) {
117 + let k = SigningKey::from_bytes(&FieldBytes::from(sk)).unwrap();
118 + let pkb: [u8; 32] = k.verifying_key().to_bytes().into();
119 + let s = k.sign_raw(&v.msg, &aux).map(|s| s.to_bytes());
120 + sign_ok = Some(match s {
121 + Ok(b) => b.as_slice() == &v.sig[..] && pkb == v.pubkey,
122 + Err(_) => false,
123 + });
124 + }
125 + details.push(VectorResult {
126 + index: v.index,
127 + expected: v.expected,
128 + verify_ok: verified == v.expected,
129 + sign_checked: sign_ok.is_some(),
130 + sign_ok,
131 + comment: v.comment.clone(),
132 + });
133 + }
134 + summarize("k256 (RustCrypto)", details)
135 +}
136 +
137 +fn summarize(library: &str, details: Vec<VectorResult>) -> VectorSummary {
138 + let n = details.len();
139 + let verify_pass = details.iter().filter(|d| d.verify_ok).count();
140 + let sign_cases = details.iter().filter(|d| d.sign_checked).count();
141 + let sign_pass = details.iter().filter(|d| d.sign_ok == Some(true)).count();
142 + VectorSummary {
143 + library: library.to_string(),
144 + n_vectors: n,
145 + verify_pass,
146 + sign_cases,
147 + sign_pass,
148 + all_pass: verify_pass == n && sign_pass == sign_cases,
149 + details,
150 + }
151 +}
added experiments/signature_benchmarks/summarize.py +80 −0
@@ -0,0 +1,80 @@
1 +"""Collect results/signature_benchmarks/bench_*.json into results/signature_benchmarks/summary.json
2 +(headline numbers per host: correctness gates, sign/verify ns per op, scalar-mult, best batch speedups,
3 +Python baselines) and print a Markdown table to stdout.
4 +
5 +Usage: .venv/bin/python experiments/signature_benchmarks/summarize.py
6 +"""
7 +from __future__ import annotations
8 +import datetime as dt, json, sys
9 +from pathlib import Path
10 +
11 +ROOT = Path(__file__).resolve().parents[2]
12 +RES = ROOT / "results" / "signature_benchmarks"
13 +
14 +
15 +def main():
16 + hosts = {}
17 + for f in sorted(RES.glob("bench_*.json")):
18 + d = json.loads(f.read_text())
19 + m = d["measurements"]
20 + sv = {k: round(v["median_ns"], 1) for k, v in m["sign_verify"].items()}
21 + sm = {k: round(v["median_ns"], 1) for k, v in m["scalar_mult"].items() if isinstance(v, dict) and "median_ns" in v}
22 + bv = m["batch_verification"]
23 + batch = {}
24 + for r in bv["results"]:
25 + batch.setdefault(r["algorithm"], {})[str(r["batch_size"])] = {
26 + "ns_per_sig": round(r["per_signature"]["median_ns"], 1),
27 + "iqr_ns": round(r["per_signature"]["iqr_ns"], 1),
28 + "msm_only_ns_per_sig": round(r["msm_only_ns_per_sig_median"], 1),
29 + "speedup_vs_k256": [round(r["speedup_vs_k256_single_verify"][k], 3) for k in ("point", "ci95_low", "ci95_high")],
30 + "speedup_vs_libsecp256k1": [round(r["speedup_vs_libsecp256k1_single_verify"][k], 3) for k in ("point", "ci95_low", "ci95_high")],
31 + "window_bits": r["window_bits"],
32 + "ops_per_sig": {k: round(v, 1) for k, v in r["op_counts_per_signature"].items()},
33 + }
34 + best = {}
35 + for alg, per in batch.items():
36 + b, row = max(per.items(), key=lambda kv: kv[1]["speedup_vs_k256"][0])
37 + best[alg] = {"batch_size": int(b), **row}
38 + py = m.get("python_baselines") or {}
39 + pyr = {k: round(v["median_ns"], 1) for k, v in (py.get("results") or {}).items()}
40 + hosts[d["label"]] = {
41 + "file": f.name, "hostname": d["host"]["hostname"], "cpu": d["host"]["cpu"], "cores": d["host"]["cores"],
42 + "ram_gb": d["host"]["ram_gb"], "os": d["host"]["os"], "timestamp_utc": d["timestamp_utc"], "seed": d["seed"],
43 + "rustc": d["toolchain"]["rustc"], "crates": {k: d["toolchain"]["crates"].get(k) for k in ("secp256k1", "secp256k1-sys", "k256")},
44 + "correctness_gates": m["correctness_gates"],
45 + "bip340_vectors": {lib: {"verify_pass": m["bip340_vectors"][lib]["verify_pass"], "sign_pass": m["bip340_vectors"][lib]["sign_pass"],
46 + "n": m["bip340_vectors"][lib]["n_vectors"]} for lib in ("libsecp256k1", "k256")},
47 + "cross_library_checks": m["cross_library_checks"],
48 + "python_reference_crosscheck": {k: v for k, v in m["python_reference_crosscheck"].items() if k != "source"},
49 + "sign_verify_median_ns": sv,
50 + "scalar_mult_median_ns": sm,
51 + "single_verify_reference_ns": bv["single_verify_reference_ns"],
52 + "batch_verification": batch,
53 + "best_batch_speedup_per_algorithm": best,
54 + "negative_controls": bv["negative_controls"]["all_invalid_batches_rejected"],
55 + "python_baselines_median_ns": pyr,
56 + "python_versions": py.get("versions"),
57 + "rust_elapsed_s": m["rust_elapsed_s"],
58 + }
59 + summary = {"generated_utc": dt.datetime.now(dt.timezone.utc).isoformat(timespec="seconds"),
60 + "n_hosts": len(hosts), "hosts": hosts,
61 + "classification": {
62 + "batch_verification_speedup": "known_reproduced",
63 + "implementation_spread": "empirical_observation",
64 + "bip340_vectors_and_cross_implementation_agreement": "known_reproduced",
65 + }}
66 + (RES / "summary.json").write_text(json.dumps(summary, indent=1))
67 + # Markdown digest
68 + print("| host | ecdsa sign/verify libsecp | ecdsa sign/verify k256 | schnorr sign/verify libsecp | schnorr sign/verify k256 | best batch (alg, b, ns/sig, S vs k256, S vs libsecp) |")
69 + print("|---|---|---|---|---|---|")
70 + for lab, h in hosts.items():
71 + s = h["sign_verify_median_ns"]
72 + alg, row = max(h["best_batch_speedup_per_algorithm"].items(), key=lambda kv: kv[1]["speedup_vs_k256"][0])
73 + print(f"| {lab} | {s['ecdsa_sign_libsecp256k1']:.0f} / {s['ecdsa_verify_libsecp256k1']:.0f} | {s['ecdsa_sign_k256']:.0f} / {s['ecdsa_verify_k256']:.0f} "
74 + f"| {s['schnorr_sign_libsecp256k1']:.0f} / {s['schnorr_verify_libsecp256k1']:.0f} | {s['schnorr_sign_k256']:.0f} / {s['schnorr_verify_k256']:.0f} "
75 + f"| {alg}, b={row['batch_size']}, {row['ns_per_sig']:.0f} ns, {row['speedup_vs_k256'][0]:.2f} [{row['speedup_vs_k256'][1]:.2f},{row['speedup_vs_k256'][2]:.2f}], {row['speedup_vs_libsecp256k1'][0]:.2f} |")
76 + print(f"\nwrote {RES / 'summary.json'}", file=sys.stderr)
77 +
78 +
79 +if __name__ == "__main__":
80 + main()
added experiments/synthetic_ecdlp/analyze_ecdlp.py +381 −0
@@ -0,0 +1,381 @@
1 +"""Analysis of the ECDLP studies: statistics with bootstrap CIs, empirical complexity fits, figures and LaTeX tables.
2 +
3 +Inputs : results/synthetic_ecdlp/{scaling_generic,scaling_j0,sweep_r,parallel,tmto}_<host>.json
4 +Outputs: results/synthetic_ecdlp/analysis.json, figures/ecdlp_*.pdf (+ copies in paper/figures), paper/tables/ecdlp_*.tex
5 +"""
6 +import glob, json, math, pathlib, re, sys
7 +import numpy as np
8 +import matplotlib
9 +matplotlib.use("Agg")
10 +import matplotlib.pyplot as plt
11 +ROOT = pathlib.Path(__file__).resolve().parents[2]
12 +R = ROOT / "results/synthetic_ecdlp"; F = ROOT / "figures"; PF = ROOT / "paper/figures"; T = ROOT / "paper/tables"
13 +for d in (F, PF, T):
14 + d.mkdir(parents=True, exist_ok=True)
15 +plt.rcParams.update({"font.family": "serif", "font.size": 9, "mathtext.fontset": "stix", "axes.grid": True, "grid.alpha": 0.3})
16 +rng = np.random.default_rng(34)
17 +ANALYSIS = {}
18 +MACROS = {}
19 +
20 +
21 +def load(study):
22 + files = sorted(glob.glob(str(R / f"{study}_*.json")))
23 + files = [f for f in files if ".chunk" not in f and ".raw" not in f]
24 + if not files:
25 + return None, None
26 + d = json.loads(pathlib.Path(files[0]).read_text())
27 + runs = [r for r in d["measurements"]["runs"] if r]
28 + for r in runs:
29 + r["config"] = r["label"].split("/")[1]; r["inst"] = r["label"].split("/")[0]
30 + r["bits"] = int(re.match(r"[gj](\d+)b", r["inst"]).group(1))
31 + r["n"] = int(r["params"]["instance"]["n"]) if "instance" in r["params"] else int(r["params"]["n"])
32 + return d, runs
33 +
34 +
35 +def boot_mean(x, B=2000):
36 + x = np.asarray(x, float)
37 + if len(x) < 2:
38 + return float(x.mean()), float(x.mean()), float(x.mean())
39 + means = rng.choice(x, size=(B, len(x)), replace=True).mean(axis=1)
40 + return float(x.mean()), float(np.percentile(means, 2.5)), float(np.percentile(means, 97.5))
41 +
42 +
43 +def boot_ratio(a, b, B=2000):
44 + """Bootstrap CI for mean(a)/mean(b) with paired resampling when lengths match."""
45 + a, b = np.asarray(a, float), np.asarray(b, float)
46 + if len(a) == len(b):
47 + idx = rng.integers(0, len(a), size=(B, len(a)))
48 + r = a[idx].mean(axis=1) / b[idx].mean(axis=1)
49 + else:
50 + r = rng.choice(a, size=(B, len(a))).mean(axis=1) / rng.choice(b, size=(B, len(b))).mean(axis=1)
51 + return float(a.mean() / b.mean()), float(np.percentile(r, 2.5)), float(np.percentile(r, 97.5))
52 +
53 +
54 +def fit_power(ns, ys, with_log=False):
55 + """Least squares on log y = log a + α log n (+ β log log2 n). Returns (a, α, β, r²) and bootstrap CI for α."""
56 + ns, ys = np.asarray(ns, float), np.asarray(ys, float)
57 + X = np.column_stack([np.ones_like(ns), np.log(ns)] + ([np.log(np.log2(ns))] if with_log else []))
58 + y = np.log(ys)
59 + coef, *_ = np.linalg.lstsq(X, y, rcond=None)
60 + pred = X @ coef; r2 = 1 - ((y - pred) ** 2).sum() / ((y - y.mean()) ** 2).sum()
61 + alphas = []
62 + for _ in range(1000):
63 + idx = rng.integers(0, len(ns), len(ns))
64 + c, *_ = np.linalg.lstsq(X[idx], y[idx], rcond=None); alphas.append(c[1])
65 + return {"a": float(math.exp(coef[0])), "alpha": float(coef[1]), "beta": float(coef[2]) if with_log else 0.0, "r2": float(r2),
66 + "alpha_ci": [float(np.percentile(alphas, 2.5)), float(np.percentile(alphas, 97.5))]}
67 +
68 +
69 +def summarise(runs, key="group_ops"):
70 + out = {}
71 + for cfg in sorted({r["config"] for r in runs}):
72 + out[cfg] = {}
73 + for bits in sorted({r["bits"] for r in runs if r["config"] == cfg}):
74 + rs = [r for r in runs if r["config"] == cfg and r["bits"] == bits]
75 + n = float(np.mean([r["n"] for r in rs]))
76 + ops = [r[key] for r in rs]; mean, lo, hi = boot_mean(ops)
77 + its = [r.get("iterations", 0) for r in rs]
78 + out[cfg][bits] = {"n_mean": n, "count": len(rs), "ops_mean": mean, "ops_ci": [lo, hi], "ops_median": float(np.median(ops)),
79 + "ops_sd": float(np.std(ops)), "ops_over_sqrt_n": mean / math.sqrt(n), "iter_mean": float(np.mean(its)),
80 + "iter_over_sqrt_n": float(np.mean(its)) / math.sqrt(n), "wall_mean": float(np.mean([r["wall_s"] for r in rs])),
81 + "cpu_mean": float(np.mean([r["cpu_s"] for r in rs])), "rss_max_mb": max(r["peak_rss_bytes"] for r in rs) / 2**20,
82 + "table_entries_mean": float(np.mean([r.get("table_entries", 0) or 0 for r in rs])),
83 + "dp_count_mean": float(np.mean([r.get("dp_count", 0) or 0 for r in rs])),
84 + "fruitless_mean": float(np.mean([r.get("fruitless_cycles", 0) or 0 for r in rs])),
85 + "ok": all(r["ok"] for r in rs)}
86 + return out
87 +
88 +
89 +LABELS = {"bsgs": "BSGS", "rho_floyd": "rho, Floyd", "rho_brent": "rho, Brent", "rho_dp": "rho, DP ($r=20$)", "rho_dp_r128": "rho, DP ($r=128$)",
90 + "rho_dp_neg": "rho, DP + negation ($r=128$)", "kangaroo": "kangaroo (full interval)",
91 + "rho_dp_aut3": r"rho, DP + $\phi$-orbits", "rho_dp_neg_aut3": r"rho, DP + negation + $\phi$ ($r=128$)", "rho_dp_neg_aut3_r20": r"rho, DP + negation + $\phi$ ($r=20$)"}
92 +THEORY = {"bsgs": lambda n: 2 * math.sqrt(n), "rho_floyd": lambda n: 3 * math.sqrt(math.pi * n / 2), "rho_brent": lambda n: 1.0 * math.sqrt(math.pi * n / 2) * 1.15,
93 + "rho_dp": lambda n: math.sqrt(math.pi * n / 2), "rho_dp_r128": lambda n: math.sqrt(math.pi * n / 2), "rho_dp_neg": lambda n: math.sqrt(math.pi * n / 4),
94 + "kangaroo": lambda n: 2 * math.sqrt(n), "rho_dp_aut3": lambda n: math.sqrt(math.pi * n / 6), "rho_dp_neg_aut3": lambda n: math.sqrt(math.pi * n / 12),
95 + "rho_dp_neg_aut3_r20": lambda n: math.sqrt(math.pi * n / 12)}
96 +
97 +
98 +def analyse_scaling():
99 + d, runs = load("scaling_generic")
100 + if not runs:
101 + return
102 + S = summarise(runs); ANALYSIS["scaling_generic"] = {"host": d["host"], "summary": S}
103 + # fits per config on group_ops
104 + fits = {}
105 + for cfg, per in S.items():
106 + ns = [v["n_mean"] for v in per.values()]; ys = [v["ops_mean"] for v in per.values()]
107 + if len(ns) >= 3:
108 + big = [(v["n_mean"], v["ops_mean"], v["iter_mean"]) for b, v in per.items() if b >= 32]
109 + fits[cfg] = {"power": fit_power(ns, ys), "power_log": fit_power(ns, ys, True),
110 + "power_ge32": fit_power([t[0] for t in big], [t[1] for t in big]) if len(big) >= 3 else None,
111 + "power_iter_ge32": fit_power([t[0] for t in big], [t[2] for t in big]) if len(big) >= 3 and all(t[2] > 0 for t in big) else None}
112 + ANALYSIS["scaling_generic"]["fits"] = fits
113 + # figure: runtime scaling (ops and wall) log-log with theory lines
114 + fig, axes = plt.subplots(1, 2, figsize=(7.2, 3.3))
115 + for cfg, per in S.items():
116 + bits = sorted(per); ns = [per[b]["n_mean"] for b in bits]
117 + axes[0].errorbar(ns, [per[b]["ops_mean"] for b in bits], yerr=[[per[b]["ops_mean"] - per[b]["ops_ci"][0] for b in bits], [per[b]["ops_ci"][1] - per[b]["ops_mean"] for b in bits]], marker="o", ms=3, lw=1, capsize=2, label=LABELS.get(cfg, cfg))
118 + axes[1].plot(ns, [per[b]["wall_mean"] for b in bits], marker="o", ms=3, lw=1, label=LABELS.get(cfg, cfg))
119 + nn = np.logspace(math.log10(2**20), math.log10(2**56), 50)
120 + axes[0].plot(nn, np.sqrt(np.pi * nn / 2), "k--", lw=0.8, label=r"$\sqrt{\pi n/2}$")
121 + axes[0].plot(nn, 2 * np.sqrt(nn), "k:", lw=0.8, label=r"$2\sqrt{n}$")
122 + for ax in axes:
123 + ax.set_xscale("log", base=2); ax.set_yscale("log"); ax.set_xlabel("group order $n$")
124 + axes[0].set_ylabel("group operations (mean, 95% CI)"); axes[1].set_ylabel("wall-clock seconds (1 thread)")
125 + axes[0].legend(fontsize=6, ncol=1, loc="lower right"); fig.tight_layout()
126 + fig.savefig(F / "ecdlp_scaling.pdf"); fig.savefig(PF / "ecdlp_scaling.pdf"); plt.close(fig)
127 + # figure: normalised ops/√n with theory constants
128 + fig, ax = plt.subplots(figsize=(6.4, 3.2))
129 + for cfg, per in S.items():
130 + bits = sorted(per)
131 + ax.plot(bits, [per[b]["ops_over_sqrt_n"] for b in bits], marker="o", ms=3, lw=1, label=LABELS.get(cfg, cfg))
132 + for c, lab in [(2, r"$2$ (BSGS, kangaroo)"), (math.sqrt(math.pi / 2), r"$\sqrt{\pi/2}$ (rho)"), (math.sqrt(math.pi / 4), r"$\sqrt{\pi/4}$ (negation)")]:
133 + ax.axhline(c, color="gray", lw=0.6, ls="--"); ax.text(bits[-1] + 0.3, c, lab, fontsize=6, va="center")
134 + ax.set_xlabel("group size (bits)"); ax.set_ylabel(r"group operations / $\sqrt{n}$"); ax.set_yscale("log"); ax.legend(fontsize=6, ncol=2)
135 + fig.tight_layout(); fig.savefig(F / "ecdlp_constants.pdf"); fig.savefig(PF / "ecdlp_constants.pdf"); plt.close(fig)
136 + # table
137 + rows = [r"\begin{tabular}{@{}llrrrrrr@{}}", r"\toprule Algorithm & Bits & Inst. & Group ops (mean) & 95\% CI & ops$/\sqrt n$ & Theory & Wall (s) \\ \midrule"]
138 + for cfg, per in S.items():
139 + for i, (b, v) in enumerate(sorted(per.items())):
140 + if b not in (20, 28, 36, 44, 48, 52, 56):
141 + continue
142 + th = THEORY[cfg](v["n_mean"]) / math.sqrt(v["n_mean"])
143 + rows.append(f"{LABELS.get(cfg, cfg) if i == 0 else ''} & {b} & {v['count']} & {v['ops_mean']:,.0f} & [{v['ops_ci'][0]:,.0f}, {v['ops_ci'][1]:,.0f}] & {v['ops_over_sqrt_n']:.2f} & {th:.2f} & {v['wall_mean']:.3f} \\\\".replace(",", r"\,"))
144 + rows += [r"\bottomrule", r"\end{tabular}"]
145 + (T / "ecdlp_scaling.tex").write_text("\\resizebox{\\textwidth}{!}{%\n" + "\n".join(rows) + "}\n")
146 + rows = [r"\begin{tabular}{@{}lrrrrrr@{}}", r"\toprule Algorithm & $\hat\alpha$ (all sizes, ops) & 95\% CI & $R^2$ & $\hat\alpha$ ($\ge 32$ bits, ops) & 95\% CI & $\hat\alpha$ ($\ge 32$ bits, iterations) \\ \midrule"]
147 + for cfg, ff in fits.items():
148 + p, pg, pi = ff["power"], ff["power_ge32"], ff["power_iter_ge32"]
149 + g = f"{pg['alpha']:.3f} & [{pg['alpha_ci'][0]:.3f}, {pg['alpha_ci'][1]:.3f}]" if pg else "--- & ---"
150 + it = f"{pi['alpha']:.3f} [{pi['alpha_ci'][0]:.3f}, {pi['alpha_ci'][1]:.3f}]" if pi else "---"
151 + rows.append(f"{LABELS.get(cfg, cfg)} & {p['alpha']:.3f} & [{p['alpha_ci'][0]:.3f}, {p['alpha_ci'][1]:.3f}] & {p['r2']:.4f} & {g} & {it} \\\\")
152 + rows += [r"\bottomrule", r"\end{tabular}"]
153 + (T / "ecdlp_fits.tex").write_text("\\resizebox{\\textwidth}{!}{%\n" + "\n".join(rows) + "}\n")
154 + for cfg, ff in fits.items():
155 + k = cfg.replace('_', '').replace('128', 'big')
156 + MACROS[f"alpha{k}"] = f"{ff['power']['alpha']:.3f}"
157 + MACROS[f"alphaLo{k}"] = f"{ff['power']['alpha_ci'][0]:.3f}"; MACROS[f"alphaHi{k}"] = f"{ff['power']['alpha_ci'][1]:.3f}"
158 + if ff["power_ge32"]:
159 + MACROS[f"alphaG{k}"] = f"{ff['power_ge32']['alpha']:.3f}"; MACROS[f"alphaGLo{k}"] = f"{ff['power_ge32']['alpha_ci'][0]:.3f}"; MACROS[f"alphaGHi{k}"] = f"{ff['power_ge32']['alpha_ci'][1]:.3f}"
160 + alphas = [ff["power_ge32"]["alpha"] for ff in fits.values() if ff["power_ge32"]]
161 + MACROS["alphaGMin"] = f"{min(alphas):.3f}"; MACROS["alphaGMax"] = f"{max(alphas):.3f}"
162 + big = max(S["rho_dp"]); MACROS["scalingMaxBits"] = str(big); MACROS["scalingInstances"] = str(S["rho_dp"][big]["count"])
163 + MACROS["rhoDpConstBig"] = f"{S['rho_dp'][big]['ops_over_sqrt_n']:.2f}"; MACROS["rhoDpIterConstBig"] = f"{S['rho_dp'][big]['iter_over_sqrt_n']:.2f}"
164 + MACROS["rhoNegIterConstBig"] = f"{S['rho_dp_neg'][big]['iter_over_sqrt_n']:.2f}"
165 + MACROS["bsgsConstBig"] = f"{S['bsgs'][max(S['bsgs'])]['ops_over_sqrt_n']:.2f}"; MACROS["kangarooConstBig"] = f"{S['kangaroo'][max(S['kangaroo'])]['ops_over_sqrt_n']:.2f}"
166 + MACROS["floydConstBig"] = f"{S['rho_floyd'][max(S['rho_floyd'])]['ops_over_sqrt_n']:.2f}"; MACROS["brentConstBig"] = f"{S['rho_brent'][max(S['rho_brent'])]['ops_over_sqrt_n']:.2f}"
167 + MACROS["scalingAllOk"] = "all" if all(v["ok"] for per in S.values() for v in per.values()) else "NOT ALL"
168 + MACROS["scalingHost"] = d["host"]["cpu"]
169 + # speedup neg vs plain (paired by instance)
170 + sp = {}
171 + for b in sorted(S["rho_dp_neg"]):
172 + a = [r["iterations"] for r in runs if r["config"] == "rho_dp_r128" and r["bits"] == b]
173 + c = [r["iterations"] for r in runs if r["config"] == "rho_dp_neg" and r["bits"] == b]
174 + sp[b] = boot_ratio(a, c)
175 + ANALYSIS["scaling_generic"]["neg_speedup_iterations"] = sp
176 + MACROS["negSpeedupBig"] = f"{sp[big][0]:.2f}"; MACROS["negSpeedupBigLo"] = f"{sp[big][1]:.2f}"; MACROS["negSpeedupBigHi"] = f"{sp[big][2]:.2f}"
177 + PB = 44
178 + allsp = [r["iterations"] for r in runs if r["config"] == "rho_dp_r128" and r["bits"] >= PB]; allc = [r["iterations"] for r in runs if r["config"] == "rho_dp_neg" and r["bits"] >= PB]
179 + pooled = boot_ratio(np.array(allsp) / np.array([math.sqrt(r["n"]) for r in runs if r["config"] == "rho_dp_r128" and r["bits"] >= PB]),
180 + np.array(allc) / np.array([math.sqrt(r["n"]) for r in runs if r["config"] == "rho_dp_neg" and r["bits"] >= PB]))
181 + MACROS["pooledMinBits"] = str(PB); MACROS["pooledN"] = str(len(allsp))
182 + # plain rho constant pooled over ≥ PB bits, with CI, against √(π/2)
183 + c = [r["iterations"] / math.sqrt(r["n"]) for r in runs if r["config"] in ("rho_dp", "rho_dp_r128") and r["bits"] >= PB]
184 + m_, lo_, hi_ = boot_mean(c); MACROS["rhoConstPooled"] = f"{m_:.3f}"; MACROS["rhoConstPooledLo"] = f"{lo_:.3f}"; MACROS["rhoConstPooledHi"] = f"{hi_:.3f}"; MACROS["rhoConstPooledN"] = str(len(c))
185 + c = [r["iterations"] / math.sqrt(r["n"]) for r in runs if r["config"] == "rho_dp_neg" and r["bits"] >= PB]
186 + m_, lo_, hi_ = boot_mean(c); MACROS["negConstPooled"] = f"{m_:.3f}"; MACROS["negConstPooledLo"] = f"{lo_:.3f}"; MACROS["negConstPooledHi"] = f"{hi_:.3f}"
187 + c = [r["group_ops"] / math.sqrt(r["n"]) for r in runs if r["config"] == "bsgs" and r["bits"] >= 36]
188 + m_, lo_, hi_ = boot_mean(c); MACROS["bsgsConstPooled"] = f"{m_:.3f}"; MACROS["bsgsConstPooledLo"] = f"{lo_:.3f}"; MACROS["bsgsConstPooledHi"] = f"{hi_:.3f}"
189 + c = [r["group_ops"] / math.sqrt(r["n"]) for r in runs if r["config"] == "kangaroo" and r["bits"] >= PB]
190 + m_, lo_, hi_ = boot_mean(c); MACROS["kangConstPooled"] = f"{m_:.3f}"; MACROS["kangConstPooledLo"] = f"{lo_:.3f}"; MACROS["kangConstPooledHi"] = f"{hi_:.3f}"
191 + ANALYSIS["scaling_generic"]["pooled_constants"] = {k: v for k, v in MACROS.items() if "Pooled" in k}
192 + MACROS["negSpeedupPooled"] = f"{pooled[0]:.2f}"; MACROS["negSpeedupPooledLo"] = f"{pooled[1]:.2f}"; MACROS["negSpeedupPooledHi"] = f"{pooled[2]:.2f}"
193 + ANALYSIS["scaling_generic"]["neg_speedup_pooled_ge36"] = pooled
194 +
195 +
196 +def analyse_j0():
197 + d, runs = load("scaling_j0")
198 + if not runs:
199 + return
200 + S = summarise(runs); ANALYSIS["scaling_j0"] = {"host": d["host"], "summary": S}
201 + base = "rho_dp"
202 + ratios = {}
203 + for cfg in ("rho_dp_neg", "rho_dp_aut3", "rho_dp_neg_aut3", "rho_dp_neg_aut3_r20"):
204 + ratios[cfg] = {}
205 + for b in sorted(S[cfg]):
206 + a = [r["iterations"] / math.sqrt(r["n"]) for r in runs if r["config"] == base and r["bits"] == b]
207 + c = [r["iterations"] / math.sqrt(r["n"]) for r in runs if r["config"] == cfg and r["bits"] == b]
208 + ratios[cfg][b] = boot_ratio(a, c)
209 + PB = 44
210 + a = [r["iterations"] / math.sqrt(r["n"]) for r in runs if r["config"] == base and r["bits"] >= PB]
211 + c = [r["iterations"] / math.sqrt(r["n"]) for r in runs if r["config"] == cfg and r["bits"] >= PB]
212 + ratios[cfg]["pooled_ge36"] = boot_ratio(a, c) # key kept for compatibility; pooled over bits ≥ PB
213 + ratios[cfg]["pooled_min_bits"] = PB; ratios[cfg]["pooled_n"] = len(a)
214 + m_, lo_, hi_ = boot_mean(c); ratios[cfg]["const_pooled"] = [m_, lo_, hi_]
215 + ratios[cfg]["cpu_ns_per_iter_ge40"] = float(np.mean([1e9 * r["cpu_s"] / max(r["iterations"], 1) for r in runs if r["config"] == cfg and r["bits"] >= 40]))
216 + # wall-clock ratio (what the user actually gains)
217 + a = [r["wall_s"] for r in runs if r["config"] == base and r["bits"] >= 40]; c = [r["wall_s"] for r in runs if r["config"] == cfg and r["bits"] >= 40]
218 + ratios[cfg]["wall_pooled_ge40"] = boot_ratio(a, c)
219 + a = [r["group_ops"] / math.sqrt(r["n"]) for r in runs if r["config"] == base and r["bits"] >= PB]; c = [r["group_ops"] / math.sqrt(r["n"]) for r in runs if r["config"] == cfg and r["bits"] >= PB]
220 + ratios[cfg]["ops_pooled_ge36"] = boot_ratio(a, c)
221 + a = [r["iterations"] / math.sqrt(r["n"]) for r in runs if r["config"] == base and r["bits"] >= PB]
222 + m_, lo_, hi_ = boot_mean(a); ratios["_plain_const_pooled"] = [m_, lo_, hi_]
223 + ratios["_plain_cpu_ns_per_iter_ge40"] = float(np.mean([1e9 * r["cpu_s"] / max(r["iterations"], 1) for r in runs if r["config"] == base and r["bits"] >= 40]))
224 + ANALYSIS["scaling_j0"]["iteration_speedup_vs_plain"] = ratios
225 + fig, axes = plt.subplots(1, 2, figsize=(7.2, 3.2))
226 + for cfg, lab, th in [("rho_dp_neg", "negation", math.sqrt(2)), ("rho_dp_aut3", r"$\phi$-orbits", math.sqrt(3)), ("rho_dp_neg_aut3", r"negation + $\phi$ ($r=128$)", math.sqrt(6)), ("rho_dp_neg_aut3_r20", r"negation + $\phi$ ($r=20$)", math.sqrt(6))]:
227 + bits = sorted(b for b in ratios[cfg] if isinstance(b, int))
228 + m = [ratios[cfg][b][0] for b in bits]; lo = [ratios[cfg][b][0] - ratios[cfg][b][1] for b in bits]; hi = [ratios[cfg][b][2] - ratios[cfg][b][0] for b in bits]
229 + l = axes[0].errorbar(bits, m, yerr=[lo, hi], marker="o", ms=3, lw=1, capsize=2, label=lab)
230 + axes[0].axhline(th, color=l[0].get_color(), lw=0.6, ls="--")
231 + axes[0].set_xlabel("group size (bits)"); axes[0].set_ylabel("iteration speed-up vs plain rho (paired, 95% CI)"); axes[0].legend(fontsize=6)
232 + axes[0].set_title(r"dashed: $\sqrt{2}$, $\sqrt{3}$, $\sqrt{6}$", fontsize=8)
233 + for cfg in ("rho_dp_neg", "rho_dp_aut3", "rho_dp_neg_aut3", "rho_dp_neg_aut3_r20"):
234 + bits = sorted(S[cfg]); axes[1].plot(bits, [S[cfg][b]["fruitless_mean"] / max(S[cfg][b]["iter_mean"], 1) * 1e3 for b in bits], marker="o", ms=3, lw=1, label=LABELS.get(cfg, cfg))
235 + axes[1].set_xlabel("group size (bits)"); axes[1].set_ylabel("fruitless cycles per 1000 iterations"); axes[1].legend(fontsize=6)
236 + fig.tight_layout(); fig.savefig(F / "ecdlp_endomorphism.pdf"); fig.savefig(PF / "ecdlp_endomorphism.pdf"); plt.close(fig)
237 + rows = [r"\begin{tabular}{@{}lrrrrr@{}}", r"\toprule Classes & Theory & Iterations$/\sqrt n$ (95\% CI) & Iteration speed-up (paired, 95\% CI) & ns per iteration & Wall-clock speed-up \\ \midrule",
238 + f"none (plain rho, $r=128$) & $\\sqrt{{\\pi/2}} = 1.253$ & {ratios['_plain_const_pooled'][0]:.3f} [{ratios['_plain_const_pooled'][1]:.3f}, {ratios['_plain_const_pooled'][2]:.3f}] & 1 & {ratios['_plain_cpu_ns_per_iter_ge40']:.0f} & 1 \\\\"]
239 + for cfg, lab, th in [("rho_dp_neg", r"$\{\pm P\}$ (negation)", r"$\sqrt2 = 1.414$"), ("rho_dp_aut3", r"$\{P,\phi P,\phi^2P\}$", r"$\sqrt3 = 1.732$"), ("rho_dp_neg_aut3", r"order-6 orbits, $r=128$", r"$\sqrt6 = 2.449$"), ("rho_dp_neg_aut3_r20", r"order-6 orbits, $r=20$", r"$\sqrt6 = 2.449$")]:
240 + p, o, w, cp = ratios[cfg]["pooled_ge36"], ratios[cfg]["ops_pooled_ge36"], ratios[cfg]["wall_pooled_ge40"], ratios[cfg]["const_pooled"]
241 + rows.append(f"{lab} & {th} & {cp[0]:.3f} [{cp[1]:.3f}, {cp[2]:.3f}] & {p[0]:.3f} [{p[1]:.3f}, {p[2]:.3f}] & {ratios[cfg]['cpu_ns_per_iter_ge40']:.0f} & {w[0]:.2f} [{w[1]:.2f}, {w[2]:.2f}] \\\\")
242 + rows += [r"\bottomrule", r"\end{tabular}"]
243 + (T / "ecdlp_j0.tex").write_text("\\resizebox{\\textwidth}{!}{%\n" + "\n".join(rows) + "}\n")
244 + for cfg, key in [("rho_dp_neg", "Neg"), ("rho_dp_aut3", "Aut"), ("rho_dp_neg_aut3", "NegAut"), ("rho_dp_neg_aut3_r20", "NegAutRtwenty")]:
245 + p = ratios[cfg]["pooled_ge36"]; MACROS[f"jzero{key}Speedup"] = f"{p[0]:.2f}"; MACROS[f"jzero{key}SpeedupLo"] = f"{p[1]:.2f}"; MACROS[f"jzero{key}SpeedupHi"] = f"{p[2]:.2f}"
246 + w = ratios[cfg]["wall_pooled_ge40"]; MACROS[f"jzero{key}Wall"] = f"{w[0]:.2f}"; MACROS[f"jzero{key}WallLo"] = f"{w[1]:.2f}"; MACROS[f"jzero{key}WallHi"] = f"{w[2]:.2f}"
247 + MACROS[f"jzero{key}NsIter"] = f"{ratios[cfg]['cpu_ns_per_iter_ge40']:.0f}"; MACROS[f"jzero{key}Const"] = f"{ratios[cfg]['const_pooled'][0]:.3f}"
248 + MACROS["jzeroPlainNsIter"] = f"{ratios['_plain_cpu_ns_per_iter_ge40']:.0f}"; MACROS["jzeroPlainConst"] = f"{ratios['_plain_const_pooled'][0]:.3f}"
249 + MACROS["jzeroPooledN"] = str(ratios["rho_dp_neg"]["pooled_n"]); MACROS["jzeroPooledMinBits"] = str(ratios["rho_dp_neg"]["pooled_min_bits"])
250 + MACROS["jzeroMaxBits"] = str(max(S["rho_dp"])); MACROS["jzeroHost"] = d["host"]["cpu"]
251 + MACROS["jzeroAllOk"] = "all" if all(v["ok"] for per in S.values() for v in per.values()) else "NOT ALL"
252 + b = max(S["rho_dp_neg_aut3"]); MACROS["jzeroFruitlessPerK"] = f"{S['rho_dp_neg_aut3'][b]['fruitless_mean'] / max(S['rho_dp_neg_aut3'][b]['iter_mean'], 1) * 1e3:.2f}"
253 + MACROS["jzeroFruitlessPerKRtwenty"] = f"{S['rho_dp_neg_aut3_r20'][b]['fruitless_mean'] / max(S['rho_dp_neg_aut3_r20'][b]['iter_mean'], 1) * 1e3:.2f}"
254 +
255 +
256 +def analyse_sweep():
257 + d, runs = load("sweep_r")
258 + if not runs:
259 + return
260 + res = {}
261 + for bits in sorted({r["bits"] for r in runs}):
262 + res[bits] = {}
263 + for cfg in sorted({r["config"] for r in runs}):
264 + rs = [r for r in runs if r["bits"] == bits and r["config"] == cfg]
265 + if not rs:
266 + continue
267 + x = [r["iterations"] / math.sqrt(r["n"]) for r in rs]; m, lo, hi = boot_mean(x)
268 + res[bits][cfg] = {"iter_over_sqrt_n": m, "ci": [lo, hi], "count": len(rs), "ops_over_sqrt_n": float(np.mean([r["group_ops"] / math.sqrt(r["n"]) for r in rs]))}
269 + ANALYSIS["sweep_r"] = {"host": d["host"], "results": res}
270 + fig, ax = plt.subplots(figsize=(6.4, 3.2))
271 + rs_ = [2, 3, 4, 8, 16, 20, 32, 64, 128]
272 + for bits, mk in zip(sorted(res), ("o", "s")):
273 + for mixed, ls in ((0, "-"), (2, "--"), (4, ":")):
274 + xs, ys, lo, hi = [], [], [], []
275 + for r in rs_:
276 + k = f"r{r}_m{mixed}"
277 + if k in res[bits]:
278 + xs.append(r); ys.append(res[bits][k]["iter_over_sqrt_n"]); lo.append(ys[-1] - res[bits][k]["ci"][0]); hi.append(res[bits][k]["ci"][1] - ys[-1])
279 + if xs:
280 + ax.errorbar(xs, ys, yerr=[lo, hi], marker=mk, ms=3, lw=1, ls=ls, capsize=2, label=f"{bits} bits, mixed={mixed or 'none'}")
281 + ax.axhline(math.sqrt(math.pi / 2), color="k", lw=0.8, ls="--"); ax.text(2.1, math.sqrt(math.pi / 2) * 1.02, r"random mapping $\sqrt{\pi/2}$", fontsize=7)
282 + ax.set_xscale("log", base=2); ax.set_xlabel("number of partitions $r$ of the adding walk"); ax.set_ylabel(r"iterations / $\sqrt{n}$ (mean, 95% CI)"); ax.legend(fontsize=6, ncol=2)
283 + fig.tight_layout(); fig.savefig(F / "ecdlp_sweep_r.pdf"); fig.savefig(PF / "ecdlp_sweep_r.pdf"); plt.close(fig)
284 + rows = [r"\begin{tabular}{@{}lrrrrrr@{}}", r"\toprule $r$ & \multicolumn{3}{c}{36 bits: iterations$/\sqrt n$} & \multicolumn{3}{c}{40 bits: iterations$/\sqrt n$} \\", r" & plain & mixed 2 & mixed 4 & plain & mixed 2 & mixed 4 \\ \midrule"]
285 + for r in rs_:
286 + cells = []
287 + for bits in (36, 40):
288 + for mixed in (0, 2, 4):
289 + k = f"r{r}_m{mixed}"; v = res.get(bits, {}).get(k)
290 + cells.append(f"{v['iter_over_sqrt_n']:.3f}" if v else "---")
291 + rows.append(f"{r} & " + " & ".join(cells) + r" \\")
292 + rows += [r"\midrule", r"random-mapping constant $\sqrt{\pi/2}$ & 1.253 & & & 1.253 & & \\", r"\bottomrule", r"\end{tabular}"]
293 + (T / "ecdlp_sweep_r.tex").write_text("\\resizebox{\\textwidth}{!}{%\n" + "\n".join(rows) + "}\n")
294 + best = min(((bits, cfg, v["iter_over_sqrt_n"], v["ci"]) for bits in res for cfg, v in res[bits].items() if cfg != "r2_m0"), key=lambda t: t[2])
295 + MACROS["sweepBestCfg"] = best[1].replace("_", r"\_"); MACROS["sweepBestConst"] = f"{best[2]:.3f}"; MACROS["sweepBestLo"] = f"{best[3][0]:.3f}"; MACROS["sweepBestHi"] = f"{best[3][1]:.3f}"
296 + MACROS["sweepRtwo"] = f"{res[40]['r2_m0']['iter_over_sqrt_n']:.2f}" if "r2_m0" in res.get(40, {}) else "n/a"
297 + MACROS["sweepRthree"] = f"{res[40]['r3_m0']['iter_over_sqrt_n']:.2f}" if "r3_m0" in res.get(40, {}) else "n/a"
298 + MACROS["sweepRtwenty"] = f"{res[40]['r20_m0']['iter_over_sqrt_n']:.3f}"; MACROS["sweepRhundred"] = f"{res[40]['r128_m0']['iter_over_sqrt_n']:.3f}"
299 + MACROS["sweepConfigs"] = str(len({r["config"] for r in runs})); MACROS["sweepInstances"] = str(len({r["inst"] for r in runs}))
300 +
301 +
302 +def analyse_parallel():
303 + d, runs = load("parallel")
304 + if not runs:
305 + return
306 + res = {}
307 + for t in sorted({r["threads"] for r in runs}):
308 + rs = [r for r in runs if r["threads"] == t]
309 + res[t] = {"wall_mean": float(np.mean([r["wall_s"] for r in rs])), "ops_mean": float(np.mean([r["group_ops"] for r in rs])),
310 + "ops_per_wall_s": float(np.mean([r["group_ops"] / r["wall_s"] for r in rs])), "count": len(rs), "cpu_mean": float(np.mean([r["cpu_s"] for r in rs]))}
311 + base = res[1]
312 + for t, v in res.items():
313 + v["speedup_throughput"] = v["ops_per_wall_s"] / base["ops_per_wall_s"]; v["efficiency"] = v["speedup_throughput"] / t
314 + v["ops_ratio_to_1thread"] = v["ops_mean"] / base["ops_mean"]
315 + ANALYSIS["parallel"] = {"host": d["host"], "results": res}
316 + fig, axes = plt.subplots(1, 2, figsize=(7.0, 3.0))
317 + ts = sorted(res); axes[0].plot(ts, [res[t]["speedup_throughput"] for t in ts], marker="o", ms=3, label="measured throughput speed-up"); axes[0].plot(ts, ts, "k--", lw=0.8, label="linear")
318 + axes[0].set_xlabel("threads"); axes[0].set_ylabel("throughput speed-up vs 1 thread"); axes[0].legend(fontsize=7)
319 + axes[1].plot(ts, [res[t]["ops_ratio_to_1thread"] for t in ts], marker="s", ms=3, color="C3"); axes[1].axhline(1, color="k", lw=0.8, ls="--")
320 + axes[1].set_xlabel("threads"); axes[1].set_ylabel("total group ops, relative to 1 thread")
321 + fig.tight_layout(); fig.savefig(F / "ecdlp_parallel.pdf"); fig.savefig(PF / "ecdlp_parallel.pdf"); plt.close(fig)
322 + rows = [r"\begin{tabular}{@{}rrrrrr@{}}", r"\toprule Threads & Wall (s) & Group ops ($\times 10^6$) & Mops/s & Speed-up & Efficiency \\ \midrule"]
323 + for t in ts:
324 + v = res[t]; rows.append(f"{t} & {v['wall_mean']:.2f} & {v['ops_mean']/1e6:.1f} & {v['ops_per_wall_s']/1e6:.0f} & {v['speedup_throughput']:.2f} & {v['efficiency']:.2f} \\\\")
325 + rows += [r"\bottomrule", r"\end{tabular}"]
326 + (T / "ecdlp_parallel.tex").write_text("\\resizebox{\\textwidth}{!}{%\n" + "\n".join(rows) + "}\n")
327 + tmax = max(ts); MACROS["parMaxThreads"] = str(tmax); MACROS["parSpeedupMax"] = f"{res[tmax]['speedup_throughput']:.1f}"; MACROS["parEffMax"] = f"{res[tmax]['efficiency']:.2f}"
328 + t16 = 16 if 16 in res else tmax; MACROS["parSpeedupSixteen"] = f"{res[t16]['speedup_throughput']:.1f}"; MACROS["parEffSixteen"] = f"{res[t16]['efficiency']:.2f}"
329 + MACROS["parOpsRatioMax"] = f"{res[tmax]['ops_ratio_to_1thread']:.2f}"; MACROS["parHost"] = d["host"]["cpu"]; MACROS["parCores"] = str(d["host"]["cores"])
330 +
331 +
332 +def analyse_tmto():
333 + d, runs = load("tmto")
334 + if not runs:
335 + return
336 + bs = {}
337 + for r in runs:
338 + if r["config"].startswith("bsgs_m"):
339 + m = int(r["config"][6:]); bs.setdefault(m, []).append(r)
340 + rh = {}
341 + for r in runs:
342 + if r["config"].startswith("rho_dp"):
343 + dpb = int(r["config"][6:]); rh.setdefault(dpb, []).append(r)
344 + B = {m: {"ops_mean": float(np.mean([r["group_ops"] for r in rs])), "table_entries": float(np.mean([r["table_entries"] for r in rs])), "table_mb": float(np.mean([r.get("table_bytes", 0) for r in rs])) / 2**20,
345 + "rss_mb": float(np.mean([r["peak_rss_bytes"] for r in rs])) / 2**20, "wall": float(np.mean([r["wall_s"] for r in rs])), "n": float(np.mean([r["n"] for r in rs])), "pred": float(np.mean([m + r["n"] / (2 * m) for r in rs]))} for m, rs in bs.items()}
346 + Rr = {dpb: {"ops_mean": float(np.mean([r["group_ops"] for r in rs])), "ops_ci": boot_mean([r["group_ops"] for r in rs])[1:], "dp_count": float(np.mean([r["dp_count"] for r in rs])), "dp_mb": float(np.mean([r.get("dp_bytes", 0) for r in rs])) / 2**20,
347 + "rss_mb": float(np.mean([r["peak_rss_bytes"] for r in rs])) / 2**20, "wall": float(np.mean([r["wall_s"] for r in rs])), "n": float(np.mean([r["n"] for r in rs])), "iter": float(np.mean([r["iterations"] for r in rs]))} for dpb, rs in rh.items()}
348 + ANALYSIS["tmto"] = {"host": d["host"], "bsgs": B, "rho": Rr}
349 + fig, axes = plt.subplots(1, 2, figsize=(7.2, 3.2))
350 + ms = sorted(B); axes[0].plot([B[m]["table_entries"] for m in ms], [B[m]["ops_mean"] for m in ms], "o-", ms=3, label="BSGS measured (36 bits)")
351 + axes[0].plot([B[m]["table_entries"] for m in ms], [B[m]["pred"] for m in ms], "k--", lw=0.8, label=r"$m + n/(2m)$ (table keyed by $x$: $\pm j$ resolved)")
352 + axes[0].set_xscale("log", base=2); axes[0].set_yscale("log"); axes[0].set_xlabel("table entries (memory)"); axes[0].set_ylabel("group operations"); axes[0].legend(fontsize=7)
353 + ds = sorted(Rr); axes[1].errorbar([max(Rr[k]["dp_count"], 1) for k in ds], [Rr[k]["ops_mean"] for k in ds], yerr=[[Rr[k]["ops_mean"] - Rr[k]["ops_ci"][0] for k in ds], [Rr[k]["ops_ci"][1] - Rr[k]["ops_mean"] for k in ds]], fmt="s-", ms=3, capsize=2, label="rho, DP (40 bits)")
354 + for k in ds:
355 + axes[1].annotate(f"$2^{{-{k}}}$", (max(Rr[k]["dp_count"], 1), Rr[k]["ops_mean"]), xytext=(3, 3), textcoords="offset points", fontsize=6)
356 + axes[1].axhline(math.sqrt(math.pi * Rr[ds[0]]["n"] / 2), color="k", lw=0.8, ls="--", label=r"$\sqrt{\pi n/2}$")
357 + axes[1].set_xscale("log", base=2); axes[1].set_xlabel("stored distinguished points (memory)"); axes[1].set_ylabel("group operations"); axes[1].legend(fontsize=7)
358 + fig.tight_layout(); fig.savefig(F / "ecdlp_tmto.pdf"); fig.savefig(PF / "ecdlp_tmto.pdf"); plt.close(fig)
359 + rows = [r"\begin{tabular}{@{}lrrrrr@{}}", r"\toprule Algorithm / parameter & Memory (entries) & Memory (MB) & Group ops & Predicted & Wall (s) \\ \midrule"]
360 + for m in ms:
361 + v = B[m]; rows.append(f"BSGS, $m=2^{{{int(math.log2(m))}}}$ (36 bits) & {v['table_entries']:,.0f} & {v['table_mb']:.1f} & {v['ops_mean']:,.0f} & {v['pred']:,.0f} & {v['wall']:.3f} \\\\".replace(",", r"\,"))
362 + rows.append(r"\midrule")
363 + for k in ds:
364 + v = Rr[k]; rows.append(f"rho, DP density $2^{{-{k}}}$ (40 bits) & {v['dp_count']:,.0f} & {v['dp_mb']:.2f} & {v['ops_mean']:,.0f} & {math.sqrt(math.pi * v['n'] / 2):,.0f} & {v['wall']:.3f} \\\\".replace(",", r"\,"))
365 + rows += [r"\bottomrule", r"\end{tabular}"]
366 + (T / "ecdlp_tmto.tex").write_text("\\resizebox{\\textwidth}{!}{%\n" + "\n".join(rows) + "}\n")
367 + MACROS["tmtoBsgsMinM"] = f"2^{{{int(math.log2(min(ms)))}}}"; MACROS["tmtoBsgsMaxM"] = f"2^{{{int(math.log2(max(ms)))}}}"
368 + dev = max(abs(B[m]["ops_mean"] / B[m]["pred"] - 1) for m in ms); MACROS["tmtoBsgsMaxDev"] = f"{100*dev:.1f}"
369 + MACROS["tmtoRhoDpMin"] = str(min(ds)); MACROS["tmtoRhoDpMax"] = str(max(ds))
370 + MACROS["tmtoRhoOpsRange"] = f"{min(Rr[k]['ops_mean'] for k in ds)/1e6:.2f}--{max(Rr[k]['ops_mean'] for k in ds)/1e6:.2f}"
371 +
372 +
373 +def main():
374 + analyse_scaling(); analyse_j0(); analyse_sweep(); analyse_parallel(); analyse_tmto()
375 + (R / "analysis.json").write_text(json.dumps(ANALYSIS, indent=1, default=str))
376 + (T / "numbers_ecdlp.tex").write_text("% generated by experiments/synthetic_ecdlp/analyze_ecdlp.py\n" + "\n".join(f"\\newcommand{{\\{k}}}{{{v}}}" for k, v in MACROS.items()) + "\n")
377 + print("studies analysed:", list(ANALYSIS), "| macros:", len(MACROS))
378 +
379 +
380 +if __name__ == "__main__":
381 + main()
added experiments/synthetic_ecdlp/ecdlp_study.py +128 −0
@@ -0,0 +1,128 @@
1 +"""ECDLP study generator/driver for the Rust engine (Axis A: H04, H08, H09, H10, H11).
2 +
3 +Studies (each writes results/synthetic_ecdlp/<study>_<tag>.jobs.json; `--run` executes locally, otherwise the jobs are
4 +shipped to a cluster node and executed with run_batch_remote.py):
5 + scaling_generic sizes 20..56, 20 random prime-order curves per size, configs bsgs / rho floyd / rho brent / rho dp /
6 + rho dp+neg (r=128) / kangaroo (full interval)
7 + scaling_j0 sizes 24..52 on j=0 curves (p ≡ 1 mod 3), configs rho dp (r=128) / +neg / +aut3 / +neg+aut3
8 + sweep_r 36- and 40-bit generic curves × r ∈ {3,4,8,16,20,32,64,128} × mixed ∈ {0,2,4}, 40 instances
9 + parallel 48-bit generic curves, threads ∈ {1,2,4,8,16,28}, 6 instances
10 + tmto bsgs memory sweep (36 bits) and rho dp-bit sweep (40 bits), 20 instances
11 +Instances are generated with PARI/GP point counting (laptop); jobs are flat objects in the engine's batch format.
12 +"""
13 +import argparse, json, math, random, sys, pathlib
14 +ROOT = pathlib.Path(__file__).resolve().parents[2]
15 +sys.path.insert(0, str(ROOT / "src"))
16 +from wp34.ec import random_prime_order_curve, cube_roots_of_unity, endomorphism_lambda
17 +
18 +OUT = ROOT / "results/synthetic_ecdlp"
19 +
20 +
21 +def gen_instances(sizes, per_size, seed, j0):
22 + rng = random.Random(seed); inst = []
23 + for bits in sizes:
24 + for i in range(per_size):
25 + E = random_prime_order_curve(bits, rng, j0=j0)
26 + d = rng.randrange(1, E.n); Q = E.mul(d, E.G)
27 + rec = {"bits": bits, "idx": i, "p": str(E.p), "a": str(E.a), "b": str(E.b), "n": str(E.n),
28 + "gx": str(E.G[0]), "gy": str(E.G[1]), "qx": str(Q[0]), "qy": str(Q[1]), "expected_d": str(d)}
29 + if j0:
30 + for beta in cube_roots_of_unity(E.p):
31 + try:
32 + lam = endomorphism_lambda(E, beta); rec["beta"] = str(beta); rec["lambda"] = str(lam); break
33 + except ValueError:
34 + continue
35 + assert "lambda" in rec
36 + inst.append(rec)
37 + return inst
38 +
39 +
40 +def base(rec):
41 + return {k: rec[k] for k in ("p", "a", "b", "n", "gx", "gy", "qx", "qy", "expected_d")}
42 +
43 +
44 +def dp_default(bits):
45 + return max(1, bits // 4 - 2)
46 +
47 +
48 +def study_scaling_generic(seed):
49 + sizes = [20, 24, 28, 32, 36, 40, 44, 48, 52, 56]
50 + inst = gen_instances(sizes, 60, seed, False); jobs = []
51 + for rec in inst:
52 + b = base(rec); lab = f"g{rec['bits']}b-{rec['idx']}"; bits = rec["bits"]
53 + if bits <= 48:
54 + jobs.append(dict(b, alg="bsgs", seed=seed, threads=1, label=lab + "/bsgs"))
55 + if bits <= 48:
56 + jobs.append(dict(b, alg="rho", cycle="floyd", r=20, seed=seed, threads=1, label=lab + "/rho_floyd"))
57 + jobs.append(dict(b, alg="rho", cycle="brent", r=20, seed=seed, threads=1, label=lab + "/rho_brent"))
58 + jobs.append(dict(b, alg="rho", cycle="dp", r=20, dp=dp_default(bits), seed=seed, threads=1, label=lab + "/rho_dp"))
59 + jobs.append(dict(b, alg="rho", cycle="dp", r=128, dp=dp_default(bits), seed=seed, threads=1, label=lab + "/rho_dp_r128"))
60 + jobs.append(dict(b, alg="rho", cycle="dp", r=128, dp=dp_default(bits), neg=True, seed=seed, threads=1, label=lab + "/rho_dp_neg"))
61 + jobs.append(dict(b, alg="kangaroo", dp=dp_default(bits), seed=seed, threads=1, label=lab + "/kangaroo"))
62 + return jobs, {"sizes": sizes, "per_size": 60}
63 +
64 +
65 +def study_scaling_j0(seed):
66 + sizes = [24, 28, 32, 36, 40, 44, 48, 52]
67 + inst = gen_instances(sizes, 60, seed + 1, True); jobs = []
68 + for rec in inst:
69 + b = base(rec); lab = f"j{rec['bits']}b-{rec['idx']}"; bits = rec["bits"]; dp = dp_default(bits)
70 + jobs.append(dict(b, alg="rho", cycle="dp", r=128, dp=dp, seed=seed, threads=1, label=lab + "/rho_dp"))
71 + jobs.append(dict(b, alg="rho", cycle="dp", r=128, dp=dp, neg=True, seed=seed, threads=1, label=lab + "/rho_dp_neg"))
72 + jobs.append(dict(b, alg="rho", cycle="dp", r=128, dp=dp, aut3=True, beta=rec["beta"], seed=seed, threads=1, label=lab + "/rho_dp_aut3", **{"lambda": rec["lambda"]}))
73 + jobs.append(dict(b, alg="rho", cycle="dp", r=128, dp=dp, neg=True, aut3=True, beta=rec["beta"], seed=seed, threads=1, label=lab + "/rho_dp_neg_aut3", **{"lambda": rec["lambda"]}))
74 + jobs.append(dict(b, alg="rho", cycle="dp", r=20, dp=dp, neg=True, aut3=True, beta=rec["beta"], seed=seed, threads=1, label=lab + "/rho_dp_neg_aut3_r20", **{"lambda": rec["lambda"]}))
75 + return jobs, {"sizes": sizes, "per_size": 60}
76 +
77 +
78 +def study_sweep_r(seed):
79 + inst = gen_instances([36, 40], 40, seed + 2, False); jobs = []
80 + for rec in inst:
81 + b = base(rec); lab = f"g{rec['bits']}b-{rec['idx']}"; dp = dp_default(rec["bits"])
82 + for r in (3, 4, 8, 16, 20, 32, 64, 128):
83 + for mixed in (0, 2, 4):
84 + kw = {"mixed": mixed} if mixed else {}
85 + jobs.append(dict(b, alg="rho", cycle="dp", r=r, dp=dp, seed=seed, threads=1, label=f"{lab}/r{r}_m{mixed}", **kw))
86 + # negative control: a bad walk — r = 2 (far from random), and Pollard's original 3-partition analogue r=3 is above
87 + jobs.append(dict(b, alg="rho", cycle="dp", r=2, dp=dp, seed=seed, threads=1, label=f"{lab}/r2_m0"))
88 + return jobs, {"sizes": [36, 40], "per_size": 40, "r": [2, 3, 4, 8, 16, 20, 32, 64, 128], "mixed": [0, 2, 4]}
89 +
90 +
91 +def study_parallel(seed):
92 + inst = gen_instances([56], 12, seed + 3, False); jobs = []
93 + for rec in inst:
94 + b = base(rec); lab = f"g56b-{rec['idx']}"
95 + for t in (1, 2, 4, 8, 16, 28):
96 + jobs.append(dict(b, alg="rho", cycle="dp", r=128, dp=12, neg=True, seed=seed, threads=t, label=f"{lab}/t{t}"))
97 + return jobs, {"sizes": [56], "per_size": 12, "threads": [1, 2, 4, 8, 16, 28]}
98 +
99 +
100 +def study_tmto(seed):
101 + jobs = []
102 + for rec in gen_instances([36], 20, seed + 4, False):
103 + b = base(rec); lab = f"g36b-{rec['idx']}"
104 + for mem in (2**10, 2**12, 2**14, 2**16, 2**18, 2**20):
105 + jobs.append(dict(b, alg="bsgs", memory=str(mem), seed=seed, threads=1, label=f"{lab}/bsgs_m{mem}"))
106 + for rec in gen_instances([40], 20, seed + 5, False):
107 + b = base(rec); lab = f"g40b-{rec['idx']}"
108 + for dp in (2, 4, 6, 8, 10, 12, 14):
109 + jobs.append(dict(b, alg="rho", cycle="dp", r=128, dp=dp, seed=seed, threads=1, label=f"{lab}/rho_dp{dp}"))
110 + return jobs, {"bsgs_bits": 36, "rho_bits": 40, "per_size": 20}
111 +
112 +
113 +STUDIES = {"scaling_generic": study_scaling_generic, "scaling_j0": study_scaling_j0, "sweep_r": study_sweep_r,
114 + "parallel": study_parallel, "tmto": study_tmto}
115 +
116 +
117 +def main():
118 + ap = argparse.ArgumentParser(); ap.add_argument("study", choices=list(STUDIES) + ["all"]); ap.add_argument("--seed", type=int, default=34)
119 + a = ap.parse_args(); OUT.mkdir(parents=True, exist_ok=True)
120 + for name in (STUDIES if a.study == "all" else [a.study]):
121 + jobs, params = STUDIES[name](a.seed)
122 + (OUT / f"{name}.jobs.json").write_text(json.dumps(jobs))
123 + (OUT / f"{name}.params.json").write_text(json.dumps(dict(params, seed=a.seed, study=name)))
124 + print(f"{name}: {len(jobs)} jobs → {OUT / (name + '.jobs.json')}")
125 +
126 +
127 +if __name__ == "__main__":
128 + main()
added experiments/synthetic_ecdlp/index_calculus_probe.py +72 −0
@@ -0,0 +1,72 @@
1 +"""Index-calculus applicability probe on prime-field curves (Axis A, hypothesis on Semaev decomposition).
2 +
3 +Question: with a factor base F = {P ∈ E(F_p) : x(P) < B}, what fraction of random points R decomposes as R = P1 + P2
4 +(P_i ∈ F), and what is the implied cost of a relation versus the generic √n cost?
5 +Theory (Semaev 2004; Gaudry 2009; Diem 2011): over a prime field there is no subfield structure; a random point is a
6 +sum of m factor-base points with probability ≈ |F|^m / (m!·n), so with |F| = n^{1/m}·c the relation-collection cost
7 +is ≈ m!·n^{1−1/m}·(cost of decomposition), never below n^{1/2} for m = 2 and worse for m ≥ 3 once the decomposition
8 +cost (solving S_{m+1} = 0) is included. We measure the m = 2 decomposition rate empirically on 16–28-bit curves.
9 +"""
10 +import argparse, math, random, sys, time, pathlib, statistics as st
11 +ROOT = pathlib.Path(__file__).resolve().parents[2]
12 +sys.path.insert(0, str(ROOT / "src"))
13 +from wp34.ec import random_prime_order_curve, sqrt_mod
14 +from wp34.provenance import record
15 +
16 +
17 +def run(bits, frac_exp, trials, rng):
18 + E = random_prime_order_curve(bits, rng)
19 + n, p = E.n, E.p
20 + B = int(p ** frac_exp) # factor base: x < B, |F| ≈ B (two points per x on average… half the x are on-curve ⇒ ≈ B)
21 + # enumerate the factor base (points with x < B); store by x for O(1) membership
22 + F = {}
23 + for x in range(B):
24 + rhs = (x * x * x + E.a * x + E.b) % p
25 + y = sqrt_mod(rhs, p)
26 + if y is not None:
27 + F[x] = y
28 + size = sum(1 if y == 0 else 2 for y in F.values())
29 + hits = 0
30 + dec_cost = 0
31 + for _ in range(trials):
32 + R = E.random_point(rng)
33 + # R = P1 + P2 ⇔ R − P1 ∈ F for some P1 ∈ F: try all P1 (|F| subtractions) — the naive decomposition
34 + found = False
35 + for x, y in F.items():
36 + for s in (y, p - y) if y else (0,):
37 + dec_cost += 1
38 + S = E.sub(R, (x, s))
39 + if S is None or S[0] < B:
40 + found = True
41 + break
42 + if found:
43 + break
44 + hits += found
45 + rate = hits / trials
46 + pred = 1 - math.exp(-size * size / (2 * n)) # m = 2 prediction: P[R ∈ F + F] ≈ 1 − exp(−|F|²/(2n))
47 + return {"bits": bits, "p": str(p), "n": str(n), "B": B, "factor_base_size": size, "trials": trials, "hits": hits,
48 + "decomposition_rate": rate, "predicted_rate": pred,
49 + "relations_needed": size, "expected_cost_group_ops": (size / rate) * size if rate else None,
50 + "sqrt_n": math.sqrt(n), "cost_over_sqrt_n": ((size / rate) * size / math.sqrt(n)) if rate else None,
51 + # even with a FREE decomposition oracle: |F| relations at 1/rate trials each, then sparse linear algebra ~ |F|²
52 + "oracle_relation_cost": (size / rate) if rate else None,
53 + "oracle_relation_cost_over_sqrt_n": (size / rate / math.sqrt(n)) if rate else None,
54 + "linear_algebra_cost_over_sqrt_n": size * size / math.sqrt(n)}
55 +
56 +
57 +def main():
58 + ap = argparse.ArgumentParser(); ap.add_argument("--seed", type=int, default=34)
59 + ap.add_argument("--out", default=str(ROOT / "results/synthetic_ecdlp/index_calculus_probe.json"))
60 + a = ap.parse_args(); rng = random.Random(a.seed); t0 = time.time()
61 + rows = []
62 + for bits, trials in [(16, 400), (20, 300), (24, 200), (28, 120)]:
63 + for fe in (0.5, 0.6):
64 + if bits * fe > 15.5: # keep |F| ≤ ~50k so the naive decomposition stays affordable
65 + continue
66 + r = run(bits, fe, trials, rng); r["frac_exp"] = fe; rows.append(r)
67 + print(f"{bits}b |F|=p^{fe}: size {r['factor_base_size']:6d} rate {r['decomposition_rate']:.4f} (pred {r['predicted_rate']:.4f}) naive cost/√n ≈ {round(r['cost_over_sqrt_n'],1)} oracle-relations/√n ≈ {round(r['oracle_relation_cost_over_sqrt_n'],2)} linalg/√n ≈ {round(r['linear_algebra_cost_over_sqrt_n'],1)}")
68 + record("index_calculus_decomposition_probe", {"m": 2, "sizes": [16, 20, 24, 28]}, {"rows": rows}, seed=a.seed, out=a.out, started=t0)
69 +
70 +
71 +if __name__ == "__main__":
72 + main()
added experiments/synthetic_ecdlp/preprocessing_python.py +81 −0
@@ -0,0 +1,81 @@
1 +"""Discrete logarithms with preprocessing (Bernstein–Lange 2013 "non-uniform cracks"; Corrigan-Gibbs–Kogan 2018 bound).
2 +
3 +Offline: T distinguished points are produced by random walks of mean length W = ⌈√(n/T)⌉ (DP mask with ≈ log2 W bits)
4 +and stored with their logarithms (walks start at known multiples of G, so each DP has a known log).
5 +Online: a walk from Q (random start a·G + Q) runs until it reaches a stored DP, at which point d is recovered.
6 +Prediction: online cost ≈ n/(T·W) ≈ √(n/T) group operations; total (T·W + online) is never below √n but the
7 +*per-instance* cost drops as 1/√T — the "non-uniform" speedup. ST² = Ω̃(n) (Corrigan-Gibbs–Kogan) bounds this.
8 +Pure-Python reference on 2^22–2^26 groups; the Rust engine repeats this at larger sizes.
9 +"""
10 +import argparse, math, random, sys, time, pathlib, statistics as st
11 +ROOT = pathlib.Path(__file__).resolve().parents[2]
12 +sys.path.insert(0, str(ROOT / "src"))
13 +from wp34.ec import random_prime_order_curve
14 +from wp34.dlp import RAddingWalk
15 +from wp34.provenance import record
16 +
17 +
18 +def experiment(bits, T, instances, rng, r=20):
19 + E = random_prime_order_curve(bits, rng)
20 + n, G = E.n, E.G
21 + # walk independent of Q: M_j = u_j G (v_j = 0) so the logs of all visited points are known offline
22 + walk = RAddingWalk(E, G, G, n, r, rng)
23 + walk.v = [0] * r
24 + walk.M = [E.mul(u, G) for u in walk.u]
25 + W = max(2, math.isqrt(n // T))
26 + dp_bits = max(1, round(math.log2(W)))
27 + mask = (1 << dp_bits) - 1
28 + E.ops.reset()
29 + table = {}
30 + offline_steps = 0
31 + max_len = 20 * (1 << dp_bits)
32 + while len(table) < T:
33 + a = rng.randrange(1, n); P = E.mul(a, G); L = 0
34 + while P is not None and (P[0] & mask) != 0 and L < max_len:
35 + P, a, _ = walk.step(P, a, 0); L += 1
36 + offline_steps += L
37 + if P is not None and (P[0] & mask) == 0 and P not in table:
38 + table[P] = a
39 + offline_ops = E.ops.group_ops()
40 + online = []
41 + for _ in range(instances):
42 + d = rng.randrange(1, n); Q = E.mul(d, G)
43 + E.ops.reset()
44 + found = None
45 + while found is None:
46 + a = rng.randrange(1, n); P = E.add(E.mul(a, G), Q); L = 0 # P = (a + d) G
47 + E.ops.reset()
48 + while True:
49 + if P is None:
50 + break
51 + if (P[0] & mask) == 0:
52 + if P in table:
53 + found = (table[P] - a) % n
54 + break
55 + P, a, _ = walk.step(P, a, 0); L += 1
56 + if L > 50 * max_len:
57 + break
58 + online.append(E.ops.group_ops()) if found is not None else None
59 + assert found == d
60 + return {"bits": bits, "n": str(n), "T": T, "W": W, "dp_bits": dp_bits, "offline_group_ops": offline_ops,
61 + "offline_steps": offline_steps, "table_entries": len(table), "instances": instances,
62 + "online_mean": st.mean(online), "online_median": st.median(online), "online_sd": st.pstdev(online),
63 + "online_ci95_halfwidth": 1.96 * st.pstdev(online) / math.sqrt(len(online)),
64 + "pred_sqrt_n_over_T": math.sqrt(n / T), "sqrt_n": math.sqrt(n),
65 + "ratio_online_to_pred": st.mean(online) / math.sqrt(n / T),
66 + "per_instance_speedup_vs_sqrt_n": math.sqrt(n) / st.mean(online)}
67 +
68 +
69 +def main():
70 + ap = argparse.ArgumentParser(); ap.add_argument("--seed", type=int, default=34)
71 + ap.add_argument("--out", default=str(ROOT / "results/synthetic_ecdlp/preprocessing_python.json"))
72 + a = ap.parse_args(); rng = random.Random(a.seed); t0 = time.time(); rows = []
73 + for bits in (22, 26):
74 + for T in (2**4, 2**6, 2**8, 2**10, 2**12):
75 + r = experiment(bits, T, 60, rng); rows.append(r)
76 + print(f"{bits}b T={T:5d} W={r['W']:5d} offline {r['offline_group_ops']:9d} online {r['online_mean']:9.1f} ± {r['online_ci95_halfwidth']:.1f} pred √(n/T) {r['pred_sqrt_n_over_T']:8.1f} ratio {r['ratio_online_to_pred']:.2f} speedup/inst {r['per_instance_speedup_vs_sqrt_n']:.1f}", flush=True)
77 + record("dlog_with_preprocessing_python", {"r": 20, "instances": 60}, {"rows": rows}, seed=a.seed, out=a.out, started=t0)
78 +
79 +
80 +if __name__ == "__main__":
81 + main()
added experiments/synthetic_ecdlp/rho_engine/.gitignore +1 −0
@@ -0,0 +1 @@
1 +target/
added experiments/synthetic_ecdlp/rho_engine/Cargo.lock +407 −0
@@ -0,0 +1,407 @@
1 +# This file is automatically @generated by Cargo.
2 +# It is not intended for manual editing.
3 +version = 4
4 +
5 +[[package]]
6 +name = "ahash"
7 +version = "0.8.12"
8 +source = "registry+https://github.com/rust-lang/crates.io-index"
9 +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
10 +dependencies = [
11 + "cfg-if",
12 + "once_cell",
13 + "version_check",
14 + "zerocopy",
15 +]
16 +
17 +[[package]]
18 +name = "allocator-api2"
19 +version = "0.2.21"
20 +source = "registry+https://github.com/rust-lang/crates.io-index"
21 +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
22 +
23 +[[package]]
24 +name = "anstream"
25 +version = "1.0.0"
26 +source = "registry+https://github.com/rust-lang/crates.io-index"
27 +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
28 +dependencies = [
29 + "anstyle",
30 + "anstyle-parse",
31 + "anstyle-query",
32 + "anstyle-wincon",
33 + "colorchoice",
34 + "is_terminal_polyfill",
35 + "utf8parse",
36 +]
37 +
38 +[[package]]
39 +name = "anstyle"
40 +version = "1.0.14"
41 +source = "registry+https://github.com/rust-lang/crates.io-index"
42 +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
43 +
44 +[[package]]
45 +name = "anstyle-parse"
46 +version = "1.0.0"
47 +source = "registry+https://github.com/rust-lang/crates.io-index"
48 +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
49 +dependencies = [
50 + "utf8parse",
51 +]
52 +
53 +[[package]]
54 +name = "anstyle-query"
55 +version = "1.1.5"
56 +source = "registry+https://github.com/rust-lang/crates.io-index"
57 +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
58 +dependencies = [
59 + "windows-sys",
60 +]
61 +
62 +[[package]]
63 +name = "anstyle-wincon"
64 +version = "3.0.11"
65 +source = "registry+https://github.com/rust-lang/crates.io-index"
66 +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
67 +dependencies = [
68 + "anstyle",
69 + "once_cell_polyfill",
70 + "windows-sys",
71 +]
72 +
73 +[[package]]
74 +name = "cfg-if"
75 +version = "1.0.5"
76 +source = "registry+https://github.com/rust-lang/crates.io-index"
77 +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
78 +
79 +[[package]]
80 +name = "clap"
81 +version = "4.6.7"
82 +source = "registry+https://github.com/rust-lang/crates.io-index"
83 +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
84 +dependencies = [
85 + "clap_builder",
86 + "clap_derive",
87 +]
88 +
89 +[[package]]
90 +name = "clap_builder"
91 +version = "4.6.7"
92 +source = "registry+https://github.com/rust-lang/crates.io-index"
93 +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
94 +dependencies = [
95 + "anstream",
96 + "anstyle",
97 + "clap_lex",
98 + "strsim",
99 +]
100 +
101 +[[package]]
102 +name = "clap_derive"
103 +version = "4.6.7"
104 +source = "registry+https://github.com/rust-lang/crates.io-index"
105 +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
106 +dependencies = [
107 + "heck",
108 + "proc-macro2",
109 + "quote",
110 + "syn 3.0.9",
111 +]
112 +
113 +[[package]]
114 +name = "clap_lex"
115 +version = "1.1.1"
116 +source = "registry+https://github.com/rust-lang/crates.io-index"
117 +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
118 +
119 +[[package]]
120 +name = "colorchoice"
121 +version = "1.0.5"
122 +source = "registry+https://github.com/rust-lang/crates.io-index"
123 +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
124 +
125 +[[package]]
126 +name = "getrandom"
127 +version = "0.3.4"
128 +source = "registry+https://github.com/rust-lang/crates.io-index"
129 +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
130 +dependencies = [
131 + "cfg-if",
132 + "libc",
133 + "r-efi",
134 + "wasip2",
135 +]
136 +
137 +[[package]]
138 +name = "hashbrown"
139 +version = "0.14.5"
140 +source = "registry+https://github.com/rust-lang/crates.io-index"
141 +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
142 +dependencies = [
143 + "ahash",
144 + "allocator-api2",
145 +]
146 +
147 +[[package]]
148 +name = "heck"
149 +version = "0.5.0"
150 +source = "registry+https://github.com/rust-lang/crates.io-index"
151 +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
152 +
153 +[[package]]
154 +name = "is_terminal_polyfill"
155 +version = "1.70.2"
156 +source = "registry+https://github.com/rust-lang/crates.io-index"
157 +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
158 +
159 +[[package]]
160 +name = "itoa"
161 +version = "1.0.18"
162 +source = "registry+https://github.com/rust-lang/crates.io-index"
163 +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
164 +
165 +[[package]]
166 +name = "libc"
167 +version = "0.2.191"
168 +source = "registry+https://github.com/rust-lang/crates.io-index"
169 +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
170 +
171 +[[package]]
172 +name = "memchr"
173 +version = "2.8.3"
174 +source = "registry+https://github.com/rust-lang/crates.io-index"
175 +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
176 +
177 +[[package]]
178 +name = "once_cell"
179 +version = "1.21.4"
180 +source = "registry+https://github.com/rust-lang/crates.io-index"
181 +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
182 +
183 +[[package]]
184 +name = "once_cell_polyfill"
185 +version = "1.70.2"
186 +source = "registry+https://github.com/rust-lang/crates.io-index"
187 +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
188 +
189 +[[package]]
190 +name = "ppv-lite86"
191 +version = "0.2.21"
192 +source = "registry+https://github.com/rust-lang/crates.io-index"
193 +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
194 +dependencies = [
195 + "zerocopy",
196 +]
197 +
198 +[[package]]
199 +name = "proc-macro2"
200 +version = "1.0.107"
201 +source = "registry+https://github.com/rust-lang/crates.io-index"
202 +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
203 +dependencies = [
204 + "unicode-ident",
205 +]
206 +
207 +[[package]]
208 +name = "quote"
209 +version = "1.0.47"
210 +source = "registry+https://github.com/rust-lang/crates.io-index"
211 +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
212 +dependencies = [
213 + "proc-macro2",
214 +]
215 +
216 +[[package]]
217 +name = "r-efi"
218 +version = "5.3.0"
219 +source = "registry+https://github.com/rust-lang/crates.io-index"
220 +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
221 +
222 +[[package]]
223 +name = "rand"
224 +version = "0.9.5"
225 +source = "registry+https://github.com/rust-lang/crates.io-index"
226 +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
227 +dependencies = [
228 + "rand_chacha",
229 + "rand_core",
230 +]
231 +
232 +[[package]]
233 +name = "rand_chacha"
234 +version = "0.9.0"
235 +source = "registry+https://github.com/rust-lang/crates.io-index"
236 +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
237 +dependencies = [
238 + "ppv-lite86",
239 + "rand_core",
240 +]
241 +
242 +[[package]]
243 +name = "rand_core"
244 +version = "0.9.5"
245 +source = "registry+https://github.com/rust-lang/crates.io-index"
246 +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
247 +dependencies = [
248 + "getrandom",
249 +]
250 +
251 +[[package]]
252 +name = "rho_engine"
253 +version = "0.1.0"
254 +dependencies = [
255 + "clap",
256 + "hashbrown",
257 + "libc",
258 + "rand",
259 + "rand_chacha",
260 + "serde",
261 + "serde_json",
262 +]
263 +
264 +[[package]]
265 +name = "serde"
266 +version = "1.0.229"
267 +source = "registry+https://github.com/rust-lang/crates.io-index"
268 +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
269 +dependencies = [
270 + "serde_core",
271 + "serde_derive",
272 +]
273 +
274 +[[package]]
275 +name = "serde_core"
276 +version = "1.0.229"
277 +source = "registry+https://github.com/rust-lang/crates.io-index"
278 +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
279 +dependencies = [
280 + "serde_derive",
281 +]
282 +
283 +[[package]]
284 +name = "serde_derive"
285 +version = "1.0.229"
286 +source = "registry+https://github.com/rust-lang/crates.io-index"
287 +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
288 +dependencies = [
289 + "proc-macro2",
290 + "quote",
291 + "syn 3.0.9",
292 +]
293 +
294 +[[package]]
295 +name = "serde_json"
296 +version = "1.0.154"
297 +source = "registry+https://github.com/rust-lang/crates.io-index"
298 +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
299 +dependencies = [
300 + "itoa",
301 + "memchr",
302 + "serde",
303 + "serde_core",
304 + "zmij",
305 +]
306 +
307 +[[package]]
308 +name = "strsim"
309 +version = "0.11.1"
310 +source = "registry+https://github.com/rust-lang/crates.io-index"
311 +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
312 +
313 +[[package]]
314 +name = "syn"
315 +version = "2.0.119"
316 +source = "registry+https://github.com/rust-lang/crates.io-index"
317 +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
318 +dependencies = [
319 + "proc-macro2",
320 + "quote",
321 + "unicode-ident",
322 +]
323 +
324 +[[package]]
325 +name = "syn"
326 +version = "3.0.9"
327 +source = "registry+https://github.com/rust-lang/crates.io-index"
328 +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
329 +dependencies = [
330 + "proc-macro2",
331 + "quote",
332 + "unicode-ident",
333 +]
334 +
335 +[[package]]
336 +name = "unicode-ident"
337 +version = "1.0.27"
338 +source = "registry+https://github.com/rust-lang/crates.io-index"
339 +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
340 +
341 +[[package]]
342 +name = "utf8parse"
343 +version = "0.2.2"
344 +source = "registry+https://github.com/rust-lang/crates.io-index"
345 +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
346 +
347 +[[package]]
348 +name = "version_check"
349 +version = "0.9.5"
350 +source = "registry+https://github.com/rust-lang/crates.io-index"
351 +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
352 +
353 +[[package]]
354 +name = "wasip2"
355 +version = "1.0.4+wasi-0.2.12"
356 +source = "registry+https://github.com/rust-lang/crates.io-index"
357 +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
358 +dependencies = [
359 + "wit-bindgen",
360 +]
361 +
362 +[[package]]
363 +name = "windows-link"
364 +version = "0.2.1"
365 +source = "registry+https://github.com/rust-lang/crates.io-index"
366 +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
367 +
368 +[[package]]
369 +name = "windows-sys"
370 +version = "0.61.2"
371 +source = "registry+https://github.com/rust-lang/crates.io-index"
372 +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
373 +dependencies = [
374 + "windows-link",
375 +]
376 +
377 +[[package]]
378 +name = "wit-bindgen"
379 +version = "0.57.1"
380 +source = "registry+https://github.com/rust-lang/crates.io-index"
381 +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
382 +
383 +[[package]]
384 +name = "zerocopy"
385 +version = "0.8.63"
386 +source = "registry+https://github.com/rust-lang/crates.io-index"
387 +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8"
388 +dependencies = [
389 + "zerocopy-derive",
390 +]
391 +
392 +[[package]]
393 +name = "zerocopy-derive"
394 +version = "0.8.63"
395 +source = "registry+https://github.com/rust-lang/crates.io-index"
396 +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f"
397 +dependencies = [
398 + "proc-macro2",
399 + "quote",
400 + "syn 2.0.119",
401 +]
402 +
403 +[[package]]
404 +name = "zmij"
405 +version = "1.0.23"
406 +source = "registry+https://github.com/rust-lang/crates.io-index"
407 +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
added experiments/synthetic_ecdlp/rho_engine/Cargo.toml +33 −0
@@ -0,0 +1,33 @@
1 +[package]
2 +name = "rho_engine"
3 +version = "0.1.0"
4 +edition = "2021"
5 +description = "Native ECDLP experimentation engine (BSGS, Pollard rho with r-adding walks / distinguished points / negation and order-3 automorphism classes, Pollard kangaroo) for WP34-UQO synthetic instances, p < 2^62"
6 +license = "MIT"
7 +
8 +[lib]
9 +name = "rho_engine"
10 +path = "src/lib.rs"
11 +
12 +[[bin]]
13 +name = "rho_engine"
14 +path = "src/main.rs"
15 +
16 +[dependencies]
17 +clap = { version = "4", features = ["derive"] }
18 +serde = { version = "1", features = ["derive"] }
19 +serde_json = "1"
20 +rand = "0.9"
21 +rand_chacha = "0.9"
22 +hashbrown = "0.14"
23 +libc = "0.2"
24 +
25 +[profile.release]
26 +opt-level = 3
27 +lto = "fat"
28 +codegen-units = 1
29 +panic = "abort"
30 +debug = false
31 +
32 +[profile.test]
33 +opt-level = 2
added experiments/synthetic_ecdlp/rho_engine/README.md +184 −0
@@ -0,0 +1,184 @@
1 +# rho_engine — native ECDLP experimentation engine (WP34-UQO, Research Axis A)
2 +
3 +Rust crate (library + binary) solving synthetic elliptic-curve discrete logarithms Q = d·G on prime-order
4 +short Weierstrass curves y² = x³ + ax + b over F_p with **p < 2^62**, by the established generic methods, with
5 +exact operation counts, memory and time measurements. Synthetic instances only (see `docs/MANDATE.md` §2):
6 +the engine takes the curve and the two points from the command line; it never touches real keys.
7 +
8 +Conventions match the Python reference `src/wp34/ec.py`: affine points, the point at infinity is a separate
9 +flag, one group addition = one field inversion + 3 multiplications (λ, λ², λ·(x₁−x₃)), one doubling = one
10 +inversion + 4 multiplications (x², λ, λ², λ·(x₁−x₃)); the `mul` counter also includes the multiplications of
11 +Montgomery's simultaneous inversion (3 per element) and the Montgomery-form conversions used by the canonical
12 +representatives (documented below), so `mul` is an upper bound of "pure" multiplication counts.
13 +
14 +```
15 +cargo build --release # → target/release/rho_engine (arm64 Mach-O, links only libSystem/libiconv)
16 +cargo test --release # unit tests + small-curve brute force + 60 fixture instances
17 +```
18 +
19 +## CLI
20 +
21 +```
22 +rho_engine solve --p P --a A --b B --n N --gx GX --gy GY --qx QX --qy QY --alg {bsgs,rho,kangaroo}
23 + [--seed S] [--threads T] [--json] [--label L] [--expected-d D]
24 + rho: [--cycle floyd|brent|dp] [--r 20] [--dp BITS] [--walks 128] [--mixed K] [--neg]
25 + [--aut3 --beta B --lambda L] [--cycle-check 32] [--max-walk-len N]
26 + kangaroo: [--lo LO] [--hi HI] [--dp BITS] [--walks W]
27 + bsgs: [--memory M] [--chains 64]
28 +rho_engine batch --file jobs.json --out results.json [--verbose]
29 +```
30 +
31 +All integers are decimal strings (also in JSON). `solve` prints one JSON object and exits 0 on success
32 +(2 if the log was not found / not verified, with an `"error"` field for invalid inputs). `batch` reads a JSON
33 +array of job objects with the same keys as the flags (`p, a, b, n, gx, gy, qx, qy, alg, seed, threads, r,
34 +cycle, dp, walks, mixed, neg, aut3, beta, lambda, lo, hi, memory, label, expected_d`), runs them sequentially
35 +(parallelism is inside each job) and writes a JSON array of result objects.
36 +
37 +Result object (common keys):
38 +
39 +| key | meaning |
40 +|---|---|
41 +| `d` | the discrete logarithm (decimal string) |
42 +| `ok` | `d·G == Q` re-verified inside Rust after the search |
43 +| `match` | `d == expected_d` when `--expected-d` was given |
44 +| `ops` | `{add, dbl, inv, mul}` summed over all threads (setup, walks, verification) |
45 +| `group_ops`, `group_ops_per_cpu_s`, `group_ops_per_wall_s` | `add + dbl` and throughput |
46 +| `iterations` | steps of the iteration function (rho/kangaroo) or giant steps computed (bsgs) |
47 +| `expected_iterations` | √(πn/(2·class)) for rho, 2√w for kangaroo (theory, for comparison) |
48 +| `dp_count`, `dp_bytes`, `table_entries`, `table_bytes` | distinguished-point / baby-step table size |
49 +| `fruitless_cycles`, `fruitless_by_length`, `lookahead_rejections`, `doubling_fallbacks` | negation/automorphism-class bookkeeping |
50 +| `restarts`, `abandoned_walks`, `useless_collisions` (`herd_collisions` for kangaroo) | walk bookkeeping |
51 +| `threads`, `wall_s`, `cpu_s` (sum of per-thread `CLOCK_THREAD_CPUTIME_ID` timers), `process_cpu_s` | time |
52 +| `peak_rss_bytes` | `getrusage(RUSAGE_SELF).ru_maxrss` |
53 +| `params` | every effective parameter plus the instance, for provenance |
54 +
55 +## Field and group arithmetic (`src/field.rs`, `src/curve.rs`)
56 +
57 +* Montgomery representation with R = 2^64, u128 products, one conditional subtraction (valid because p < 2^62).
58 + Inversion by the extended Euclidean algorithm on i64, converted back with R³ mod p.
59 +* Montgomery's simultaneous inversion (`Field::batch_inv`, 3(k−1) multiplications + 1 inversion) is used by
60 + every batched phase: the W walks of a thread (rho), the W kangaroos of a thread, and the S independent
61 + chains of the baby-step and giant-step phases of BSGS.
62 +* Scalar bookkeeping modulo the prime n uses u128 products and `%` (`Modn`).
63 +* Random walk starts u·G + v·Q use two fixed-base comb tables (16 nibble positions × 16 entries each), so a
64 + restart costs ≤ 31 additions; these additions are included in `ops`.
65 +
66 +## Algorithms
67 +
68 +### `bsgs` — baby-step giant-step (Shanks 1971)
69 +
70 +m = ⌈√n⌉ baby steps (`--memory m` overrides: time/memory trade-off, giant steps become ⌈n/m⌉+1), x-coordinate
71 +keyed `hashbrown` table x(jG) → j for 0 < j < m; giant points R_i = Q − i·(mG); a hit means R_i = ±jG, so both
72 +d = i·m + j and d = i·m − j are verified. Reported: `table_entries`, `table_bytes` (capacity × 17 bytes),
73 +`baby_steps`, `giant_steps_planned`, `build_s`. The giant phase is split across `--threads`.
74 +
75 +### `rho` — Pollard rho (Pollard 1978) with Teske's r-adding walk (Teske 2001)
76 +
77 +* **Multipliers.** M_j = u_j·G + v_j·Q, j ∈ [0, r), with u_j, v_j uniform in [1, n) from a ChaCha20 RNG seeded
78 + by `--seed` (r = 20 by default, Teske's recommendation).
79 +* **Partition.** j = h(x) with h(x) = ⌊(mix64(x_M) ≫ 32) · r / 2^32⌋ where x_M is the Montgomery residue of
80 + the x-coordinate and mix64 the splitmix64 finaliser; the distinguished-point predicate uses the *low* bits of
81 + the same mix (`mix64(x_M) & (2^d − 1) == 0`), so the two decisions are independent.
82 +* **Iteration function.** f(P) = ‖P + M_j‖ (adding partition) or f(P) = ‖2P‖ (doubling partition), with the
83 + scalars (u, v) updated accordingly (u + u_j, v + v_j) or (2u, 2v). ‖·‖ is the canonical representative
84 + defined below (identity without `--neg/--aut3`).
85 +* **`--mixed k`** (Teske's mixed walk): every k-th partition (j ≡ k−1 mod k) is a doubling partition; the
86 + default k = 0 is the pure r-adding walk.
87 +* **Cycle detection, single walk**: `--cycle floyd` (x_i vs x_{2i}, 3 steps per iteration) or `--cycle brent`
88 + (teleporting tortoise at powers of two, 1 step per iteration). A collision with different scalars gives
89 + d = (u₁ − u₂)/(v₂ − v₁) mod n; identical scalars mean a fruitless cycle (handled below); the degenerate
90 + v₁ = v₂ case restarts from a new random point.
91 +* **`--cycle dp` (default): van Oorschot–Wiener parallel collision search (1999).** Each of `--threads` threads
92 + runs `--walks` (default 128) independent walks, each started at a random ‖u·G + v·Q‖ and advanced together
93 + with one simultaneous inversion per step. A point whose mix has `--dp` low zero bits (default
94 + ⌊log₂n/4⌋ − 2, ≥ 1) is distinguished: (x, y, u, v) is sent to a shared table (`Mutex<HashMap>`) and the walk
95 + restarts at a fresh random point. Two entries with the same x and different scalars solve the log
96 + (d = (u₁ − u₂)/(v₂ − v₁) if y₁ = y₂, d = −(u₁ + u₂)/(v₁ + v₂) if y₁ = −y₂). Walks exceeding
97 + `--max-walk-len` (default 64·2^dp) steps without a distinguished point are abandoned. With one thread the
98 + run is fully deterministic (the per-thread RNG is derived from `--seed` and the thread index).
99 +
100 +#### Equivalence classes
101 +
102 +* **`--neg` (negation map; Wiener–Zuccherato 1999, Gallant–Lambert–Vanstone 2000).** Canonical
103 + representative of {P, −P}: the point whose standard-form y satisfies 2y < p, i.e. y = min(y, p − y);
104 + choosing −P flips the scalars (u, v) ↦ (−u, −v). Expected iterations drop from √(πn/2) to √(πn/4).
105 +* **`--aut3` (order-3 automorphism ζ: (x, y) ↦ (βx, y) = λ·(x, y) on a = 0 curves with p ≡ 1 mod 3;
106 + Duursma–Gaudry–Morain 1999, GLV 2000).** Requires `--beta` and `--lambda` from the caller; at start-up the
107 + engine checks β³ ≡ 1 (mod p), λ³ ≡ 1 (mod n) and (β·gx, gy) = λ·G, and aborts otherwise. Canonical
108 + representative of {P, ζP, ζ²P}: the one with the smallest standard-form x among {x, βx, β²x}; choosing
109 + ζ^i P multiplies the scalars by λ^i. `--neg --aut3` composes both (orbit of size 6): the x rule is applied
110 + first, then the y rule. Expected iterations √(πn/6) and √(πn/12) respectively.
111 +* **Fruitless cycles (Bos–Kleinjung–Lenstra 2010, Bernstein–Lange–Schwabe 2011).**
112 + 1. *2-cycles, look-ahead.* With `--neg`, after computing R = ‖P + M_j‖ the engine checks h(R) ≠ j; if
113 + h(R) = j (the configuration that allows ‖R + M_j‖ = ‖P‖), the step is discarded (`lookahead_rejections`,
114 + one wasted addition) and the next partition j+1 is tried, and so on; after r rejections f(P) = ‖2P‖
115 + (`doubling_fallbacks`). f remains a function of P alone, so colliding walks stay merged. This is the
116 + BKL "next partition" rule; it leaves residual 2-cycles with probability ≈ 1/(2r³) per step (when the
117 + partition sequence j, j+1 repeats from R), which the periodic check below catches — the measured
118 + `fruitless_by_length` histograms confirm the ≈ r⁻³ rate (e.g. 96 two-cycles in 26 649 steps at r = 4,
119 + 1 in 6 569 steps at r = 20).
120 + 2. *Longer cycles (4, 6, … with `--neg`; 3, 6, … with `--aut3`), periodic check.* Every walk remembers its
121 + point every `--cycle-check` K steps (default 32) and compares every subsequent point with it (one
122 + comparison per step), so any fruitless cycle of length ≤ K is detected within ≤ K + length steps of
123 + entering it. Detection also happens in Floyd/Brent mode when the collision has identical scalars.
124 + 3. *Escape.* The cycle is traversed once to find its minimum point P_min (smallest Montgomery x, ties by y)
125 + and the walk continues from ‖2·P_min‖ with doubled scalars — a function of the cycle only, so two walks
126 + trapped in the same cycle leave it to the same point (BLS 2011). If the same P_min is met twice in a row
127 + the walk restarts from a fresh random point. Counts: `fruitless_cycles`, `fruitless_by_length`.
128 + Because the 4-cycle rate is ≈ 1/(4r²) per step, use a larger r (`--r 128`, `--r 512`) with `--neg`, as
129 + BLS recommend; r = 20 is kept as the default for comparability with Teske's walk.
130 +
131 +### `kangaroo` — Pollard's lambda method (Pollard 1978, 2000) in the van Oorschot–Wiener parallel form
132 +
133 +For d ∈ [lo, hi] (`--lo --hi`, default [0, n−1]), width w = hi − lo + 1. N = threads × `--walks` kangaroos
134 +(default `--walks` = clamp(2^(⌊log₂w/4⌋−2), 2, 64), even), half tame, half wild. Tame kangaroo c starts at
135 +position t_c = lo + ⌊w/2⌋ + c·s (point t_c·G), wild kangaroo c at offset o_c = c·s (point Q + o_c·G), with
136 +spacing s = ⌈m/(N/2)⌉. Jump set J = {2^0·G, …, 2^{k−1}·G} (Pollard 2000), j = h(x) ∈ [0, k), k chosen so that
137 +the mean jump m = (2^k − 1)/k is closest to N·√w/4 (= √w/2 for the classical single tame/wild pair). Every
138 +kangaroo keeps its position modulo n and reports distinguished points (same predicate and default `--dp` as
139 +rho, computed from w) to the shared table; a tame/wild collision with positions (t, o) gives d = t − o (same
140 +y) or d = −t − o (opposite y), verified before being accepted. A same-herd collision (merged trails) or a
141 +kangaroo that has travelled more than 3w + 4·m·2^dp restarts at a fresh random position of its herd
142 +(`herd_collisions`, `abandoned_walks`, `restarts`). Expected cost ≈ 2√w group operations plus the
143 +distinguished-point overshoot N·2^dp.
144 +
145 +## Tests
146 +
147 +* `cargo test --release`: Montgomery arithmetic round-trips and batch inversion; group law against repeated
148 + addition; **small curves** (9 primes 101…32771, two prime-order curves each, orders enumerated by Legendre
149 + symbols, every log brute-forced for n ≤ 300 and 30 spread values otherwise; 1 052 (curve, d) pairs × 14
150 + algorithm/option combinations); **fixtures** `tests/fixtures/ecdlp_instances.json` (60 instances: 10 generic
151 + curves for each of 20/24/28/32 bits and 10 j = 0 curves for 24 and 32 bits with β, λ and random d,
152 + generated by `scripts/gen_fixtures.py` from `src/wp34/ec.py` + PARI/GP) with 15–19 combinations each;
153 + 1-thread determinism; rejection of wrong (β, λ).
154 +* `tests/test_rho_engine.py` (repository `pytest`): the release binary on every fixture with bsgs, rho
155 + floyd / brent / dp (1 and 4 threads) / neg / mixed, aut3 and neg+aut3 (j = 0 only), kangaroo on [0, n);
156 + the answer is re-verified in pure Python (`Curve.mul`), determinism, batch mode, memory/interval overrides.
157 +
158 +## Measured throughput (Apple M5 Max laptop, macOS 27, rustc 1.95, see `research_log/2026-11-01-rho-engine.md`)
159 +
160 +Single thread, group operations per CPU-second (one run each; raw JSON with host provenance in
161 +`bench/results-laptop-2026-11-01.json`): rho dp (r = 20, 128 walks) **45.8 M/s** at 48 bits and **48.5 M/s**
162 +at 56 bits; rho `--neg --r 128` ≈ 30.6 M/s; rho `--neg --aut3 --r 128` ≈ 18–19 M/s (the literal
163 +min{x, βx, β²x} / min{y, p−y} rules cost 2 multiplications + 4 Montgomery conversions per step); kangaroo
164 +≈ 48.5 M/s; BSGS ≈ 41 M/s (m = 2^24, 288 MB); single-walk Brent (one inversion per step) ≈ 9 M/s.
165 +16 threads: ≈ 610 M group ops/s wall-clock (12.6× over one thread; the M5 Max mixes performance and
166 +efficiency cores), a 62-bit instance is solved in ≈ 3 s (≈ 1.8 G group operations).
167 +
168 +## References
169 +
170 +* V. Shanks, *Class number, a theory of factorization, and genera*, Proc. Symp. Pure Math. 20 (1971) — BSGS.
171 +* J. M. Pollard, *Monte Carlo methods for index computation (mod p)*, Math. Comp. 32 (1978) — rho and lambda.
172 +* E. Teske, *On random walks for Pollard's rho method*, Math. Comp. 70 (2001) 809–825 — r-adding and mixed walks.
173 +* P. C. van Oorschot, M. J. Wiener, *Parallel collision search with cryptanalytic applications*, J. Cryptology
174 + 12 (1999) 1–28 — distinguished points, parallel rho and kangaroo.
175 +* J. M. Pollard, *Kangaroos, Monopoly and discrete logarithms*, J. Cryptology 13 (2000) 437–447 — power-of-two jumps.
176 +* M. J. Wiener, R. J. Zuccherato, *Faster attacks on elliptic curve cryptosystems*, SAC 1998, LNCS 1556 (1999) — negation map.
177 +* R. Gallant, R. Lambert, S. Vanstone, *Improving the parallelized Pollard lambda search on anomalous binary
178 + curves*, Math. Comp. 69 (2000) 1699–1705 — automorphism classes.
179 +* I. Duursma, P. Gaudry, F. Morain, *Speeding up the discrete log computation on curves with automorphisms*,
180 + ASIACRYPT 1999, LNCS 1716 — order-3/6 automorphisms, fruitless cycles.
181 +* J. W. Bos, T. Kleinjung, A. K. Lenstra, *On the use of the negation map in the Pollard rho method*,
182 + ANTS-IX 2010, LNCS 6197 — look-ahead, cycle reduction.
183 +* D. J. Bernstein, T. Lange, P. Schwabe, *On the correct use of the negation map in the Pollard rho method*,
184 + PKC 2011, LNCS 6571 — fruitless-cycle detection and escape by doubling the minimum point.
added experiments/synthetic_ecdlp/rho_engine/bench/instances.json +93 −0
@@ -0,0 +1,93 @@
1 +{
2 + "seed": 3434,
3 + "instances": [
4 + {
5 + "label": "40b",
6 + "bits": 40,
7 + "j0": false,
8 + "p": "731430219521",
9 + "a": "521528616608",
10 + "b": "663625585920",
11 + "n": "731429817529",
12 + "gx": "517581188518",
13 + "gy": "107871235268",
14 + "qx": "681265047281",
15 + "qy": "111572846696",
16 + "d": "433493891655"
17 + },
18 + {
19 + "label": "40b-j0",
20 + "bits": 40,
21 + "j0": true,
22 + "p": "714234242731",
23 + "a": "0",
24 + "b": "528306577757",
25 + "n": "714235916041",
26 + "gx": "527991693032",
27 + "gy": "73180929049",
28 + "qx": "71218463675",
29 + "qy": "173287751683",
30 + "d": "661186003656",
31 + "beta": "218304033388",
32 + "lambda": "556326491469"
33 + },
34 + {
35 + "label": "48b",
36 + "bits": 48,
37 + "j0": false,
38 + "p": "155144852036743",
39 + "a": "40973904029449",
40 + "b": "50165993888260",
41 + "n": "155144849091497",
42 + "gx": "135480611363721",
43 + "gy": "99500221039596",
44 + "qx": "109488244491104",
45 + "qy": "19909733934125",
46 + "d": "14528122912011"
47 + },
48 + {
49 + "label": "56b",
50 + "bits": 56,
51 + "j0": false,
52 + "p": "52851429077766751",
53 + "a": "36682246493992806",
54 + "b": "40760070500328357",
55 + "n": "52851429107108077",
56 + "gx": "38707659002839334",
57 + "gy": "21726370519849091",
58 + "qx": "36077599086064332",
59 + "qy": "13481317455474452",
60 + "d": "24479240793007431"
61 + },
62 + {
63 + "label": "62b",
64 + "bits": 62,
65 + "j0": false,
66 + "p": "2850235469079068767",
67 + "a": "72168723512146639",
68 + "b": "2186774851242129870",
69 + "n": "2850235469913039331",
70 + "gx": "520788155796944739",
71 + "gy": "835393125801522201",
72 + "qx": "2071333916179257992",
73 + "qy": "466785116337883209",
74 + "d": "270768534343907931"
75 + },
76 + {
77 + "label": "62b-j0",
78 + "bits": 62,
79 + "j0": true,
80 + "p": "3193862153808758863",
81 + "a": "0",
82 + "b": "486182553241064640",
83 + "n": "3193862151858946639",
84 + "gx": "2131725010231442889",
85 + "gy": "2293632308748068329",
86 + "qx": "2141113378277301019",
87 + "qy": "2698260872469869339",
88 + "d": "540182030775404335",
89 + "beta": "1232012874494899405",
90 + "lambda": "1107271255826070558"
91 + }
92 + ]
93 +}
added experiments/synthetic_ecdlp/rho_engine/bench/results-laptop-2026-11-01.json +786 −0
@@ -0,0 +1,786 @@
1 +{
2 + "experiment": "rho_engine throughput (laptop)",
3 + "timestamp_utc": "2026-11-01T06:24:59+00:00",
4 + "host": {
5 + "hostname": "MacBookPro-de-simon-pierre-2.local",
6 + "cpu": "Apple M5 Max",
7 + "cores": 18,
8 + "os": "27.0",
9 + "machine": "arm64",
10 + "rustc": "rustc 1.95.0 (59807616e 2026-04-14)"
11 + },
12 + "binary": "experiments/synthetic_ecdlp/rho_engine/target/release/rho_engine (cargo build --release, lto=fat)",
13 + "instances": "experiments/synthetic_ecdlp/rho_engine/bench/instances.json (seed 3434, gen_bench_instances.py)",
14 + "command_template": "rho_engine solve --p \u2026 --a \u2026 --b \u2026 --n \u2026 --gx \u2026 --gy \u2026 --qx \u2026 --qy \u2026 --expected-d \u2026 --seed S <args>",
15 + "note": "single runs (not \u2265 10 repetitions): engine validation / throughput only, not a scaling experiment; the scaling driver is written by another process",
16 + "runs_40b": [
17 + {
18 + "label": "40b",
19 + "args": "--alg rho --threads 1",
20 + "ok": true,
21 + "match": true,
22 + "iterations": 818560,
23 + "expected_iterations": 1071880.2501598836,
24 + "group_ops": 880054,
25 + "group_ops_per_cpu_s": 36339026.442245655,
26 + "group_ops_per_wall_s": 35447574.90428634,
27 + "cpu_s": 0.024217875,
28 + "wall_s": 0.024826917,
29 + "fruitless_cycles": 0,
30 + "lookahead_rejections": 0,
31 + "dp_count": 3289,
32 + "dp_bytes": 473088,
33 + "restarts": 3418,
34 + "peak_rss_bytes": 3211264,
35 + "ops": {
36 + "add": 879856,
37 + "dbl": 198,
38 + "inv": 67889,
39 + "mul": 5083250
40 + }
41 + },
42 + {
43 + "label": "40b",
44 + "args": "--alg rho --threads 1",
45 + "ok": true,
46 + "match": true,
47 + "iterations": 2022272,
48 + "expected_iterations": 1071880.2501598836,
49 + "group_ops": 2168188,
50 + "group_ops_per_cpu_s": 36345529.1793001,
51 + "group_ops_per_wall_s": 35793396.5060618,
52 + "cpu_s": 0.059654875,
53 + "wall_s": 0.060575084,
54 + "fruitless_cycles": 0,
55 + "lookahead_rejections": 0,
56 + "dp_count": 8062,
57 + "dp_bytes": 473088,
58 + "restarts": 8191,
59 + "peak_rss_bytes": 3194880,
60 + "ops": {
61 + "add": 2167990,
62 + "dbl": 198,
63 + "inv": 161715,
64 + "mul": 12539980
65 + }
66 + },
67 + {
68 + "label": "40b",
69 + "args": "--alg rho --threads 1",
70 + "ok": true,
71 + "match": true,
72 + "iterations": 1116416,
73 + "expected_iterations": 1071880.2501598836,
74 + "group_ops": 1196474,
75 + "group_ops_per_cpu_s": 36079216.90627124,
76 + "group_ops_per_wall_s": 35574098.29445474,
77 + "cpu_s": 0.033162416,
78 + "wall_s": 0.033633291,
79 + "fruitless_cycles": 0,
80 + "lookahead_rejections": 0,
81 + "dp_count": 4348,
82 + "dp_bytes": 473088,
83 + "restarts": 4477,
84 + "peak_rss_bytes": 3211264,
85 + "ops": {
86 + "add": 1196276,
87 + "dbl": 198,
88 + "inv": 88780,
89 + "mul": 6921424
90 + }
91 + },
92 + {
93 + "label": "40b",
94 + "args": "--alg rho --threads 1 --walks 256",
95 + "ok": true,
96 + "match": true,
97 + "iterations": 809728,
98 + "expected_iterations": 1071880.2501598836,
99 + "group_ops": 872957,
100 + "group_ops_per_cpu_s": 39081574.47139231,
101 + "group_ops_per_wall_s": 38520598.48161354,
102 + "cpu_s": 0.022336792,
103 + "wall_s": 0.022662083,
104 + "fruitless_cycles": 0,
105 + "lookahead_rejections": 0,
106 + "dp_count": 3258,
107 + "dp_bytes": 473088,
108 + "restarts": 3515,
109 + "peak_rss_bytes": 3244032,
110 + "ops": {
111 + "add": 872759,
112 + "dbl": 198,
113 + "inv": 66392,
114 + "mul": 5041927
115 + }
116 + },
117 + {
118 + "label": "40b",
119 + "args": "--alg rho --threads 1 --walks 512",
120 + "ok": true,
121 + "match": true,
122 + "iterations": 792576,
123 + "expected_iterations": 1071880.2501598836,
124 + "group_ops": 858682,
125 + "group_ops_per_cpu_s": 37146116.557500705,
126 + "group_ops_per_wall_s": 36367366.46761959,
127 + "cpu_s": 0.023116333000000003,
128 + "wall_s": 0.023611333,
129 + "fruitless_cycles": 0,
130 + "lookahead_rejections": 0,
131 + "dp_count": 3166,
132 + "dp_bytes": 473088,
133 + "restarts": 3679,
134 + "peak_rss_bytes": 3293184,
135 + "ops": {
136 + "add": 858484,
137 + "dbl": 198,
138 + "inv": 67654,
139 + "mul": 4950876
140 + }
141 + },
142 + {
143 + "label": "40b",
144 + "args": "--alg rho --threads 1 --neg",
145 + "ok": true,
146 + "match": true,
147 + "iterations": 604971,
148 + "expected_iterations": 757933.7935079867,
149 + "group_ops": 683128,
150 + "group_ops_per_cpu_s": 25548558.091528002,
151 + "group_ops_per_wall_s": 24967367.58828331,
152 + "cpu_s": 0.026738417000000004,
153 + "wall_s": 0.027360834,
154 + "fruitless_cycles": 522,
155 + "lookahead_rejections": 33553,
156 + "dp_count": 2336,
157 + "dp_bytes": 236544,
158 + "restarts": 2465,
159 + "peak_rss_bytes": 2949120,
160 + "ops": {
161 + "add": 682408,
162 + "dbl": 720,
163 + "inv": 52446,
164 + "mul": 4590459
165 + }
166 + },
167 + {
168 + "label": "40b",
169 + "args": "--alg rho --threads 1 --neg --r 128",
170 + "ok": true,
171 + "match": true,
172 + "iterations": 761951,
173 + "expected_iterations": 757933.7935079867,
174 + "group_ops": 828098,
175 + "group_ops_per_cpu_s": 25743646.07446687,
176 + "group_ops_per_wall_s": 25262454.38043048,
177 + "cpu_s": 0.032167083,
178 + "wall_s": 0.032779792,
179 + "fruitless_cycles": 15,
180 + "lookahead_rejections": 5998,
181 + "dp_count": 3104,
182 + "dp_bytes": 236544,
183 + "restarts": 3234,
184 + "peak_rss_bytes": 2932736,
185 + "ops": {
186 + "add": 827885,
187 + "dbl": 213,
188 + "inv": 66225,
189 + "mul": 5547370
190 + }
191 + },
192 + {
193 + "label": "40b-j0",
194 + "args": "--alg rho --threads 1 --neg --aut3 --r 128",
195 + "ok": true,
196 + "match": true,
197 + "iterations": 567331,
198 + "expected_iterations": 432419.3862024441,
199 + "group_ops": 617062,
200 + "group_ops_per_cpu_s": 19354203.5160702,
201 + "group_ops_per_wall_s": 18915106.61837147,
202 + "cpu_s": 0.031882583000000006,
203 + "wall_s": 0.032622708,
204 + "fruitless_cycles": 5,
205 + "lookahead_rejections": 4470,
206 + "dp_count": 2263,
207 + "dp_bytes": 236544,
208 + "restarts": 2392,
209 + "peak_rss_bytes": 2949120,
210 + "ops": {
211 + "add": 616858,
212 + "dbl": 204,
213 + "inv": 49753,
214 + "mul": 7002962
215 + }
216 + },
217 + {
218 + "label": "40b",
219 + "args": "--alg rho --threads 16",
220 + "ok": true,
221 + "match": true,
222 + "iterations": 3687168,
223 + "expected_iterations": 1071880.2501598836,
224 + "group_ops": 3982117,
225 + "group_ops_per_cpu_s": 28906611.80408115,
226 + "group_ops_per_wall_s": 329601109.12232417,
227 + "cpu_s": 0.13775799900000002,
228 + "wall_s": 0.012081625,
229 + "fruitless_cycles": 0,
230 + "lookahead_rejections": 0,
231 + "dp_count": 14576,
232 + "dp_bytes": 946176,
233 + "restarts": 16626,
234 + "peak_rss_bytes": 5079040,
235 + "ops": {
236 + "add": 3981919,
237 + "dbl": 198,
238 + "inv": 323755,
239 + "mul": 22950441
240 + }
241 + },
242 + {
243 + "label": "40b",
244 + "args": "--alg kangaroo --threads 1",
245 + "ok": true,
246 + "match": true,
247 + "iterations": 1274944,
248 + "expected_iterations": 1710473.4052641683,
249 + "group_ops": 1275756,
250 + "group_ops_per_cpu_s": 46646695.18631405,
251 + "group_ops_per_wall_s": 45885482.602946825,
252 + "cpu_s": 0.027349333000000003,
253 + "wall_s": 0.027803042,
254 + "fruitless_cycles": 0,
255 + "lookahead_rejections": null,
256 + "dp_count": 4865,
257 + "dp_bytes": 946176,
258 + "restarts": 0,
259 + "peak_rss_bytes": 3702784,
260 + "ops": {
261 + "add": 1275638,
262 + "dbl": 118,
263 + "inv": 20733,
264 + "mul": 7612376
265 + }
266 + },
267 + {
268 + "label": "40b",
269 + "args": "--alg kangaroo --threads 8",
270 + "ok": true,
271 + "match": true,
272 + "iterations": 1053504,
273 + "expected_iterations": 1710473.4052641683,
274 + "group_ops": 1057816,
275 + "group_ops_per_cpu_s": 34160653.6579991,
276 + "group_ops_per_wall_s": 238690359.3388616,
277 + "cpu_s": 0.030965918,
278 + "wall_s": 0.00443175,
279 + "fruitless_cycles": 0,
280 + "lookahead_rejections": null,
281 + "dp_count": 4025,
282 + "dp_bytes": 946176,
283 + "restarts": 0,
284 + "peak_rss_bytes": 3932160,
285 + "ops": {
286 + "add": 1057698,
287 + "dbl": 118,
288 + "inv": 20773,
289 + "mul": 6301156
290 + }
291 + },
292 + {
293 + "label": "40b",
294 + "args": "--alg bsgs --threads 1",
295 + "ok": true,
296 + "match": true,
297 + "iterations": 793702,
298 + "expected_iterations": null,
299 + "group_ops": 1652210,
300 + "group_ops_per_cpu_s": 38779455.819231465,
301 + "group_ops_per_wall_s": 38199878.719240405,
302 + "cpu_s": 0.042605291,
303 + "wall_s": 0.043251708,
304 + "fruitless_cycles": 0,
305 + "lookahead_rejections": null,
306 + "dp_count": 0,
307 + "dp_bytes": 0,
308 + "restarts": null,
309 + "peak_rss_bytes": 20463616,
310 + "ops": {
311 + "add": 1649906,
312 + "dbl": 2304,
313 + "inv": 29138,
314 + "mul": 9853920
315 + }
316 + },
317 + {
318 + "label": "40b",
319 + "args": "--alg rho --cycle brent",
320 + "ok": true,
321 + "match": true,
322 + "iterations": 820986,
323 + "expected_iterations": 1071880.2501598836,
324 + "group_ops": 821988,
325 + "group_ops_per_cpu_s": 9679286.524899945,
326 + "group_ops_per_wall_s": 9570631.007201364,
327 + "cpu_s": 0.08492237500000001,
328 + "wall_s": 0.0858865,
329 + "fruitless_cycles": 0,
330 + "lookahead_rejections": 0,
331 + "dp_count": 0,
332 + "dp_bytes": 0,
333 + "restarts": 1,
334 + "peak_rss_bytes": 2572288,
335 + "ops": {
336 + "add": 821790,
337 + "dbl": 198,
338 + "inv": 821988,
339 + "mul": 3287148
340 + }
341 + }
342 + ],
343 + "runs_48_62b": [
344 + {
345 + "label": "48b",
346 + "args": "--alg rho --threads 1",
347 + "seed": 1,
348 + "ok": true,
349 + "match": true,
350 + "iterations": 24591744,
351 + "expected_iterations": 15610924.350405136,
352 + "group_ops": 25109596,
353 + "group_ops_per_cpu_s": 45797340.440173544,
354 + "group_ops_per_wall_s": 45609497.136305064,
355 + "cpu_s": 0.548276292,
356 + "wall_s": 0.550534375,
357 + "fruitless_cycles": 0,
358 + "lookahead_rejections": 0,
359 + "dp_count": 24023,
360 + "dp_bytes": 1892352,
361 + "restarts": 24152,
362 + "peak_rss_bytes": 4816896,
363 + "threads": 1
364 + },
365 + {
366 + "label": "48b",
367 + "args": "--alg rho --threads 1",
368 + "seed": 2,
369 + "ok": true,
370 + "match": true,
371 + "iterations": 30817408,
372 + "expected_iterations": 15610924.350405136,
373 + "group_ops": 31466447,
374 + "group_ops_per_cpu_s": 45705472.893354565,
375 + "group_ops_per_wall_s": 45593400.721835315,
376 + "cpu_s": 0.68846125,
377 + "wall_s": 0.690153542,
378 + "fruitless_cycles": 0,
379 + "lookahead_rejections": 0,
380 + "dp_count": 30155,
381 + "dp_bytes": 1892352,
382 + "restarts": 30284,
383 + "peak_rss_bytes": 4816896,
384 + "threads": 1
385 + },
386 + {
387 + "label": "48b",
388 + "args": "--alg rho --threads 1",
389 + "seed": 3,
390 + "ok": true,
391 + "match": true,
392 + "iterations": 15180032,
393 + "expected_iterations": 15610924.350405136,
394 + "group_ops": 15497692,
395 + "group_ops_per_cpu_s": 45837180.37259068,
396 + "group_ops_per_wall_s": 45608840.2648612,
397 + "cpu_s": 0.338103083,
398 + "wall_s": 0.339795792,
399 + "fruitless_cycles": 0,
400 + "lookahead_rejections": 0,
401 + "dp_count": 14672,
402 + "dp_bytes": 1892352,
403 + "restarts": 14801,
404 + "peak_rss_bytes": 4816896,
405 + "threads": 1
406 + },
407 + {
408 + "label": "48b",
409 + "args": "--alg rho --threads 1 --neg",
410 + "seed": 1,
411 + "ok": true,
412 + "match": true,
413 + "iterations": 3297585,
414 + "expected_iterations": 11038590.468761671,
415 + "group_ops": 3551141,
416 + "group_ops_per_cpu_s": 29883405.99203549,
417 + "group_ops_per_wall_s": 29829812.68159049,
418 + "cpu_s": 0.11883320800000001,
419 + "wall_s": 0.119046708,
420 + "fruitless_cycles": 2743,
421 + "lookahead_rejections": 181477,
422 + "dp_count": 3186,
423 + "dp_bytes": 946176,
424 + "restarts": 3315,
425 + "peak_rss_bytes": 3768320,
426 + "threads": 1
427 + },
428 + {
429 + "label": "48b",
430 + "args": "--alg rho --threads 1 --neg --r 128",
431 + "seed": 1,
432 + "ok": true,
433 + "match": true,
434 + "iterations": 10905345,
435 + "expected_iterations": 11038590.468761671,
436 + "group_ops": 11227077,
437 + "group_ops_per_cpu_s": 30703480.781911954,
438 + "group_ops_per_wall_s": 30590111.657691985,
439 + "cpu_s": 0.365661375,
440 + "wall_s": 0.367016542,
441 + "fruitless_cycles": 162,
442 + "lookahead_rejections": 86580,
443 + "dp_count": 10698,
444 + "dp_bytes": 946176,
445 + "restarts": 10827,
446 + "peak_rss_bytes": 3751936,
447 + "threads": 1
448 + },
449 + {
450 + "label": "48b",
451 + "args": "--alg rho --threads 1 --neg --r 512",
452 + "seed": 1,
453 + "ok": true,
454 + "match": true,
455 + "iterations": 7922105,
456 + "expected_iterations": 11038590.468761671,
457 + "group_ops": 8117552,
458 + "group_ops_per_cpu_s": 30637864.55357652,
459 + "group_ops_per_wall_s": 30312861.756528098,
460 + "cpu_s": 0.264951625,
461 + "wall_s": 0.267792334,
462 + "fruitless_cycles": 2,
463 + "lookahead_rejections": 15569,
464 + "dp_count": 7734,
465 + "dp_bytes": 946176,
466 + "restarts": 7863,
467 + "peak_rss_bytes": 3784704,
468 + "threads": 1
469 + },
470 + {
471 + "label": "48b",
472 + "args": "--alg kangaroo --threads 1",
473 + "seed": 1,
474 + "ok": true,
475 + "match": true,
476 + "iterations": 28446848,
477 + "expected_iterations": 24911431.038099516,
478 + "group_ops": 28447747,
479 + "group_ops_per_cpu_s": 48896072.148473635,
480 + "group_ops_per_wall_s": 48262243.41579228,
481 + "cpu_s": 0.58180025,
482 + "wall_s": 0.589441041,
483 + "fruitless_cycles": 0,
484 + "lookahead_rejections": null,
485 + "dp_count": 27717,
486 + "dp_bytes": 3784704,
487 + "restarts": 0,
488 + "peak_rss_bytes": 6963200,
489 + "threads": 1
490 + },
491 + {
492 + "label": "48b",
493 + "args": "--alg bsgs --threads 1",
494 + "seed": 1,
495 + "ok": true,
496 + "match": true,
497 + "iterations": 12369670,
498 + "expected_iterations": null,
499 + "group_ops": 24829561,
500 + "group_ops_per_cpu_s": 41085638.09829535,
501 + "group_ops_per_wall_s": 39174132.709165245,
502 + "cpu_s": 0.6043367500000001,
503 + "wall_s": 0.633825417,
504 + "fruitless_cycles": 0,
505 + "lookahead_rejections": null,
506 + "dp_count": 0,
507 + "dp_bytes": 0,
508 + "restarts": null,
509 + "peak_rss_bytes": 287850496,
510 + "threads": 1
511 + },
512 + {
513 + "label": "48b",
514 + "args": "--alg rho --cycle brent",
515 + "seed": 1,
516 + "ok": true,
517 + "match": true,
518 + "iterations": 33895682,
519 + "expected_iterations": 15610924.350405136,
520 + "group_ops": 33896766,
521 + "group_ops_per_cpu_s": 8970791.03771472,
522 + "group_ops_per_wall_s": 8918128.114160826,
523 + "cpu_s": 3.778570458,
524 + "wall_s": 3.8008835,
525 + "fruitless_cycles": 0,
526 + "lookahead_rejections": 0,
527 + "dp_count": 0,
528 + "dp_bytes": 0,
529 + "restarts": 1,
530 + "peak_rss_bytes": 2588672,
531 + "threads": 1
532 + },
533 + {
534 + "label": "56b",
535 + "args": "--alg rho --threads 1",
536 + "seed": 1,
537 + "ok": true,
538 + "match": true,
539 + "iterations": 125526528,
540 + "expected_iterations": 288129885.13395524,
541 + "group_ops": 126305165,
542 + "group_ops_per_cpu_s": 48496065.27925469,
543 + "group_ops_per_wall_s": 48366163.60382494,
544 + "cpu_s": 2.6044415,
545 + "wall_s": 2.6114365,
546 + "fruitless_cycles": 0,
547 + "lookahead_rejections": 0,
548 + "dp_count": 30709,
549 + "dp_bytes": 7569408,
550 + "restarts": 30838,
551 + "peak_rss_bytes": 11321344,
552 + "threads": 1
553 + },
554 + {
555 + "label": "56b",
556 + "args": "--alg rho --threads 1 --neg --r 128",
557 + "seed": 1,
558 + "ok": true,
559 + "match": true,
560 + "iterations": 186183000,
561 + "expected_iterations": 203738595.64072078,
562 + "group_ops": 188811477,
563 + "group_ops_per_cpu_s": 30523187.568696056,
564 + "group_ops_per_wall_s": 30119138.558060113,
565 + "cpu_s": 6.185837458,
566 + "wall_s": 6.268820625,
567 + "fruitless_cycles": 2995,
568 + "lookahead_rejections": 1471467,
569 + "dp_count": 45613,
570 + "dp_bytes": 3784704,
571 + "restarts": 45742,
572 + "peak_rss_bytes": 7012352,
573 + "threads": 1
574 + },
575 + {
576 + "label": "56b",
577 + "args": "--alg kangaroo --threads 1",
578 + "seed": 1,
579 + "ok": true,
580 + "match": true,
581 + "iterations": 756651712,
582 + "expected_iterations": 459788773.70857185,
583 + "group_ops": 756652689,
584 + "group_ops_per_cpu_s": 48364227.392447785,
585 + "group_ops_per_wall_s": 47840544.45652862,
586 + "cpu_s": 15.644883208,
587 + "wall_s": 15.81613875,
588 + "fruitless_cycles": 0,
589 + "lookahead_rejections": null,
590 + "dp_count": 182153,
591 + "dp_bytes": 15138816,
592 + "restarts": 0,
593 + "peak_rss_bytes": 19906560,
594 + "threads": 1
595 + },
596 + {
597 + "label": "56b",
598 + "args": "--alg rho --threads 2",
599 + "seed": 1,
600 + "ok": true,
601 + "match": true,
602 + "iterations": 251270912,
603 + "expected_iterations": 288129885.13395524,
604 + "group_ops": 252824474,
605 + "group_ops_per_cpu_s": 45921777.2005417,
606 + "group_ops_per_wall_s": 88971972.70610732,
607 + "cpu_s": 5.505546375,
608 + "wall_s": 2.8416192909999998,
609 + "fruitless_cycles": 0,
610 + "lookahead_rejections": 0,
611 + "dp_count": 61331,
612 + "dp_bytes": 7569408,
613 + "restarts": 61588,
614 + "peak_rss_bytes": 11354112,
615 + "threads": 2
616 + },
617 + {
618 + "label": "56b",
619 + "args": "--alg rho --threads 4",
620 + "seed": 1,
621 + "ok": true,
622 + "match": true,
623 + "iterations": 499844992,
624 + "expected_iterations": 288129885.13395524,
625 + "group_ops": 502928223,
626 + "group_ops_per_cpu_s": 45480647.45926857,
627 + "group_ops_per_wall_s": 178155622.63715833,
628 + "cpu_s": 11.058070874000002,
629 + "wall_s": 2.822971375,
630 + "fruitless_cycles": 0,
631 + "lookahead_rejections": 0,
632 + "dp_count": 121776,
633 + "dp_bytes": 7569408,
634 + "restarts": 122289,
635 + "peak_rss_bytes": 11386880,
636 + "threads": 4
637 + },
638 + {
639 + "label": "56b",
640 + "args": "--alg rho --threads 8",
641 + "seed": 1,
642 + "ok": true,
643 + "match": true,
644 + "iterations": 434961024,
645 + "expected_iterations": 288129885.13395524,
646 + "group_ops": 437667839,
647 + "group_ops_per_cpu_s": 41769378.9924586,
648 + "group_ops_per_wall_s": 327183963.1016182,
649 + "cpu_s": 10.478198373,
650 + "wall_s": 1.337681208,
651 + "fruitless_cycles": 0,
652 + "lookahead_rejections": 0,
653 + "dp_count": 106338,
654 + "dp_bytes": 7569408,
655 + "restarts": 107363,
656 + "peak_rss_bytes": 11730944,
657 + "threads": 8
658 + },
659 + {
660 + "label": "56b",
661 + "args": "--alg rho --threads 12",
662 + "seed": 1,
663 + "ok": true,
664 + "match": true,
665 + "iterations": 463601408,
666 + "expected_iterations": 288129885.13395524,
667 + "group_ops": 466485338,
668 + "group_ops_per_cpu_s": 40984058.136490576,
669 + "group_ops_per_wall_s": 482494755.22576857,
670 + "cpu_s": 11.382116833,
671 + "wall_s": 0.9668195,
672 + "fruitless_cycles": 0,
673 + "lookahead_rejections": 0,
674 + "dp_count": 112848,
675 + "dp_bytes": 7569408,
676 + "restarts": 114385,
677 + "peak_rss_bytes": 11911168,
678 + "threads": 12
679 + },
680 + {
681 + "label": "56b",
682 + "args": "--alg rho --threads 16",
683 + "seed": 1,
684 + "ok": true,
685 + "match": true,
686 + "iterations": 616153984,
687 + "expected_iterations": 288129885.13395524,
688 + "group_ops": 619991330,
689 + "group_ops_per_cpu_s": 40543836.02991315,
690 + "group_ops_per_wall_s": 613344186.6170815,
691 + "cpu_s": 15.291876416000001,
692 + "wall_s": 1.010837542,
693 + "fruitless_cycles": 0,
694 + "lookahead_rejections": 0,
695 + "dp_count": 150169,
696 + "dp_bytes": 7569408,
697 + "restarts": 152218,
698 + "peak_rss_bytes": 12140544,
699 + "threads": 16
700 + },
701 + {
702 + "label": "62b",
703 + "args": "--alg rho --threads 16",
704 + "seed": 1,
705 + "ok": true,
706 + "match": true,
707 + "iterations": 1814063360,
708 + "expected_iterations": 2115925189.2824397,
709 + "group_ops": 1820376671,
710 + "group_ops_per_cpu_s": 40322020.17090835,
711 + "group_ops_per_wall_s": 608172098.4482187,
712 + "cpu_s": 45.145968958999994,
713 + "wall_s": 2.993193334,
714 + "fruitless_cycles": 0,
715 + "lookahead_rejections": 0,
716 + "dp_count": 221103,
717 + "dp_bytes": 30277632,
718 + "restarts": 223152,
719 + "peak_rss_bytes": 37896192,
720 + "threads": 16
721 + },
722 + {
723 + "label": "62b",
724 + "args": "--alg rho --threads 16 --neg --r 128",
725 + "seed": 1,
726 + "ok": true,
727 + "match": true,
728 + "iterations": 1429178265,
729 + "expected_iterations": 1496185049.8250422,
730 + "group_ops": 1445488064,
731 + "group_ops_per_cpu_s": 26778682.072889388,
732 + "group_ops_per_wall_s": 396900933.9164604,
733 + "cpu_s": 53.979059166000006,
734 + "wall_s": 3.641936666,
735 + "fruitless_cycles": 23011,
736 + "lookahead_rejections": 11302681,
737 + "dp_count": 174798,
738 + "dp_bytes": 15138816,
739 + "restarts": 176847,
740 + "peak_rss_bytes": 20643840,
741 + "threads": 16
742 + },
743 + {
744 + "label": "62b-j0",
745 + "args": "--alg rho --threads 16 --neg --aut3 --r 128",
746 + "seed": 1,
747 + "ok": true,
748 + "match": true,
749 + "iterations": 693894612,
750 + "expected_iterations": 914413011.757568,
751 + "group_ops": 701837262,
752 + "group_ops_per_cpu_s": 18209079.79519132,
753 + "group_ops_per_wall_s": 274718868.38832784,
754 + "cpu_s": 38.543258083000005,
755 + "wall_s": 2.554747208,
756 + "fruitless_cycles": 1261,
757 + "lookahead_rejections": 5471740,
758 + "dp_count": 84822,
759 + "dp_bytes": 7569408,
760 + "restarts": 86871,
761 + "peak_rss_bytes": 12107776,
762 + "threads": 16
763 + },
764 + {
765 + "label": "62b",
766 + "args": "--alg kangaroo --threads 16",
767 + "seed": 1,
768 + "ok": true,
769 + "match": true,
770 + "iterations": 4614096512,
771 + "expected_iterations": 3376528080.684678,
772 + "group_ops": 4614108399,
773 + "group_ops_per_cpu_s": 39068604.55175319,
774 + "group_ops_per_wall_s": 579880668.1805246,
775 + "cpu_s": 118.10271833199998,
776 + "wall_s": 7.956996417,
777 + "fruitless_cycles": 0,
778 + "lookahead_rejections": null,
779 + "dp_count": 561610,
780 + "dp_bytes": 60555264,
781 + "restarts": 0,
782 + "peak_rss_bytes": 72302592,
783 + "threads": 16
784 + }
785 + ]
786 +}
\ No newline at end of file
added experiments/synthetic_ecdlp/rho_engine/scripts/gen_bench_instances.py +27 −0
@@ -0,0 +1,27 @@
1 +#!/usr/bin/env python
2 +"""Generate larger synthetic instances (40–62 bits) for throughput measurements of rho_engine.
3 +
4 +Usage: PYTHONPATH=src .venv/bin/python experiments/synthetic_ecdlp/rho_engine/scripts/gen_bench_instances.py
5 +Writes experiments/synthetic_ecdlp/rho_engine/bench/instances.json (decimal strings, with d for verification).
6 +"""
7 +import json, random, pathlib, sys, time
8 +ROOT = pathlib.Path(__file__).resolve().parents[4]
9 +sys.path.insert(0, str(ROOT / "src"))
10 +from wp34.ec import random_prime_order_curve, cube_roots_of_unity, endomorphism_lambda # noqa: E402
11 +
12 +rng = random.Random(3434)
13 +out = []
14 +for bits, j0 in [(40, False), (40, True), (48, False), (56, False), (62, False), (62, True)]:
15 + E = random_prime_order_curve(bits, rng, j0=j0)
16 + d = rng.randrange(1, E.n)
17 + Q = E.mul(d, E.G)
18 + rec = {"label": f"{bits}b{'-j0' if j0 else ''}", "bits": bits, "j0": j0, "p": str(E.p), "a": str(E.a), "b": str(E.b),
19 + "n": str(E.n), "gx": str(E.G[0]), "gy": str(E.G[1]), "qx": str(Q[0]), "qy": str(Q[1]), "d": str(d)}
20 + if j0:
21 + beta = cube_roots_of_unity(E.p, rng)[0]
22 + rec["beta"], rec["lambda"] = str(beta), str(endomorphism_lambda(E, beta))
23 + out.append(rec)
24 + print(rec["label"], E.p, E.n, file=sys.stderr)
25 +path = ROOT / "experiments/synthetic_ecdlp/rho_engine/bench/instances.json"
26 +path.write_text(json.dumps({"seed": 3434, "instances": out}, indent=1) + "\n")
27 +print("wrote", path, file=sys.stderr)
added experiments/synthetic_ecdlp/rho_engine/scripts/gen_fixtures.py +58 −0
@@ -0,0 +1,58 @@
1 +#!/usr/bin/env python
2 +"""Generate tests/fixtures/ecdlp_instances.json for rho_engine.
3 +
4 +10 generic prime-order curves for each of 20, 24, 28, 32 bits and 10 j = 0 curves (a = 0, p ≡ 1 mod 3)
5 +for each of 24 and 32 bits, each with a random secret d and Q = dG, computed with the pure-Python
6 +reference `wp34.ec` (point counting by PARI/GP `ellcard`). All integers are decimal strings.
7 +
8 +Usage (from the repository root): PYTHONPATH=src .venv/bin/python experiments/synthetic_ecdlp/rho_engine/scripts/gen_fixtures.py [--seed 34] [--out tests/fixtures/ecdlp_instances.json]
9 +"""
10 +import argparse, json, random, pathlib, sys, time
11 +
12 +ROOT = pathlib.Path(__file__).resolve().parents[4]
13 +sys.path.insert(0, str(ROOT / "src"))
14 +from wp34.ec import random_prime_order_curve, cube_roots_of_unity, endomorphism_lambda # noqa: E402
15 +
16 +
17 +def main():
18 + ap = argparse.ArgumentParser()
19 + ap.add_argument("--seed", type=int, default=34)
20 + ap.add_argument("--out", default=str(ROOT / "tests" / "fixtures" / "ecdlp_instances.json"))
21 + ap.add_argument("--per-size", type=int, default=10)
22 + args = ap.parse_args()
23 + rng = random.Random(args.seed)
24 + plan = [(20, False), (24, False), (28, False), (32, False), (24, True), (32, True)]
25 + out = []
26 + t0 = time.time()
27 + for bits, j0 in plan:
28 + for i in range(args.per_size):
29 + E = random_prime_order_curve(bits, rng, j0=j0)
30 + d = rng.randrange(1, E.n)
31 + Q = E.mul(d, E.G)
32 + assert E.is_on_curve(Q) and Q is not None
33 + rec = {
34 + "label": f"{bits}b{'-j0' if j0 else ''}-{i:02d}",
35 + "bits": bits, "j0": j0,
36 + "p": str(E.p), "a": str(E.a), "b": str(E.b), "n": str(E.n),
37 + "gx": str(E.G[0]), "gy": str(E.G[1]), "qx": str(Q[0]), "qy": str(Q[1]),
38 + "d": str(d),
39 + }
40 + if j0:
41 + assert E.n % 3 == 1, "prime order n of a j=0 curve with p ≡ 1 mod 3 must be ≡ 1 mod 3"
42 + betas = cube_roots_of_unity(E.p, rng)
43 + beta = betas[0]
44 + lam = endomorphism_lambda(E, beta)
45 + assert (beta * E.G[0] % E.p, E.G[1]) == E.mul(lam, E.G)
46 + rec["beta"] = str(beta)
47 + rec["lambda"] = str(lam)
48 + out.append(rec)
49 + print(f"{rec['label']}: p={E.p} n={E.n} d={d}", file=sys.stderr)
50 + doc = {"generator": "experiments/synthetic_ecdlp/rho_engine/scripts/gen_fixtures.py", "seed": args.seed,
51 + "reference": "src/wp34/ec.py (random_prime_order_curve via PARI/GP ellcard)",
52 + "elapsed_s": round(time.time() - t0, 1), "instances": out}
53 + pathlib.Path(args.out).write_text(json.dumps(doc, indent=1) + "\n")
54 + print(f"wrote {len(out)} instances to {args.out}", file=sys.stderr)
55 +
56 +
57 +if __name__ == "__main__":
58 + main()
added experiments/synthetic_ecdlp/rho_engine/src/bsgs.rs +233 −0
@@ -0,0 +1,233 @@
1 +//! Baby-step giant-step (Shanks 1971) with x-coordinate keys and ± lookup.
2 +//!
3 +//! Baby table: x(jG) → j for 0 < j < m (m = ⌈√n⌉ or --memory). Giant steps R_i = Q − i·(mG),
4 +//! i = 0, 1, …, ⌈n/m⌉; a hit x(R_i) = x(jG) means R_i = ±jG, i.e. d = i·m ± j (both verified).
5 +//! Both phases are chains advanced by a constant point, so m/S independent chains are advanced
6 +//! together with Montgomery's simultaneous inversion (S = `chains`); the giant phase is split
7 +//! across threads.
8 +
9 +use crate::curve::{Counters, Pt};
10 +use crate::instance::{isqrt_ceil, Instance};
11 +use crate::stats::FxBuild;
12 +use hashbrown::HashMap;
13 +use serde::{Deserialize, Serialize};
14 +use std::sync::atomic::{AtomicBool, Ordering};
15 +use std::sync::Mutex;
16 +use std::time::Instant;
17 +
18 +#[derive(Clone, Debug, Serialize, Deserialize)]
19 +pub struct BsgsConfig {
20 + /// number of baby steps m (default ⌈√n⌉)
21 + pub memory: Option<u64>,
22 + pub threads: usize,
23 + /// independent chains per thread for batch inversion
24 + pub chains: usize,
25 +}
26 +
27 +#[derive(Clone, Debug, Serialize, Deserialize)]
28 +pub struct BsgsResult {
29 + pub d: Option<u64>,
30 + pub ops: Counters,
31 + /// giant steps actually computed (all threads)
32 + pub iterations: u64,
33 + pub baby_steps: u64,
34 + pub giant_steps_planned: u64,
35 + pub table_entries: u64,
36 + pub table_bytes: u64,
37 + pub threads: usize,
38 + pub wall_s: f64,
39 + pub cpu_s: f64,
40 + pub build_s: f64,
41 +}
42 +
43 +/// Advance every chain by the constant point `s` (P_c ← P_c + s) with one batched inversion.
44 +/// Chains at infinity or with x(P_c) = x(s) take the slow path.
45 +fn advance_chains(inst: &Instance, pts: &mut [Pt], s: &Pt, dens: &mut [u64], scratch: &mut [u64], special: &mut [bool], c: &mut Counters) {
46 + let f = &inst.curve.f;
47 + let k = pts.len();
48 + for i in 0..k {
49 + let p = &pts[i];
50 + let sp = p.inf || p.x == s.x;
51 + special[i] = sp;
52 + dens[i] = if sp { f.one } else { f.sub(s.x, p.x) };
53 + }
54 + let (m, inv) = f.batch_inv(&mut dens[..k], &mut scratch[..k]);
55 + c.mul += m;
56 + c.inv += inv;
57 + for i in 0..k {
58 + if special[i] {
59 + pts[i] = inst.curve.add(&pts[i], s, c);
60 + } else {
61 + let lam = f.mul(f.sub(s.y, pts[i].y), dens[i]);
62 + pts[i] = inst.curve.finish_add(&pts[i], s.x, lam);
63 + c.add += 1;
64 + c.mul += 3;
65 + }
66 + }
67 +}
68 +
69 +pub fn solve_bsgs(inst: &Instance, cfg: &BsgsConfig) -> BsgsResult {
70 + let t0 = Instant::now();
71 + let cpu0 = crate::stats::thread_cpu_s();
72 + let n = inst.n.n;
73 + let mut c = Counters::default();
74 + let m = cfg.memory.unwrap_or(isqrt_ceil(n as u128) as u64).max(1).min(n);
75 + let t_total = (n as u128).div_ceil(m as u128) as u64 + 1;
76 + let threads = cfg.threads.max(1);
77 + let empty = |d: Option<u64>, c: Counters, t0: Instant, cpu0: f64| BsgsResult {
78 + d,
79 + ops: c,
80 + iterations: 0,
81 + baby_steps: m,
82 + giant_steps_planned: t_total,
83 + table_entries: 0,
84 + table_bytes: 0,
85 + threads,
86 + wall_s: t0.elapsed().as_secs_f64(),
87 + cpu_s: crate::stats::thread_cpu_s() - cpu0,
88 + build_s: 0.0,
89 + };
90 + if inst.q.inf {
91 + return empty(Some(0), c, t0, cpu0);
92 + }
93 +
94 + // ---- baby steps --------------------------------------------------------------------------
95 + let s = cfg.chains.max(1).min(m as usize);
96 + let len = (m as usize).div_ceil(s);
97 + let mut table: HashMap<u64, u64, FxBuild> = HashMap::with_capacity_and_hasher(m as usize + 1, Default::default());
98 + let mut pts: Vec<Pt> = (0..s).map(|ci| inst.curve.mul((ci * len) as u64, &inst.g, &mut c)).collect();
99 + let mut dens = vec![0u64; s];
100 + let mut scratch = vec![0u64; s];
101 + let mut special = vec![false; s];
102 + for step in 0..len {
103 + let mut active = 0;
104 + for ci in 0..s {
105 + let j = (ci * len + step) as u64;
106 + if j >= m {
107 + break;
108 + }
109 + active = ci + 1;
110 + if !pts[ci].inf {
111 + table.entry(pts[ci].x).or_insert(j);
112 + }
113 + }
114 + if active == 0 {
115 + break;
116 + }
117 + if step + 1 < len {
118 + advance_chains(inst, &mut pts[..active], &inst.g, &mut dens, &mut scratch, &mut special, &mut c);
119 + }
120 + }
121 + let build_s = t0.elapsed().as_secs_f64();
122 + let table_entries = table.len() as u64;
123 + let table_bytes = table.capacity() as u64 * (std::mem::size_of::<(u64, u64)>() as u64 + 1);
124 +
125 + // ---- giant steps (threaded) ---------------------------------------------------------------
126 + let mg = inst.curve.mul(m, &inst.g, &mut c);
127 + let neg_mg = inst.curve.neg(&mg);
128 + let found: Mutex<Option<u64>> = Mutex::new(None);
129 + let stop = AtomicBool::new(false);
130 + let per: Mutex<Vec<(Counters, u64, f64)>> = Mutex::new(Vec::new());
131 + let table = &table;
132 + let block = (t_total as usize).div_ceil(threads);
133 + std::thread::scope(|sc| {
134 + for t in 0..threads {
135 + let found = &found;
136 + let stop = &stop;
137 + let per = &per;
138 + let i_begin = (t * block) as u64;
139 + let i_end = (((t + 1) * block) as u64).min(t_total);
140 + if i_begin >= i_end {
141 + continue;
142 + }
143 + sc.spawn(move || {
144 + let cpu0 = crate::stats::thread_cpu_s();
145 + let mut c = Counters::default();
146 + let count = (i_end - i_begin) as usize;
147 + let s = cfg.chains.max(1).min(count);
148 + let len = count.div_ceil(s);
149 + // chain ci starts at R_{i_begin + ci·len} = Q − (i_begin + ci·len)·mG
150 + let mut pts: Vec<Pt> = (0..s)
151 + .map(|ci| {
152 + let i0 = i_begin + (ci * len) as u64;
153 + let off = inst.curve.mul(i0 % n, &neg_mg, &mut c);
154 + inst.curve.add(&inst.q, &off, &mut c)
155 + })
156 + .collect();
157 + let mut dens = vec![0u64; s];
158 + let mut scratch = vec![0u64; s];
159 + let mut special = vec![false; s];
160 + let mut iters = 0u64;
161 + let mut solved: Option<u64> = None;
162 + 'outer: for step in 0..len {
163 + if stop.load(Ordering::Relaxed) {
164 + break;
165 + }
166 + let mut active = 0;
167 + for ci in 0..s {
168 + let i = i_begin + (ci * len + step) as u64;
169 + if i >= i_end {
170 + break;
171 + }
172 + active = ci + 1;
173 + iters += 1;
174 + let r = pts[ci];
175 + if r.inf {
176 + // Q = i·m·G
177 + let d = inst.n.mul(i % n, m % n);
178 + if inst.verify(d, &mut c) {
179 + solved = Some(d);
180 + break 'outer;
181 + }
182 + continue;
183 + }
184 + if let Some(&j) = table.get(&r.x) {
185 + let im = inst.n.mul(i % n, m % n);
186 + for d in [inst.n.add(im, j % n), inst.n.sub(im, j % n)] {
187 + if inst.verify(d, &mut c) {
188 + solved = Some(d);
189 + break 'outer;
190 + }
191 + }
192 + }
193 + }
194 + if active == 0 {
195 + break;
196 + }
197 + if step + 1 < len {
198 + advance_chains(inst, &mut pts[..active], &neg_mg, &mut dens, &mut scratch, &mut special, &mut c);
199 + }
200 + }
201 + if let Some(d) = solved {
202 + let mut f = found.lock().unwrap();
203 + if f.is_none() {
204 + *f = Some(d);
205 + }
206 + stop.store(true, Ordering::Relaxed);
207 + }
208 + per.lock().unwrap().push((c, iters, crate::stats::thread_cpu_s() - cpu0));
209 + });
210 + }
211 + });
212 + let mut iterations = 0u64;
213 + let mut cpu_s = crate::stats::thread_cpu_s() - cpu0;
214 + for (pc, it, cpu) in per.lock().unwrap().iter() {
215 + c.merge(pc);
216 + iterations += it;
217 + cpu_s += cpu;
218 + }
219 + let d = *found.lock().unwrap();
220 + BsgsResult {
221 + d,
222 + ops: c,
223 + iterations,
224 + baby_steps: m,
225 + giant_steps_planned: t_total,
226 + table_entries,
227 + table_bytes,
228 + threads,
229 + wall_s: t0.elapsed().as_secs_f64(),
230 + cpu_s,
231 + build_s,
232 + }
233 +}
added experiments/synthetic_ecdlp/rho_engine/src/curve.rs +243 −0
@@ -0,0 +1,243 @@
1 +//! Short Weierstrass curve y² = x³ + ax + b over F_p (p < 2^62), affine points in Montgomery form.
2 +//!
3 +//! Conventions match `src/wp34/ec.py`: affine coordinates, `None`/`Pt::INF` is the point at infinity,
4 +//! the group law counts one group addition (`add`) or doubling (`dbl`) plus one field inversion (`inv`)
5 +//! and the field multiplications (`mul`, squarings included) actually performed.
6 +
7 +use crate::field::Field;
8 +use serde::{Deserialize, Serialize};
9 +
10 +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
11 +pub struct Counters {
12 + pub add: u64,
13 + pub dbl: u64,
14 + pub inv: u64,
15 + pub mul: u64,
16 +}
17 +
18 +impl Counters {
19 + #[inline]
20 + pub fn merge(&mut self, o: &Counters) {
21 + self.add += o.add;
22 + self.dbl += o.dbl;
23 + self.inv += o.inv;
24 + self.mul += o.mul;
25 + }
26 + pub fn group_ops(&self) -> u64 {
27 + self.add + self.dbl
28 + }
29 +}
30 +
31 +/// Affine point in Montgomery form; `inf` marks the point at infinity.
32 +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
33 +pub struct Pt {
34 + pub x: u64,
35 + pub y: u64,
36 + pub inf: bool,
37 +}
38 +
39 +impl Pt {
40 + pub const INF: Pt = Pt { x: 0, y: 0, inf: true };
41 + #[inline(always)]
42 + pub fn new(x: u64, y: u64) -> Pt {
43 + Pt { x, y, inf: false }
44 + }
45 +}
46 +
47 +#[derive(Clone, Copy, Debug)]
48 +pub struct Curve {
49 + pub f: Field,
50 + /// a, b in Montgomery form
51 + pub a: u64,
52 + pub b: u64,
53 + /// standard-form a, b (for JSON / checks)
54 + pub a_std: u64,
55 + pub b_std: u64,
56 +}
57 +
58 +impl Curve {
59 + pub fn new(p: u64, a: u64, b: u64) -> Curve {
60 + let f = Field::new(p);
61 + assert!(a < p && b < p, "a, b must be reduced mod p");
62 + // discriminant check: 4a³ + 27b² ≠ 0
63 + let am = f.to_mont(a);
64 + let bm = f.to_mont(b);
65 + let a3 = f.mul(f.mul(am, am), am);
66 + let d = f.add(f.mul(f.to_mont(4 % p), a3), f.mul(f.to_mont(27 % p), f.sqr(bm)));
67 + assert!(d != 0, "singular curve");
68 + Curve { f, a: am, b: bm, a_std: a, b_std: b }
69 + }
70 +
71 + #[inline]
72 + pub fn p(&self) -> u64 {
73 + self.f.p
74 + }
75 +
76 + pub fn point_from_std(&self, x: u64, y: u64) -> Pt {
77 + Pt::new(self.f.to_mont(x % self.f.p), self.f.to_mont(y % self.f.p))
78 + }
79 +
80 + pub fn point_to_std(&self, p: &Pt) -> Option<(u64, u64)> {
81 + if p.inf {
82 + None
83 + } else {
84 + Some((self.f.from_mont(p.x), self.f.from_mont(p.y)))
85 + }
86 + }
87 +
88 + pub fn is_on_curve(&self, pt: &Pt) -> bool {
89 + if pt.inf {
90 + return true;
91 + }
92 + let f = &self.f;
93 + let lhs = f.sqr(pt.y);
94 + let rhs = f.add(f.add(f.mul(f.sqr(pt.x), pt.x), f.mul(self.a, pt.x)), self.b);
95 + lhs == rhs
96 + }
97 +
98 + #[inline(always)]
99 + pub fn neg(&self, pt: &Pt) -> Pt {
100 + if pt.inf {
101 + *pt
102 + } else {
103 + Pt::new(pt.x, self.f.neg(pt.y))
104 + }
105 + }
106 +
107 + /// Full affine addition with all special cases (one inversion). Counted.
108 + pub fn add(&self, p: &Pt, q: &Pt, c: &mut Counters) -> Pt {
109 + if p.inf {
110 + return *q;
111 + }
112 + if q.inf {
113 + return *p;
114 + }
115 + let f = &self.f;
116 + if p.x == q.x {
117 + if f.add(p.y, q.y) == 0 {
118 + return Pt::INF;
119 + }
120 + return self.dbl(p, c);
121 + }
122 + c.add += 1;
123 + c.inv += 1;
124 + c.mul += 3;
125 + let lam = f.mul(f.sub(q.y, p.y), f.inv(f.sub(q.x, p.x)));
126 + self.finish_add(p, q.x, lam)
127 + }
128 +
129 + /// Doubling with all special cases (one inversion). Counted.
130 + pub fn dbl(&self, p: &Pt, c: &mut Counters) -> Pt {
131 + if p.inf || p.y == 0 {
132 + return Pt::INF;
133 + }
134 + let f = &self.f;
135 + c.dbl += 1;
136 + c.inv += 1;
137 + c.mul += 4;
138 + let num = f.add(f.mul(f.to_mont(3), f.sqr(p.x)), self.a);
139 + let lam = f.mul(num, f.inv(f.dbl(p.y)));
140 + self.finish_dbl(p, lam)
141 + }
142 +
143 + /// Given the slope λ of the chord through p and q (q.x = `qx`), compute p + q.
144 + /// Costs 2 field multiplications (λ², λ·(x1−x3)); counted by the caller.
145 + #[inline(always)]
146 + pub fn finish_add(&self, p: &Pt, qx: u64, lam: u64) -> Pt {
147 + let f = &self.f;
148 + let x3 = f.sub(f.sub(f.sqr(lam), p.x), qx);
149 + let y3 = f.sub(f.mul(lam, f.sub(p.x, x3)), p.y);
150 + Pt::new(x3, y3)
151 + }
152 +
153 + /// Given the tangent slope λ at p, compute 2p (2 multiplications, counted by caller).
154 + #[inline(always)]
155 + pub fn finish_dbl(&self, p: &Pt, lam: u64) -> Pt {
156 + let f = &self.f;
157 + let x3 = f.sub(f.sqr(lam), f.dbl(p.x));
158 + let y3 = f.sub(f.mul(lam, f.sub(p.x, x3)), p.y);
159 + Pt::new(x3, y3)
160 + }
161 +
162 + /// Numerator of the tangent slope: 3x² + a (one squaring + one multiplication, counted by caller).
163 + #[inline(always)]
164 + pub fn tangent_num(&self, p: &Pt) -> u64 {
165 + let f = &self.f;
166 + f.add(f.mul(f.to_mont(3), f.sqr(p.x)), self.a)
167 + }
168 +
169 + /// Left-to-right double-and-add scalar multiplication (k any u64). Counted.
170 + pub fn mul(&self, k: u64, p: &Pt, c: &mut Counters) -> Pt {
171 + if p.inf || k == 0 {
172 + return Pt::INF;
173 + }
174 + let mut r = Pt::INF;
175 + let bits = 64 - k.leading_zeros();
176 + for i in (0..bits).rev() {
177 + r = self.dbl(&r, c);
178 + if (k >> i) & 1 == 1 {
179 + r = self.add(&r, p, c);
180 + }
181 + }
182 + r
183 + }
184 +
185 + pub fn eq(&self, p: &Pt, q: &Pt) -> bool {
186 + if p.inf || q.inf {
187 + return p.inf && q.inf;
188 + }
189 + p.x == q.x && p.y == q.y
190 + }
191 +
192 + /// Random point by lifting random x (standard-form output converted to Montgomery).
193 + pub fn random_point<R: rand::Rng>(&self, rng: &mut R) -> Pt {
194 + let f = &self.f;
195 + loop {
196 + let x = rng.random_range(0..f.p);
197 + let xm = f.to_mont(x);
198 + let rhs = f.add(f.add(f.mul(f.sqr(xm), xm), f.mul(self.a, xm)), self.b);
199 + if let Some(y) = f.sqrt_plain(f.from_mont(rhs)) {
200 + let y = if rng.random_bool(0.5) { y } else { (f.p - y) % f.p };
201 + return Pt::new(xm, f.to_mont(y));
202 + }
203 + }
204 + }
205 +}
206 +
207 +/// Fixed-base comb table: 16 nibble positions × 16 entries, so that k·P costs ≤ 15 additions
208 +/// (plus the table precomputation). Used to start random walks cheaply (u·G + v·Q).
209 +pub struct CombTable {
210 + pub table: Vec<[Pt; 16]>, // table[i][d] = d·16^i·P
211 +}
212 +
213 +impl CombTable {
214 + pub fn new(curve: &Curve, p: &Pt, c: &mut Counters) -> CombTable {
215 + let mut table = Vec::with_capacity(16);
216 + let mut base = *p; // 16^i P
217 + for _ in 0..16 {
218 + let mut row = [Pt::INF; 16];
219 + for d in 1..16 {
220 + row[d] = curve.add(&row[d - 1], &base, c);
221 + }
222 + // next base = 16·base
223 + let mut nb = base;
224 + for _ in 0..4 {
225 + nb = curve.dbl(&nb, c);
226 + }
227 + base = nb;
228 + table.push(row);
229 + }
230 + CombTable { table }
231 + }
232 +
233 + pub fn mul(&self, curve: &Curve, k: u64, c: &mut Counters) -> Pt {
234 + let mut r = Pt::INF;
235 + for i in 0..16 {
236 + let d = ((k >> (4 * i)) & 0xF) as usize;
237 + if d != 0 {
238 + r = curve.add(&r, &self.table[i][d], c);
239 + }
240 + }
241 + r
242 + }
243 +}
added experiments/synthetic_ecdlp/rho_engine/src/field.rs +353 −0
@@ -0,0 +1,353 @@
1 +//! Prime-field arithmetic for p < 2^62 in Montgomery form (R = 2^64).
2 +//!
3 +//! All field elements handled by the walks are kept in Montgomery representation
4 +//! `a·R mod p`; conversion happens only at the boundaries (CLI input/output, JSON) and
5 +//! where a canonical (standard-form) value is needed for an ordering decision.
6 +//! Products of two residues fit in a u128; the Montgomery reduction needs one
7 +//! conditional subtraction because p < 2^62 guarantees (a·b + m·p)/R < 2p.
8 +//! Exact integer arithmetic only (CONVENTIONS.md §1).
9 +
10 +#[derive(Clone, Copy, Debug)]
11 +pub struct Field {
12 + pub p: u64,
13 + /// -p^{-1} mod 2^64
14 + pub pinv: u64,
15 + /// R mod p (Montgomery form of 1)
16 + pub one: u64,
17 + /// R^2 mod p
18 + pub r2: u64,
19 + /// R^3 mod p (used to convert a plain inverse back to Montgomery form)
20 + pub r3: u64,
21 +}
22 +
23 +impl Field {
24 + pub fn new(p: u64) -> Field {
25 + assert!(p > 3 && p & 1 == 1, "p must be an odd prime > 3");
26 + assert!(p < (1u64 << 62), "p must be < 2^62");
27 + // Newton iteration for p^{-1} mod 2^64
28 + let mut inv: u64 = 1;
29 + for _ in 0..6 {
30 + inv = inv.wrapping_mul(2u64.wrapping_sub(p.wrapping_mul(inv)));
31 + }
32 + debug_assert_eq!(p.wrapping_mul(inv), 1);
33 + let pinv = inv.wrapping_neg();
34 + let r = ((1u128 << 64) % p as u128) as u64;
35 + let r2 = ((r as u128 * r as u128) % p as u128) as u64;
36 + let r3 = ((r2 as u128 * r as u128) % p as u128) as u64;
37 + Field { p, pinv, one: r, r2, r3 }
38 + }
39 +
40 + /// Montgomery reduction of t < p·2^64: returns t·R^{-1} mod p.
41 + #[inline(always)]
42 + pub fn redc(&self, t: u128) -> u64 {
43 + let m = (t as u64).wrapping_mul(self.pinv);
44 + let u = ((t + (m as u128) * (self.p as u128)) >> 64) as u64;
45 + if u >= self.p {
46 + u - self.p
47 + } else {
48 + u
49 + }
50 + }
51 +
52 + #[inline(always)]
53 + pub fn mul(&self, a: u64, b: u64) -> u64 {
54 + self.redc(a as u128 * b as u128)
55 + }
56 +
57 + #[inline(always)]
58 + pub fn sqr(&self, a: u64) -> u64 {
59 + self.redc(a as u128 * a as u128)
60 + }
61 +
62 + #[inline(always)]
63 + pub fn add(&self, a: u64, b: u64) -> u64 {
64 + let s = a + b; // < 2^63, no overflow
65 + if s >= self.p {
66 + s - self.p
67 + } else {
68 + s
69 + }
70 + }
71 +
72 + #[inline(always)]
73 + pub fn sub(&self, a: u64, b: u64) -> u64 {
74 + if a >= b {
75 + a - b
76 + } else {
77 + a + self.p - b
78 + }
79 + }
80 +
81 + #[inline(always)]
82 + pub fn neg(&self, a: u64) -> u64 {
83 + if a == 0 {
84 + 0
85 + } else {
86 + self.p - a
87 + }
88 + }
89 +
90 + #[inline(always)]
91 + pub fn dbl(&self, a: u64) -> u64 {
92 + self.add(a, a)
93 + }
94 +
95 + /// Standard integer a (0 ≤ a < p) → Montgomery form.
96 + #[inline(always)]
97 + pub fn to_mont(&self, a: u64) -> u64 {
98 + debug_assert!(a < self.p);
99 + self.mul(a, self.r2)
100 + }
101 +
102 + /// Montgomery form → standard integer.
103 + #[inline(always)]
104 + pub fn from_mont(&self, a: u64) -> u64 {
105 + self.redc(a as u128)
106 + }
107 +
108 + /// Inverse of a standard-form element by the extended Euclidean algorithm (a ≠ 0).
109 + pub fn inv_plain(&self, a: u64) -> u64 {
110 + debug_assert!(a != 0 && a < self.p);
111 + let (mut r0, mut r1) = (self.p as i64, a as i64);
112 + let (mut t0, mut t1) = (0i64, 1i64);
113 + while r1 != 0 {
114 + let q = r0 / r1;
115 + let r2 = r0 - q * r1;
116 + r0 = r1;
117 + r1 = r2;
118 + let t2 = t0 - q * t1;
119 + t0 = t1;
120 + t1 = t2;
121 + }
122 + debug_assert_eq!(r0, 1, "inverse of non-unit");
123 + if t0 < 0 {
124 + (t0 + self.p as i64) as u64
125 + } else {
126 + t0 as u64
127 + }
128 + }
129 +
130 + /// Inverse of a Montgomery-form element, result in Montgomery form.
131 + /// inv_plain(aR) = a^{-1} R^{-1}; multiplying by R^3 (one Montgomery product) gives a^{-1} R.
132 + #[inline]
133 + pub fn inv(&self, a: u64) -> u64 {
134 + let t = self.inv_plain(a);
135 + self.mul(t, self.r3)
136 + }
137 +
138 + /// Montgomery's simultaneous inversion: replaces every element of `xs` (all non-zero,
139 + /// Montgomery form) by its inverse, using 3(k−1) multiplications and a single inversion.
140 + /// `scratch` must have length ≥ xs.len(). Returns (muls, inversions) performed.
141 + pub fn batch_inv(&self, xs: &mut [u64], scratch: &mut [u64]) -> (u64, u64) {
142 + let k = xs.len();
143 + if k == 0 {
144 + return (0, 0);
145 + }
146 + scratch[0] = xs[0];
147 + for i in 1..k {
148 + scratch[i] = self.mul(scratch[i - 1], xs[i]);
149 + }
150 + let mut acc = self.inv(scratch[k - 1]);
151 + for i in (1..k).rev() {
152 + let inv_i = self.mul(acc, scratch[i - 1]);
153 + acc = self.mul(acc, xs[i]);
154 + xs[i] = inv_i;
155 + }
156 + xs[0] = acc;
157 + ((3 * (k as u64 - 1)) + 1, 1)
158 + }
159 +
160 + pub fn pow_plain(&self, a: u64, mut e: u64) -> u64 {
161 + let mut base = self.to_mont(a);
162 + let mut acc = self.one;
163 + while e > 0 {
164 + if e & 1 == 1 {
165 + acc = self.mul(acc, base);
166 + }
167 + base = self.sqr(base);
168 + e >>= 1;
169 + }
170 + self.from_mont(acc)
171 + }
172 +
173 + /// Legendre symbol as 0, 1 or p−1 (standard form input).
174 + pub fn legendre(&self, a: u64) -> u64 {
175 + self.pow_plain(a % self.p, (self.p - 1) / 2)
176 + }
177 +
178 + /// Tonelli–Shanks square root of a standard-form element; None if non-residue.
179 + pub fn sqrt_plain(&self, a: u64) -> Option<u64> {
180 + let p = self.p;
181 + let a = a % p;
182 + if a == 0 {
183 + return Some(0);
184 + }
185 + if self.legendre(a) != 1 {
186 + return None;
187 + }
188 + if p % 4 == 3 {
189 + return Some(self.pow_plain(a, (p + 1) / 4));
190 + }
191 + let (mut q, mut s) = (p - 1, 0u32);
192 + while q % 2 == 0 {
193 + q /= 2;
194 + s += 1;
195 + }
196 + let mut z = 2u64;
197 + while self.legendre(z) != p - 1 {
198 + z += 1;
199 + }
200 + let mut m = s;
201 + let mut c = self.to_mont(self.pow_plain(z, q));
202 + let mut t = self.to_mont(self.pow_plain(a, q));
203 + let mut r = self.to_mont(self.pow_plain(a, (q + 1) / 2));
204 + while t != self.one {
205 + let mut i = 0u32;
206 + let mut t2 = t;
207 + while t2 != self.one {
208 + t2 = self.sqr(t2);
209 + i += 1;
210 + }
211 + let mut b = c;
212 + for _ in 0..(m - i - 1) {
213 + b = self.sqr(b);
214 + }
215 + m = i;
216 + c = self.sqr(b);
217 + t = self.mul(t, c);
218 + r = self.mul(r, b);
219 + }
220 + Some(self.from_mont(r))
221 + }
222 +}
223 +
224 +/// Arithmetic modulo the (prime) group order n < 2^62 — used for the scalar bookkeeping.
225 +#[derive(Clone, Copy, Debug)]
226 +pub struct Modn {
227 + pub n: u64,
228 +}
229 +
230 +impl Modn {
231 + pub fn new(n: u64) -> Modn {
232 + assert!(n > 1 && n < (1u64 << 62));
233 + Modn { n }
234 + }
235 + #[inline(always)]
236 + pub fn add(&self, a: u64, b: u64) -> u64 {
237 + let s = a + b;
238 + if s >= self.n {
239 + s - self.n
240 + } else {
241 + s
242 + }
243 + }
244 + #[inline(always)]
245 + pub fn sub(&self, a: u64, b: u64) -> u64 {
246 + if a >= b {
247 + a - b
248 + } else {
249 + a + self.n - b
250 + }
251 + }
252 + #[inline(always)]
253 + pub fn neg(&self, a: u64) -> u64 {
254 + if a == 0 {
255 + 0
256 + } else {
257 + self.n - a
258 + }
259 + }
260 + #[inline(always)]
261 + pub fn mul(&self, a: u64, b: u64) -> u64 {
262 + ((a as u128 * b as u128) % self.n as u128) as u64
263 + }
264 + #[inline(always)]
265 + pub fn dbl(&self, a: u64) -> u64 {
266 + self.add(a, a)
267 + }
268 + pub fn inv(&self, a: u64) -> Option<u64> {
269 + if a == 0 {
270 + return None;
271 + }
272 + let (mut r0, mut r1) = (self.n as i64, a as i64);
273 + let (mut t0, mut t1) = (0i64, 1i64);
274 + while r1 != 0 {
275 + let q = r0 / r1;
276 + let r2 = r0 - q * r1;
277 + r0 = r1;
278 + r1 = r2;
279 + let t2 = t0 - q * t1;
280 + t0 = t1;
281 + t1 = t2;
282 + }
283 + if r0 != 1 {
284 + return None;
285 + }
286 + Some(if t0 < 0 { (t0 + self.n as i64) as u64 } else { t0 as u64 })
287 + }
288 + /// Solve a·d ≡ b (mod n); None if a ≡ 0.
289 + pub fn solve(&self, a: u64, b: u64) -> Option<u64> {
290 + self.inv(a).map(|ai| self.mul(ai, b))
291 + }
292 + pub fn pow(&self, a: u64, mut e: u64) -> u64 {
293 + let mut base = a % self.n;
294 + let mut acc = 1u64;
295 + while e > 0 {
296 + if e & 1 == 1 {
297 + acc = self.mul(acc, base);
298 + }
299 + base = self.mul(base, base);
300 + e >>= 1;
301 + }
302 + acc
303 + }
304 +}
305 +
306 +#[cfg(test)]
307 +mod tests {
308 + use super::*;
309 +
310 + #[test]
311 + fn montgomery_roundtrip_and_inverse() {
312 + for &p in &[1_000_003u64, 4611686018427387847u64 /* 2^62-57 */, 2305843009213693951u64] {
313 + let f = Field::new(p);
314 + let mut x = 123456789u64 % p;
315 + for _ in 0..1000 {
316 + x = (x as u128 * 6364136223846793005u128 % p as u128) as u64 + 1;
317 + x %= p;
318 + if x == 0 {
319 + continue;
320 + }
321 + let xm = f.to_mont(x);
322 + assert_eq!(f.from_mont(xm), x);
323 + let y = f.inv(xm);
324 + assert_eq!(f.from_mont(f.mul(xm, y)), 1);
325 + let prod = f.from_mont(f.mul(xm, xm));
326 + assert_eq!(prod, ((x as u128 * x as u128) % p as u128) as u64);
327 + }
328 + }
329 + }
330 +
331 + #[test]
332 + fn batch_inverse_matches_single() {
333 + let p = 4611686018427387847u64;
334 + let f = Field::new(p);
335 + let mut xs: Vec<u64> = (1..300u64).map(|i| f.to_mont(i * 987654321 % p)).collect();
336 + let expect: Vec<u64> = xs.iter().map(|&x| f.inv(x)).collect();
337 + let mut scratch = vec![0u64; xs.len()];
338 + f.batch_inv(&mut xs, &mut scratch);
339 + assert_eq!(xs, expect);
340 + }
341 +
342 + #[test]
343 + fn sqrt_works() {
344 + for &p in &[1_000_003u64, 1_000_033u64, 4611686018427387847u64] {
345 + let f = Field::new(p);
346 + for a in 2..200u64 {
347 + if let Some(r) = f.sqrt_plain(a) {
348 + assert_eq!(((r as u128 * r as u128) % p as u128) as u64, a % p);
349 + }
350 + }
351 + }
352 + }
353 +}
added experiments/synthetic_ecdlp/rho_engine/src/instance.rs +128 −0
@@ -0,0 +1,128 @@
1 +//! An ECDLP instance Q = d·G on a prime-order curve, plus the optional order-3 automorphism data.
2 +
3 +use crate::curve::{Counters, Curve, Pt};
4 +use crate::field::Modn;
5 +
6 +#[derive(Clone, Copy, Debug)]
7 +pub struct Instance {
8 + pub curve: Curve,
9 + pub n: Modn,
10 + pub g: Pt,
11 + pub q: Pt,
12 + /// β (Montgomery form) with (βx, y) = λ·(x, y); only for a = 0, p ≡ 1 (mod 3), n ≡ 1 (mod 3)
13 + pub beta: Option<u64>,
14 + pub lambda: Option<u64>,
15 +}
16 +
17 +impl Instance {
18 + #[allow(clippy::too_many_arguments)]
19 + pub fn new(p: u64, a: u64, b: u64, n: u64, gx: u64, gy: u64, qx: u64, qy: u64) -> Result<Instance, String> {
20 + let curve = Curve::new(p, a, b);
21 + let g = curve.point_from_std(gx, gy);
22 + let q = curve.point_from_std(qx, qy);
23 + if !curve.is_on_curve(&g) {
24 + return Err("G is not on the curve".into());
25 + }
26 + if !curve.is_on_curve(&q) {
27 + return Err("Q is not on the curve".into());
28 + }
29 + if g.inf {
30 + return Err("G must not be the point at infinity".into());
31 + }
32 + let mut c = Counters::default();
33 + if !curve.mul(n, &g, &mut c).inf {
34 + return Err("n·G != O: n is not the order of G".into());
35 + }
36 + Ok(Instance { curve, n: Modn::new(n), g, q, beta: None, lambda: None })
37 + }
38 +
39 + /// Install (β, λ) and verify φ(G) = (β·gx, gy) = λ·G on this instance; abort otherwise.
40 + pub fn with_aut3(mut self, beta_std: u64, lambda: u64) -> Result<Instance, String> {
41 + let f = &self.curve.f;
42 + if self.curve.a_std != 0 {
43 + return Err("--aut3 requires a = 0 (j-invariant 0)".into());
44 + }
45 + if f.p % 3 != 1 || self.n.n % 3 != 1 {
46 + return Err("--aut3 requires p ≡ 1 (mod 3) and n ≡ 1 (mod 3)".into());
47 + }
48 + if beta_std == 0 || beta_std >= f.p || beta_std == 1 {
49 + return Err("beta must satisfy 1 < beta < p".into());
50 + }
51 + if lambda <= 1 || lambda >= self.n.n {
52 + return Err("lambda must satisfy 1 < lambda < n".into());
53 + }
54 + if f.pow_plain(beta_std, 3) != 1 {
55 + return Err("beta^3 != 1 mod p".into());
56 + }
57 + if self.n.pow(lambda, 3) != 1 {
58 + return Err("lambda^3 != 1 mod n".into());
59 + }
60 + let beta = f.to_mont(beta_std);
61 + let phi_g = Pt::new(f.mul(beta, self.g.x), self.g.y);
62 + let mut c = Counters::default();
63 + let lam_g = self.curve.mul(lambda, &self.g, &mut c);
64 + if !self.curve.eq(&phi_g, &lam_g) {
65 + return Err("(beta*gx, gy) != lambda*G: wrong (beta, lambda) pair for this G".into());
66 + }
67 + self.beta = Some(beta);
68 + self.lambda = Some(lambda);
69 + Ok(self)
70 + }
71 +
72 + /// Check d·G == Q (counted in `c`).
73 + pub fn verify(&self, d: u64, c: &mut Counters) -> bool {
74 + let r = self.curve.mul(d % self.n.n, &self.g, c);
75 + self.curve.eq(&r, &self.q)
76 + }
77 +
78 + /// ⌈log2 n⌉
79 + pub fn bits(&self) -> u32 {
80 + 64 - self.n.n.leading_zeros()
81 + }
82 +}
83 +
84 +/// Integer square root (floor) of a u128.
85 +pub fn isqrt(n: u128) -> u128 {
86 + if n < 2 {
87 + return n;
88 + }
89 + let mut x = (n as f64).sqrt() as u128; // float only as a starting guess; corrected exactly below
90 + while x * x > n {
91 + x -= 1;
92 + }
93 + while (x + 1) * (x + 1) <= n {
94 + x += 1;
95 + }
96 + x
97 +}
98 +
99 +/// ⌈√n⌉
100 +pub fn isqrt_ceil(n: u128) -> u128 {
101 + let r = isqrt(n);
102 + if r * r == n {
103 + r
104 + } else {
105 + r + 1
106 + }
107 +}
108 +
109 +/// 64-bit mixing function (splitmix64 finaliser) used for partition / distinguished-point tests.
110 +#[inline(always)]
111 +pub fn mix64(x: u64) -> u64 {
112 + let mut z = x.wrapping_add(0x9E3779B97F4A7C15);
113 + z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
114 + z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
115 + z ^ (z >> 31)
116 +}
117 +
118 +/// Partition index in [0, r) from the high 32 bits of mix64(x) (multiply-high, no division).
119 +#[inline(always)]
120 +pub fn partition(x: u64, r: u64) -> u32 {
121 + (((mix64(x) >> 32) * r) >> 32) as u32
122 +}
123 +
124 +/// Distinguished-point predicate: the low `dp_bits` bits of mix64(x) are zero.
125 +#[inline(always)]
126 +pub fn is_distinguished(x: u64, dp_mask: u64) -> bool {
127 + (mix64(x) & dp_mask) == 0
128 +}
added experiments/synthetic_ecdlp/rho_engine/src/kangaroo.rs +288 −0
@@ -0,0 +1,288 @@
1 +//! Pollard's kangaroo (lambda) method for d ∈ [lo, hi], in the van Oorschot–Wiener parallel form
2 +//! with tame/wild herds and distinguished points (Pollard 2000 for the power-of-two jump set).
3 +//!
4 +//! Tame kangaroo c starts at position t_c = lo + ⌊w/2⌋ + c·s (point t_c·G); wild kangaroo c starts
5 +//! at offset o_c = c·s (point Q + o_c·G); s = ⌈mean jump / (N/2)⌉ with N kangaroos in total.
6 +//! Jump set J = {2^0 G, …, 2^{k−1} G}, j = h(x) ∈ [0, k), k chosen so that (2^k − 1)/k ≈ N·√w/4
7 +//! (which is √w/2 for the classical single tame/wild pair). Every kangaroo keeps its position mod n;
8 +//! a tame/wild distinguished-point collision with positions (t, o) gives d = t − o (same y) or
9 +//! d = −t − o (opposite y). Same-herd collisions reposition the newcomer by a random jump.
10 +
11 +use crate::curve::{CombTable, Counters, Pt};
12 +use crate::instance::{is_distinguished, isqrt, partition, Instance};
13 +use crate::rho::{RhoConfig, WalkStats};
14 +use crate::stats::FxBuild;
15 +use hashbrown::HashMap;
16 +use rand::{Rng, SeedableRng};
17 +use rand_chacha::ChaCha20Rng;
18 +use serde::{Deserialize, Serialize};
19 +use std::sync::atomic::{AtomicBool, Ordering};
20 +use std::sync::Mutex;
21 +use std::time::Instant;
22 +
23 +#[derive(Clone, Debug, Serialize, Deserialize)]
24 +pub struct KangarooConfig {
25 + pub lo: u64,
26 + pub hi: u64,
27 + pub threads: usize,
28 + /// kangaroos per thread (0 = automatic, even number, half tame / half wild)
29 + pub walks: usize,
30 + /// 0 = automatic
31 + pub dp_bits: u32,
32 + pub seed: u64,
33 +}
34 +
35 +#[derive(Clone, Debug, Serialize, Deserialize)]
36 +pub struct KangarooResult {
37 + pub d: Option<u64>,
38 + pub stats: WalkStats,
39 + pub dp_count: u64,
40 + pub dp_bytes: u64,
41 + pub table_entries: u64,
42 + pub threads: usize,
43 + pub kangaroos_per_thread: usize,
44 + pub dp_bits: u32,
45 + pub jump_count: u32,
46 + pub mean_jump: u64,
47 + pub interval_width: u64,
48 + pub wall_s: f64,
49 +}
50 +
51 +#[derive(Clone, Copy)]
52 +struct Roo {
53 + p: Pt,
54 + dist: u64,
55 + tame: bool,
56 + /// total distance jumped since the last (re)start; a kangaroo that travels much further than
57 + /// the interval width has lost its herd and is restarted at a fresh random position
58 + travel: u128,
59 +}
60 +
61 +#[derive(Clone, Copy)]
62 +struct DpEntry {
63 + y: u64,
64 + dist: u64,
65 + tame: bool,
66 +}
67 +
68 +pub fn auto_kangaroos(w: u64) -> usize {
69 + let bits = 64 - w.leading_zeros();
70 + let k = 1usize << (bits / 4).saturating_sub(2).min(6);
71 + k.clamp(2, 64).next_multiple_of(2)
72 +}
73 +
74 +/// Pick k with (2^k − 1)/k closest to `target`.
75 +pub fn choose_jump_count(target: u64) -> u32 {
76 + let mut best = 1u32;
77 + let mut best_err = u128::MAX;
78 + for k in 1..=62u32 {
79 + let mean = (((1u128 << k) - 1) / k as u128) as u128;
80 + let err = mean.abs_diff(target as u128);
81 + if err < best_err {
82 + best_err = err;
83 + best = k;
84 + }
85 + }
86 + best
87 +}
88 +
89 +pub fn solve_kangaroo(inst: &Instance, cfg: &KangarooConfig) -> KangarooResult {
90 + let t0 = Instant::now();
91 + let n = inst.n.n;
92 + assert!(cfg.lo <= cfg.hi, "lo must be ≤ hi");
93 + let w = cfg.hi - cfg.lo + 1;
94 + let wbits = 64 - w.leading_zeros();
95 + let threads = cfg.threads.max(1);
96 + let per_thread = if cfg.walks == 0 { auto_kangaroos(w) } else { cfg.walks.max(2).next_multiple_of(2) };
97 + let total_roos = (threads * per_thread) as u64;
98 + let dp_bits = if cfg.dp_bits == 0 { RhoConfig::default_dp_bits(wbits) } else { cfg.dp_bits };
99 + let dp_mask = (1u64 << dp_bits.min(63)) - 1;
100 + let sqrt_w = isqrt(w as u128) as u64;
101 + let target_mean = (total_roos * sqrt_w / 4).max(1);
102 + let k = choose_jump_count(target_mean);
103 + let mean_jump = (((1u128 << k) - 1) / k as u128) as u64;
104 + let spacing = (mean_jump / (total_roos / 2)).max(1);
105 +
106 + let mut setup = Counters::default();
107 + let comb_g = CombTable::new(&inst.curve, &inst.g, &mut setup);
108 + let jumps: Vec<Pt> = (0..k).map(|i| comb_g.mul(&inst.curve, (1u64 << i) % n, &mut setup)).collect();
109 + let mut total = WalkStats { fruitless_by_length: vec![], ..Default::default() };
110 + total.ops.merge(&setup);
111 +
112 + if inst.q.inf {
113 + return KangarooResult { d: Some(0), stats: total, dp_count: 0, dp_bytes: 0, table_entries: 0, threads, kangaroos_per_thread: per_thread, dp_bits, jump_count: k, mean_jump, interval_width: w, wall_s: t0.elapsed().as_secs_f64() };
114 + }
115 +
116 + let expected_dps = 2.0 * (w as f64).sqrt() / (1u64 << dp_bits.min(63)) as f64;
117 + let table: Mutex<HashMap<u64, DpEntry, FxBuild>> = Mutex::new(HashMap::with_capacity_and_hasher((expected_dps * 3.0) as usize + 1024, Default::default()));
118 + let found: Mutex<Option<u64>> = Mutex::new(None);
119 + let stop = AtomicBool::new(false);
120 + let stats: Mutex<Vec<WalkStats>> = Mutex::new(Vec::new());
121 + let mid = cfg.lo + w / 2;
122 + let jumps = &jumps;
123 + let comb_g = &comb_g;
124 +
125 + std::thread::scope(|s| {
126 + for t in 0..threads {
127 + let table = &table;
128 + let found = &found;
129 + let stop = &stop;
130 + let stats = &stats;
131 + s.spawn(move || {
132 + let cpu0 = crate::stats::thread_cpu_s();
133 + let mut seed = [0u8; 32];
134 + seed[..8].copy_from_slice(&cfg.seed.to_le_bytes());
135 + seed[8..16].copy_from_slice(&(t as u64).to_le_bytes());
136 + seed[16..24].copy_from_slice(b"kangaroo");
137 + let mut rng = ChaCha20Rng::from_seed(seed);
138 + let mut st = WalkStats { fruitless_by_length: vec![], ..Default::default() };
139 + let f = &inst.curve.f;
140 + let curve = &inst.curve;
141 + let half = per_thread / 2;
142 + let mut roos: Vec<Roo> = Vec::with_capacity(per_thread);
143 + let mut solved: Option<u64> = None;
144 + let start = |st: &mut WalkStats, tame: bool, dist: u64| -> Roo {
145 + let base = comb_g.mul(curve, dist % n, &mut st.ops);
146 + let p = if tame { base } else { curve.add(&inst.q, &base, &mut st.ops) };
147 + Roo { p, dist: dist % n, tame, travel: 0 }
148 + };
149 + // random fresh start for a lost / merged kangaroo
150 + let fresh = |st: &mut WalkStats, rng: &mut ChaCha20Rng, tame: bool| -> Roo {
151 + let off = rng.random_range(0..w);
152 + let dist = if tame { (cfg.lo as u128 + off as u128) % n as u128 } else { off as u128 % n as u128 } as u64;
153 + st.restarts += 1;
154 + start(st, tame, dist)
155 + };
156 + let max_travel: u128 = 3 * w as u128 + 4 * (mean_jump as u128) * (1u128 << dp_bits.min(40));
157 + for c in 0..per_thread {
158 + let gc = (t * half + c % half) as u64;
159 + let tame = c < half;
160 + let dist = if tame { (mid as u128 + gc as u128 * spacing as u128) % n as u128 } else { (gc as u128 * spacing as u128) % n as u128 } as u64;
161 + let r = start(&mut st, tame, dist);
162 + if r.p.inf {
163 + // wild: Q = −dist·G ⇒ d = −dist ; tame: dist ≡ 0 — harmless, re-seed
164 + if !tame {
165 + let d = inst.n.neg(r.dist);
166 + if inst.verify(d, &mut st.ops) {
167 + solved = Some(d);
168 + }
169 + }
170 + let fr = fresh(&mut st, &mut rng, tame);
171 + roos.push(fr);
172 + } else {
173 + roos.push(r);
174 + }
175 + }
176 + let mut dens = vec![0u64; per_thread];
177 + let mut scratch = vec![0u64; per_thread];
178 + let mut js = vec![0u32; per_thread];
179 + let mut special = vec![false; per_thread];
180 + let mut dps: Vec<(u64, DpEntry)> = Vec::new();
181 + 'main: while solved.is_none() && !stop.load(Ordering::Relaxed) {
182 + for i in 0..per_thread {
183 + let j = partition(roos[i].p.x, k as u64);
184 + js[i] = j;
185 + let jp = &jumps[j as usize];
186 + let sp = roos[i].p.inf || roos[i].p.x == jp.x;
187 + special[i] = sp;
188 + dens[i] = if sp { f.one } else { f.sub(jp.x, roos[i].p.x) };
189 + }
190 + let (m, inv) = f.batch_inv(&mut dens[..per_thread], &mut scratch[..per_thread]);
191 + st.ops.mul += m;
192 + st.ops.inv += inv;
193 + for i in 0..per_thread {
194 + let j = js[i] as usize;
195 + let jp = jumps[j];
196 + let jd = (1u64 << j) % n;
197 + let r = &mut roos[i];
198 + if special[i] {
199 + // P = ±2^j G
200 + if !r.tame {
201 + // Q + o·G = ±2^j·G ⇒ d = ±2^j − o
202 + for d in [inst.n.sub(jd, r.dist), inst.n.sub(inst.n.neg(jd), r.dist)] {
203 + if inst.verify(d, &mut st.ops) {
204 + solved = Some(d);
205 + break 'main;
206 + }
207 + }
208 + }
209 + let np = curve.add(&r.p, &jp, &mut st.ops);
210 + st.iterations += 1;
211 + if np.inf {
212 + *r = fresh(&mut st, &mut rng, r.tame);
213 + } else {
214 + r.p = np;
215 + r.dist = inst.n.add(r.dist, jd);
216 + r.travel += 1u128 << j;
217 + }
218 + } else {
219 + let lam = f.mul(f.sub(jp.y, r.p.y), dens[i]);
220 + r.p = curve.finish_add(&r.p, jp.x, lam);
221 + r.dist = inst.n.add(r.dist, jd);
222 + r.travel += 1u128 << j;
223 + st.ops.add += 1;
224 + st.ops.mul += 3;
225 + st.iterations += 1;
226 + }
227 + if r.travel > max_travel {
228 + st.abandoned_walks += 1;
229 + *r = fresh(&mut st, &mut rng, r.tame);
230 + continue;
231 + }
232 + if is_distinguished(r.p.x, dp_mask) {
233 + st.dp_found += 1;
234 + dps.push((r.p.x, DpEntry { y: r.p.y, dist: r.dist, tame: r.tame }));
235 + }
236 + }
237 + if !dps.is_empty() {
238 + let mut tab = table.lock().unwrap();
239 + for (x, e) in dps.drain(..) {
240 + match tab.get(&x) {
241 + None => {
242 + tab.insert(x, e);
243 + }
244 + Some(old) => {
245 + let old = *old;
246 + if old.tame == e.tame {
247 + if old.dist != e.dist {
248 + // merged trails inside one herd: reposition the newcomer
249 + st.useless_collisions += 1;
250 + if let Some(r) = roos.iter_mut().find(|r| r.p.x == x && r.tame == e.tame) {
251 + *r = fresh(&mut st, &mut rng, r.tame);
252 + }
253 + }
254 + continue;
255 + }
256 + let (tame_d, wild_d) = if old.tame { (old.dist, e.dist) } else { (e.dist, old.dist) };
257 + let cand = if old.y == e.y { inst.n.sub(tame_d, wild_d) } else { inst.n.sub(inst.n.neg(tame_d), wild_d) };
258 + if inst.verify(cand, &mut st.ops) {
259 + solved = Some(cand);
260 + break;
261 + }
262 + st.useless_collisions += 1;
263 + }
264 + }
265 + }
266 + }
267 + }
268 + if let Some(d) = solved {
269 + let mut fd = found.lock().unwrap();
270 + if fd.is_none() {
271 + *fd = Some(d);
272 + }
273 + stop.store(true, Ordering::Relaxed);
274 + }
275 + st.cpu_s = crate::stats::thread_cpu_s() - cpu0;
276 + stats.lock().unwrap().push(st);
277 + });
278 + }
279 + });
280 + for st in stats.lock().unwrap().iter() {
281 + total.merge(st);
282 + }
283 + let tab = table.lock().unwrap();
284 + let entries = tab.len() as u64;
285 + let bytes = tab.capacity() as u64 * (std::mem::size_of::<(u64, DpEntry)>() as u64 + 1);
286 + let d = *found.lock().unwrap();
287 + KangarooResult { d, stats: total, dp_count: entries, dp_bytes: bytes, table_entries: entries, threads, kangaroos_per_thread: per_thread, dp_bits, jump_count: k, mean_jump, interval_width: w, wall_s: t0.elapsed().as_secs_f64() }
288 +}
added experiments/synthetic_ecdlp/rho_engine/src/lib.rs +254 −0
@@ -0,0 +1,254 @@
1 +//! rho_engine — native ECDLP experimentation engine for WP34-UQO (synthetic instances, p < 2^62).
2 +//!
3 +//! Library entry point: [`run`] takes a [`SolveSpec`] (the CLI flags or one element of a batch file)
4 +//! and returns the JSON result object documented in README.md.
5 +
6 +pub mod bsgs;
7 +pub mod curve;
8 +pub mod field;
9 +pub mod instance;
10 +pub mod kangaroo;
11 +pub mod rho;
12 +pub mod stats;
13 +
14 +use serde::{Deserialize, Serialize};
15 +use serde_json::{json, Value};
16 +use std::time::Instant;
17 +
18 +/// One ECDLP job. Big integers are decimal strings so that the JSON stays exact.
19 +#[derive(Clone, Debug, Serialize, Deserialize)]
20 +pub struct SolveSpec {
21 + pub p: String,
22 + pub a: String,
23 + pub b: String,
24 + pub n: String,
25 + pub gx: String,
26 + pub gy: String,
27 + pub qx: String,
28 + pub qy: String,
29 + pub alg: String,
30 + #[serde(default = "default_seed")]
31 + pub seed: u64,
32 + #[serde(default)]
33 + pub label: Option<String>,
34 + #[serde(default)]
35 + pub expected_d: Option<String>,
36 + // rho
37 + #[serde(default = "default_r")]
38 + pub r: usize,
39 + #[serde(default)]
40 + pub cycle: Option<String>,
41 + #[serde(default)]
42 + pub dp: Option<u32>,
43 + #[serde(default = "default_one")]
44 + pub threads: usize,
45 + #[serde(default)]
46 + pub walks: Option<usize>,
47 + #[serde(default)]
48 + pub mixed: usize,
49 + #[serde(default)]
50 + pub neg: bool,
51 + #[serde(default)]
52 + pub aut3: bool,
53 + #[serde(default)]
54 + pub beta: Option<String>,
55 + #[serde(default)]
56 + pub lambda: Option<String>,
57 + #[serde(default = "default_cycle_check")]
58 + pub cycle_check: u64,
59 + #[serde(default)]
60 + pub max_walk_len: Option<u64>,
61 + // kangaroo
62 + #[serde(default)]
63 + pub lo: Option<String>,
64 + #[serde(default)]
65 + pub hi: Option<String>,
66 + // bsgs
67 + #[serde(default)]
68 + pub memory: Option<String>,
69 + #[serde(default = "default_chains")]
70 + pub chains: usize,
71 +}
72 +
73 +fn default_seed() -> u64 {
74 + 1
75 +}
76 +fn default_r() -> usize {
77 + 20
78 +}
79 +fn default_one() -> usize {
80 + 1
81 +}
82 +fn default_cycle_check() -> u64 {
83 + 32
84 +}
85 +fn default_chains() -> usize {
86 + 64
87 +}
88 +
89 +fn parse_u64(name: &str, s: &str) -> Result<u64, String> {
90 + s.trim().parse::<u64>().map_err(|e| format!("{name}: cannot parse '{s}' as u64 ({e})"))
91 +}
92 +
93 +/// Run one job and return the result JSON object (never panics on a bad spec: returns {"error": …}).
94 +pub fn run(spec: &SolveSpec) -> Value {
95 + match run_inner(spec) {
96 + Ok(v) => v,
97 + Err(e) => json!({ "error": e, "ok": false, "label": spec.label, "params": serde_json::to_value(spec).unwrap_or(Value::Null) }),
98 + }
99 +}
100 +
101 +fn run_inner(spec: &SolveSpec) -> Result<Value, String> {
102 + let t0 = Instant::now();
103 + let cpu0 = stats::process_cpu_s();
104 + let p = parse_u64("p", &spec.p)?;
105 + let a = parse_u64("a", &spec.a)?;
106 + let b = parse_u64("b", &spec.b)?;
107 + let n = parse_u64("n", &spec.n)?;
108 + let gx = parse_u64("gx", &spec.gx)?;
109 + let gy = parse_u64("gy", &spec.gy)?;
110 + let qx = parse_u64("qx", &spec.qx)?;
111 + let qy = parse_u64("qy", &spec.qy)?;
112 + if p >= (1u64 << 62) {
113 + return Err("p must be < 2^62".into());
114 + }
115 + if n >= (1u64 << 62) {
116 + return Err("n must be < 2^62".into());
117 + }
118 + let mut inst = instance::Instance::new(p, a, b, n, gx, gy, qx, qy)?;
119 + if spec.aut3 {
120 + let beta = parse_u64("beta", spec.beta.as_deref().ok_or("--aut3 requires --beta")?)?;
121 + let lambda = parse_u64("lambda", spec.lambda.as_deref().ok_or("--aut3 requires --lambda")?)?;
122 + inst = inst.with_aut3(beta, lambda)?;
123 + }
124 + let bits = inst.bits();
125 + let expected_d = spec.expected_d.as_deref().map(|s| parse_u64("expected_d", s)).transpose()?;
126 +
127 + let (d, ok, mut out) = match spec.alg.as_str() {
128 + "bsgs" => {
129 + let memory = spec.memory.as_deref().map(|s| parse_u64("memory", s)).transpose()?;
130 + let cfg = bsgs::BsgsConfig { memory, threads: spec.threads.max(1), chains: spec.chains.max(1) };
131 + let res = bsgs::solve_bsgs(&inst, &cfg);
132 + let ok = res.d.map(|d| inst.verify(d, &mut curve::Counters::default())).unwrap_or(false);
133 + let out = json!({
134 + "ops": res.ops,
135 + "iterations": res.iterations,
136 + "dp_count": 0,
137 + "dp_bytes": 0,
138 + "fruitless_cycles": 0,
139 + "threads": res.threads,
140 + "cpu_s": res.cpu_s,
141 + "table_entries": res.table_entries,
142 + "table_bytes": res.table_bytes,
143 + "baby_steps": res.baby_steps.to_string(),
144 + "giant_steps_planned": res.giant_steps_planned.to_string(),
145 + "build_s": res.build_s,
146 + "params": { "alg": "bsgs", "memory": res.baby_steps.to_string(), "threads": res.threads, "chains": cfg.chains, "seed": spec.seed },
147 + });
148 + (res.d, ok, out)
149 + }
150 + "rho" => {
151 + let mode = match spec.cycle.as_deref().unwrap_or("dp") {
152 + "floyd" => rho::RhoMode::Floyd,
153 + "brent" => rho::RhoMode::Brent,
154 + "dp" => rho::RhoMode::Dp,
155 + other => return Err(format!("unknown --cycle '{other}' (floyd|brent|dp)")),
156 + };
157 + let dp_bits = spec.dp.unwrap_or_else(|| rho::RhoConfig::default_dp_bits(bits));
158 + if spec.r < 2 {
159 + return Err("--r must be ≥ 2".into());
160 + }
161 + if spec.mixed == 1 {
162 + return Err("--mixed k must be ≥ 2 (every k-th partition doubles)".into());
163 + }
164 + let cfg = rho::RhoConfig {
165 + r: spec.r,
166 + mixed: spec.mixed,
167 + neg: spec.neg,
168 + aut3: spec.aut3,
169 + cycle_check: spec.cycle_check.max(2),
170 + dp_bits,
171 + walks: spec.walks.unwrap_or(128).max(1),
172 + threads: spec.threads.max(1),
173 + mode,
174 + seed: spec.seed,
175 + max_walk_len: spec.max_walk_len.unwrap_or((64u64 << dp_bits.min(40)).max(1 << 16)),
176 + };
177 + let res = rho::solve_rho(&inst, &cfg);
178 + let ok = res.d.map(|d| inst.verify(d, &mut curve::Counters::default())).unwrap_or(false);
179 + let class = (if cfg.neg { 2 } else { 1 }) * (if cfg.aut3 { 3 } else { 1 });
180 + let out = json!({
181 + "ops": res.stats.ops,
182 + "iterations": res.stats.iterations,
183 + "dp_count": res.dp_count,
184 + "dp_bytes": res.dp_bytes,
185 + "fruitless_cycles": res.stats.fruitless_cycles,
186 + "fruitless_by_length": res.stats.fruitless_by_length.iter().enumerate().filter(|(_, c)| **c > 0).map(|(l, c)| json!({"length": l, "count": c})).collect::<Vec<_>>(),
187 + "lookahead_rejections": res.stats.lookahead_rejections,
188 + "doubling_fallbacks": res.stats.doubling_fallbacks,
189 + "abandoned_walks": res.stats.abandoned_walks,
190 + "restarts": res.stats.restarts,
191 + "useless_collisions": res.stats.useless_collisions,
192 + "threads": res.threads,
193 + "cpu_s": res.stats.cpu_s,
194 + "table_entries": res.table_entries,
195 + "expected_iterations": rho::expected_iterations(n, class),
196 + "class_size": class,
197 + "params": { "alg": "rho", "mode": cfg.mode, "r": cfg.r, "mixed": cfg.mixed, "neg": cfg.neg, "aut3": cfg.aut3, "dp": cfg.dp_bits, "walks": cfg.walks, "threads": cfg.threads, "cycle_check": cfg.cycle_check, "max_walk_len": cfg.max_walk_len, "seed": cfg.seed, "beta": spec.beta, "lambda": spec.lambda },
198 + });
199 + (res.d, ok, out)
200 + }
201 + "kangaroo" => {
202 + let lo = spec.lo.as_deref().map(|s| parse_u64("lo", s)).transpose()?.unwrap_or(0);
203 + let hi = spec.hi.as_deref().map(|s| parse_u64("hi", s)).transpose()?.unwrap_or(n - 1);
204 + if hi >= n || lo > hi {
205 + return Err("need 0 ≤ lo ≤ hi < n".into());
206 + }
207 + let cfg = kangaroo::KangarooConfig { lo, hi, threads: spec.threads.max(1), walks: spec.walks.unwrap_or(0), dp_bits: spec.dp.unwrap_or(0), seed: spec.seed };
208 + let res = kangaroo::solve_kangaroo(&inst, &cfg);
209 + let ok = res.d.map(|d| inst.verify(d, &mut curve::Counters::default())).unwrap_or(false);
210 + let out = json!({
211 + "ops": res.stats.ops,
212 + "iterations": res.stats.iterations,
213 + "dp_count": res.dp_count,
214 + "dp_bytes": res.dp_bytes,
215 + "fruitless_cycles": 0,
216 + "herd_collisions": res.stats.useless_collisions,
217 + "abandoned_walks": res.stats.abandoned_walks,
218 + "restarts": res.stats.restarts,
219 + "threads": res.threads,
220 + "cpu_s": res.stats.cpu_s,
221 + "table_entries": res.table_entries,
222 + "expected_iterations": 2.0 * (res.interval_width as f64).sqrt(),
223 + "params": { "alg": "kangaroo", "lo": lo.to_string(), "hi": hi.to_string(), "interval_width": res.interval_width.to_string(), "threads": res.threads, "kangaroos_per_thread": res.kangaroos_per_thread, "dp": res.dp_bits, "jump_count": res.jump_count, "mean_jump": res.mean_jump.to_string(), "seed": spec.seed },
224 + });
225 + (res.d, ok, out)
226 + }
227 + other => return Err(format!("unknown algorithm '{other}' (bsgs|rho|kangaroo)")),
228 + };
229 +
230 + let obj = out.as_object_mut().unwrap();
231 + let wall = t0.elapsed().as_secs_f64();
232 + obj.insert("d".into(), d.map(|v| Value::String(v.to_string())).unwrap_or(Value::Null));
233 + obj.insert("ok".into(), Value::Bool(ok));
234 + obj.insert("wall_s".into(), json!(wall));
235 + obj.insert("process_cpu_s".into(), json!(stats::process_cpu_s() - cpu0));
236 + obj.insert("peak_rss_bytes".into(), json!(stats::peak_rss_bytes()));
237 + obj.insert("bits".into(), json!(bits));
238 + obj.insert("seed".into(), json!(spec.seed));
239 + obj.insert("label".into(), spec.label.clone().map(Value::String).unwrap_or(Value::Null));
240 + obj.insert("engine".into(), json!(format!("rho_engine {}", env!("CARGO_PKG_VERSION"))));
241 + let ops = obj.get("ops").cloned().unwrap_or(Value::Null);
242 + let group_ops = ops.get("add").and_then(|v| v.as_u64()).unwrap_or(0) + ops.get("dbl").and_then(|v| v.as_u64()).unwrap_or(0);
243 + let cpu = obj.get("cpu_s").and_then(|v| v.as_f64()).unwrap_or(0.0);
244 + obj.insert("group_ops".into(), json!(group_ops));
245 + obj.insert("group_ops_per_cpu_s".into(), json!(if cpu > 0.0 { group_ops as f64 / cpu } else { 0.0 }));
246 + obj.insert("group_ops_per_wall_s".into(), json!(if wall > 0.0 { group_ops as f64 / wall } else { 0.0 }));
247 + if let Some(e) = expected_d {
248 + obj.insert("expected_d".into(), Value::String(e.to_string()));
249 + obj.insert("match".into(), Value::Bool(d == Some(e % n)));
250 + }
251 + let instance = json!({ "p": spec.p, "a": spec.a, "b": spec.b, "n": spec.n, "gx": spec.gx, "gy": spec.gy, "qx": spec.qx, "qy": spec.qy });
252 + obj.get_mut("params").and_then(|v| v.as_object_mut()).map(|m| m.insert("instance".into(), instance));
253 + Ok(out)
254 +}
added experiments/synthetic_ecdlp/rho_engine/src/main.rs +206 −0
@@ -0,0 +1,206 @@
1 +//! CLI: `rho_engine solve …` (one JSON object on stdout) and `rho_engine batch …` (JSON array).
2 +
3 +use clap::{Args, Parser, Subcommand};
4 +use rho_engine::SolveSpec;
5 +use std::io::Write;
6 +
7 +#[derive(Parser)]
8 +#[command(name = "rho_engine", version, about = "Native ECDLP engine (BSGS / Pollard rho / kangaroo) for synthetic prime-order curves, p < 2^62")]
9 +struct Cli {
10 + #[command(subcommand)]
11 + cmd: Cmd,
12 +}
13 +
14 +#[derive(Subcommand)]
15 +enum Cmd {
16 + /// Solve one instance Q = d·G and print a JSON result object
17 + Solve(SolveArgs),
18 + /// Run every job of a JSON array file sequentially and write a JSON array of results
19 + Batch {
20 + /// input file: JSON array of job objects (same keys as the `solve` flags, decimal strings)
21 + #[arg(long)]
22 + file: String,
23 + /// output file (JSON array); "-" for stdout
24 + #[arg(long, default_value = "-")]
25 + out: String,
26 + /// print progress lines on stderr
27 + #[arg(long)]
28 + verbose: bool,
29 + },
30 +}
31 +
32 +#[derive(Args)]
33 +struct SolveArgs {
34 + #[arg(long)]
35 + p: String,
36 + #[arg(long)]
37 + a: String,
38 + #[arg(long)]
39 + b: String,
40 + /// prime order of G
41 + #[arg(long)]
42 + n: String,
43 + #[arg(long)]
44 + gx: String,
45 + #[arg(long)]
46 + gy: String,
47 + #[arg(long)]
48 + qx: String,
49 + #[arg(long)]
50 + qy: String,
51 + /// bsgs | rho | kangaroo
52 + #[arg(long)]
53 + alg: String,
54 + #[arg(long, default_value_t = 1)]
55 + seed: u64,
56 + /// print JSON (always on; kept for compatibility)
57 + #[arg(long)]
58 + json: bool,
59 + /// optional label copied to the output
60 + #[arg(long)]
61 + label: Option<String>,
62 + /// optional known answer (adds "match")
63 + #[arg(long)]
64 + expected_d: Option<String>,
65 + /// rho: number of partitions of the r-adding walk
66 + #[arg(long, default_value_t = 20)]
67 + r: usize,
68 + /// rho: floyd | brent (single walk) | dp (van Oorschot–Wiener, default)
69 + #[arg(long)]
70 + cycle: Option<String>,
71 + /// rho/kangaroo: distinguished-point bits (default ⌊bits/4⌋ − 2, ≥ 1)
72 + #[arg(long)]
73 + dp: Option<u32>,
74 + #[arg(long, default_value_t = 1)]
75 + threads: usize,
76 + /// rho: walks per thread (default 128); kangaroo: kangaroos per thread (default automatic)
77 + #[arg(long)]
78 + walks: Option<usize>,
79 + /// rho: Teske mixed walk — every k-th partition is a doubling partition (0 = pure adding walk)
80 + #[arg(long, default_value_t = 0)]
81 + mixed: usize,
82 + /// rho: negation map (classes {P, −P})
83 + #[arg(long)]
84 + neg: bool,
85 + /// rho: order-3 automorphism classes (a = 0 curves; requires --beta and --lambda)
86 + #[arg(long)]
87 + aut3: bool,
88 + #[arg(long)]
89 + beta: Option<String>,
90 + #[arg(long)]
91 + lambda: Option<String>,
92 + /// rho: remember one point every K steps for fruitless-cycle detection
93 + #[arg(long, default_value_t = 32)]
94 + cycle_check: u64,
95 + /// rho (dp): abandon a walk after this many steps without a distinguished point
96 + #[arg(long)]
97 + max_walk_len: Option<u64>,
98 + /// kangaroo: interval lower bound (default 0)
99 + #[arg(long)]
100 + lo: Option<String>,
101 + /// kangaroo: interval upper bound (default n − 1)
102 + #[arg(long)]
103 + hi: Option<String>,
104 + /// bsgs: number of baby steps m (default ⌈√n⌉)
105 + #[arg(long)]
106 + memory: Option<String>,
107 + /// bsgs: chains per thread for batch inversion
108 + #[arg(long, default_value_t = 64)]
109 + chains: usize,
110 +}
111 +
112 +impl From<SolveArgs> for SolveSpec {
113 + fn from(a: SolveArgs) -> SolveSpec {
114 + SolveSpec {
115 + p: a.p,
116 + a: a.a,
117 + b: a.b,
118 + n: a.n,
119 + gx: a.gx,
120 + gy: a.gy,
121 + qx: a.qx,
122 + qy: a.qy,
123 + alg: a.alg,
124 + seed: a.seed,
125 + label: a.label,
126 + expected_d: a.expected_d,
127 + r: a.r,
128 + cycle: a.cycle,
129 + dp: a.dp,
130 + threads: a.threads,
131 + walks: a.walks,
132 + mixed: a.mixed,
133 + neg: a.neg,
134 + aut3: a.aut3,
135 + beta: a.beta,
136 + lambda: a.lambda,
137 + cycle_check: a.cycle_check,
138 + max_walk_len: a.max_walk_len,
139 + lo: a.lo,
140 + hi: a.hi,
141 + memory: a.memory,
142 + chains: a.chains,
143 + }
144 + }
145 +}
146 +
147 +fn main() {
148 + let cli = Cli::parse();
149 + match cli.cmd {
150 + Cmd::Solve(args) => {
151 + let spec: SolveSpec = args.into();
152 + let out = rho_engine::run(&spec);
153 + println!("{}", serde_json::to_string(&out).unwrap());
154 + if out.get("ok").and_then(|v| v.as_bool()) != Some(true) {
155 + std::process::exit(2);
156 + }
157 + }
158 + Cmd::Batch { file, out, verbose } => {
159 + let text = std::fs::read_to_string(&file).unwrap_or_else(|e| {
160 + eprintln!("cannot read {file}: {e}");
161 + std::process::exit(1)
162 + });
163 + let specs: Vec<SolveSpec> = serde_json::from_str(&text).unwrap_or_else(|e| {
164 + eprintln!("cannot parse {file}: {e}");
165 + std::process::exit(1)
166 + });
167 + let mut results = Vec::with_capacity(specs.len());
168 + let mut failures = 0usize;
169 + for (i, spec) in specs.iter().enumerate() {
170 + let res = rho_engine::run(spec);
171 + let ok = res.get("ok").and_then(|v| v.as_bool()) == Some(true);
172 + if !ok {
173 + failures += 1;
174 + }
175 + if verbose {
176 + eprintln!(
177 + "[{}/{}] {} alg={} ok={} wall={:.3}s group_ops={}",
178 + i + 1,
179 + specs.len(),
180 + spec.label.as_deref().unwrap_or("-"),
181 + spec.alg,
182 + ok,
183 + res.get("wall_s").and_then(|v| v.as_f64()).unwrap_or(0.0),
184 + res.get("group_ops").and_then(|v| v.as_u64()).unwrap_or(0)
185 + );
186 + }
187 + results.push(res);
188 + }
189 + let text = serde_json::to_string_pretty(&results).unwrap();
190 + if out == "-" {
191 + println!("{text}");
192 + } else {
193 + let mut f = std::fs::File::create(&out).unwrap_or_else(|e| {
194 + eprintln!("cannot write {out}: {e}");
195 + std::process::exit(1)
196 + });
197 + f.write_all(text.as_bytes()).unwrap();
198 + f.write_all(b"\n").unwrap();
199 + }
200 + if failures > 0 {
201 + eprintln!("{failures} job(s) failed");
202 + std::process::exit(2);
203 + }
204 + }
205 + }
206 +}
added experiments/synthetic_ecdlp/rho_engine/src/rho.rs +792 −0
@@ -0,0 +1,792 @@
1 +//! Pollard rho: Teske r-adding / mixed walks, Floyd / Brent single-walk cycle detection,
2 +//! van Oorschot–Wiener distinguished-point parallel collision search, negation map and
3 +//! order-3 automorphism classes with fruitless-cycle handling.
4 +//!
5 +//! Iteration function (see README.md for the full specification and references):
6 +//! j = h(x(P)) ∈ [0, r), f(P) = ‖P + M_j‖ (adding partition) or ‖2P‖ (doubling partition, --mixed),
7 +//! where M_j = u_j G + v_j Q and ‖·‖ is the canonical representative of the equivalence class
8 +//! (identity, {P, −P} with --neg, {P, ζP, ζ²P} with --aut3, the order-6 orbit with both).
9 +//! With --neg the look-ahead rule of Bos–Kleinjung–Lenstra avoids fruitless 2-cycles, and every
10 +//! walk remembers one point every `cycle_check` steps to detect longer fruitless cycles, escaped by
11 +//! doubling the minimum point of the cycle (Bernstein–Lange–Schwabe 2011).
12 +
13 +use crate::curve::{CombTable, Counters, Pt};
14 +use crate::instance::{is_distinguished, partition, Instance};
15 +use rand::{Rng, SeedableRng};
16 +use rand_chacha::ChaCha20Rng;
17 +use serde::{Deserialize, Serialize};
18 +use std::sync::atomic::{AtomicBool, Ordering};
19 +use std::sync::Mutex;
20 +use std::time::Instant;
21 +
22 +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
23 +#[serde(rename_all = "lowercase")]
24 +pub enum RhoMode {
25 + Floyd,
26 + Brent,
27 + Dp,
28 +}
29 +
30 +#[derive(Clone, Debug, Serialize, Deserialize)]
31 +pub struct RhoConfig {
32 + pub r: usize,
33 + /// 0 = pure r-adding walk; k > 0 = partitions j with j % k == k−1 are doubling partitions
34 + pub mixed: usize,
35 + pub neg: bool,
36 + pub aut3: bool,
37 + /// remember one point every `cycle_check` steps (detects fruitless cycles of length ≤ cycle_check)
38 + pub cycle_check: u64,
39 + pub dp_bits: u32,
40 + /// walks per thread (batch size of the simultaneous inversion)
41 + pub walks: usize,
42 + pub threads: usize,
43 + pub mode: RhoMode,
44 + pub seed: u64,
45 + /// walks longer than this (steps since last distinguished point) are abandoned and restarted
46 + pub max_walk_len: u64,
47 +}
48 +
49 +impl RhoConfig {
50 + pub fn default_dp_bits(bits: u32) -> u32 {
51 + (bits / 4).saturating_sub(2).max(1)
52 + }
53 +}
54 +
55 +#[derive(Clone, Copy, Debug)]
56 +pub struct Mult {
57 + pub pt: Pt,
58 + pub u: u64,
59 + pub v: u64,
60 +}
61 +
62 +#[derive(Clone, Debug, Default, Serialize, Deserialize)]
63 +pub struct WalkStats {
64 + pub ops: Counters,
65 + /// steps of the iteration function actually taken (advances), all walks
66 + pub iterations: u64,
67 + pub fruitless_cycles: u64,
68 + /// fruitless cycles by length (index = length, up to 64)
69 + pub fruitless_by_length: Vec<u64>,
70 + /// look-ahead rejections (2-cycle avoidance), each costs one wasted addition
71 + pub lookahead_rejections: u64,
72 + pub doubling_fallbacks: u64,
73 + pub abandoned_walks: u64,
74 + pub restarts: u64,
75 + pub dp_found: u64,
76 + pub useless_collisions: u64,
77 + pub cpu_s: f64,
78 +}
79 +
80 +impl WalkStats {
81 + pub fn merge(&mut self, o: &WalkStats) {
82 + self.ops.merge(&o.ops);
83 + self.iterations += o.iterations;
84 + self.fruitless_cycles += o.fruitless_cycles;
85 + if self.fruitless_by_length.len() < o.fruitless_by_length.len() {
86 + self.fruitless_by_length.resize(o.fruitless_by_length.len(), 0);
87 + }
88 + for (i, v) in o.fruitless_by_length.iter().enumerate() {
89 + self.fruitless_by_length[i] += v;
90 + }
91 + self.lookahead_rejections += o.lookahead_rejections;
92 + self.doubling_fallbacks += o.doubling_fallbacks;
93 + self.abandoned_walks += o.abandoned_walks;
94 + self.restarts += o.restarts;
95 + self.dp_found += o.dp_found;
96 + self.useless_collisions += o.useless_collisions;
97 + self.cpu_s += o.cpu_s;
98 + }
99 +}
100 +
101 +#[derive(Clone, Copy, Debug)]
102 +pub struct Walk {
103 + pub p: Pt,
104 + pub u: u64,
105 + pub v: u64,
106 + /// partition override for the look-ahead retry (u32::MAX = none, FORCE_DBL = doubling fallback)
107 + ovr: u32,
108 + tries: u32,
109 + /// steps since the last (re)start
110 + pub steps: u64,
111 + saved: Pt,
112 + saved_step: u64,
113 + last_escape_x: u64,
114 +}
115 +
116 +const NO_OVR: u32 = u32::MAX;
117 +const FORCE_DBL: u32 = u32::MAX - 1;
118 +
119 +/// Data shared by all threads of one rho run.
120 +pub struct Shared<'a> {
121 + pub inst: &'a Instance,
122 + pub cfg: &'a RhoConfig,
123 + pub mults: Vec<Mult>,
124 + pub comb_g: CombTable,
125 + pub comb_q: CombTable,
126 + pub beta2: u64,
127 + pub lam2: u64,
128 + pub dp_mask: u64,
129 + pub setup_ops: Counters,
130 +}
131 +
132 +impl<'a> Shared<'a> {
133 + pub fn new(inst: &'a Instance, cfg: &'a RhoConfig) -> Shared<'a> {
134 + let mut c = Counters::default();
135 + let mut rng = ChaCha20Rng::seed_from_u64(cfg.seed ^ 0x5EED_0000_0000_0001);
136 + let comb_g = CombTable::new(&inst.curve, &inst.g, &mut c);
137 + let comb_q = CombTable::new(&inst.curve, &inst.q, &mut c);
138 + let n = inst.n.n;
139 + let mut mults = Vec::with_capacity(cfg.r);
140 + while mults.len() < cfg.r {
141 + let u = rng.random_range(1..n);
142 + let v = rng.random_range(1..n);
143 + let pt = inst.curve.add(&comb_g.mul(&inst.curve, u, &mut c), &comb_q.mul(&inst.curve, v, &mut c), &mut c);
144 + if pt.inf {
145 + continue; // u + v·d ≡ 0 — astronomically unlikely; resample
146 + }
147 + mults.push(Mult { pt, u, v });
148 + }
149 + let (beta2, lam2) = match (inst.beta, inst.lambda) {
150 + (Some(b), Some(l)) => (inst.curve.f.sqr(b), inst.n.mul(l, l)),
151 + _ => (0, 0),
152 + };
153 + let dp_mask = if cfg.dp_bits >= 64 { u64::MAX } else { (1u64 << cfg.dp_bits) - 1 };
154 + Shared { inst, cfg, mults, comb_g, comb_q, beta2, lam2, dp_mask, setup_ops: c }
155 + }
156 +}
157 +
158 +pub enum Outcome {
159 + Advanced,
160 + Rejected,
161 + Solved(u64),
162 +}
163 +
164 +/// Per-thread walk context.
165 +pub struct Ctx<'a> {
166 + pub sh: &'a Shared<'a>,
167 + pub rng: ChaCha20Rng,
168 + pub st: WalkStats,
169 + dens: Vec<u64>,
170 + scratch: Vec<u64>,
171 + js: Vec<u32>,
172 + dbls: Vec<bool>,
173 + special: Vec<bool>,
174 +}
175 +
176 +#[derive(Clone, Copy, Debug)]
177 +pub struct DpRecord {
178 + pub x: u64,
179 + pub y: u64,
180 + pub u: u64,
181 + pub v: u64,
182 +}
183 +
184 +impl<'a> Ctx<'a> {
185 + pub fn new(sh: &'a Shared<'a>, thread_id: u64) -> Ctx<'a> {
186 + let mut seed = [0u8; 32];
187 + seed[..8].copy_from_slice(&sh.cfg.seed.to_le_bytes());
188 + seed[8..16].copy_from_slice(&thread_id.to_le_bytes());
189 + seed[16..24].copy_from_slice(b"rho_walk");
190 + let w = sh.cfg.walks.max(1);
191 + Ctx {
192 + sh,
193 + rng: ChaCha20Rng::from_seed(seed),
194 + st: WalkStats { fruitless_by_length: vec![0; 65], ..Default::default() },
195 + dens: vec![0; w],
196 + scratch: vec![0; w],
197 + js: vec![0; w],
198 + dbls: vec![false; w],
199 + special: vec![false; w],
200 + }
201 + }
202 +
203 + #[inline(always)]
204 + fn partition_of(&self, x: u64) -> u32 {
205 + partition(x, self.sh.cfg.r as u64)
206 + }
207 +
208 + #[inline(always)]
209 + fn is_dbl_partition(&self, j: u32) -> bool {
210 + let k = self.sh.cfg.mixed;
211 + k > 0 && (j as usize + 1) % k == 0
212 + }
213 +
214 + /// Canonical representative of the class of (P; u, v) and the adjusted scalars.
215 + #[inline(always)]
216 + pub fn canon(&mut self, mut p: Pt, mut u: u64, mut v: u64) -> (Pt, u64, u64) {
217 + let inst = self.sh.inst;
218 + let f = &inst.curve.f;
219 + let cfg = self.sh.cfg;
220 + if cfg.aut3 {
221 + // candidates x, βx, β²x ↔ P, λP, λ²P ; pick the minimum standard-form x
222 + let beta = inst.beta.unwrap();
223 + let lam = inst.lambda.unwrap();
224 + let x1 = f.mul(beta, p.x);
225 + let x2 = f.mul(self.sh.beta2, p.x);
226 + let s0 = f.from_mont(p.x);
227 + let s1 = f.from_mont(x1);
228 + let s2 = f.from_mont(x2);
229 + self.st.ops.mul += 5;
230 + if s1 < s0 && s1 <= s2 {
231 + p.x = x1;
232 + u = inst.n.mul(u, lam);
233 + v = inst.n.mul(v, lam);
234 + } else if s2 < s0 && s2 < s1 {
235 + p.x = x2;
236 + u = inst.n.mul(u, self.sh.lam2);
237 + v = inst.n.mul(v, self.sh.lam2);
238 + }
239 + }
240 + if cfg.neg {
241 + // keep the representative with y_std < p − y_std, i.e. 2·y_std < p
242 + let ys = f.from_mont(p.y);
243 + self.st.ops.mul += 1;
244 + if 2 * ys > f.p {
245 + p.y = f.neg(p.y);
246 + u = inst.n.neg(u);
247 + v = inst.n.neg(v);
248 + }
249 + }
250 + (p, u, v)
251 + }
252 +
253 + /// d = num / den mod n, verified against Q. None if den ≡ 0 or verification fails.
254 + fn try_solve(&mut self, num: u64, den: u64) -> Option<u64> {
255 + let inst = self.sh.inst;
256 + let d = inst.n.solve(den, num)?;
257 + if inst.verify(d, &mut self.st.ops) {
258 + Some(d)
259 + } else {
260 + None
261 + }
262 + }
263 +
264 + /// Fresh random walk start ‖u G + v Q‖. Err(d) if the start happens to be O (then d = −u/v).
265 + pub fn random_walk(&mut self) -> Result<Walk, u64> {
266 + let inst = self.sh.inst;
267 + let n = inst.n.n;
268 + loop {
269 + let u = self.rng.random_range(0..n);
270 + let v = self.rng.random_range(1..n);
271 + let p = inst.curve.add(
272 + &self.sh.comb_g.mul(&inst.curve, u, &mut self.st.ops),
273 + &self.sh.comb_q.mul(&inst.curve, v, &mut self.st.ops),
274 + &mut self.st.ops,
275 + );
276 + self.st.restarts += 1;
277 + if p.inf {
278 + if let Some(d) = self.try_solve(inst.n.neg(u), v) {
279 + return Err(d);
280 + }
281 + continue;
282 + }
283 + let (p, u, v) = self.canon(p, u, v);
284 + return Ok(Walk {
285 + p,
286 + u,
287 + v,
288 + ovr: NO_OVR,
289 + tries: 0,
290 + steps: 0,
291 + saved: Pt::INF,
292 + saved_step: 0,
293 + last_escape_x: u64::MAX,
294 + });
295 + }
296 + }
297 +
298 + #[inline(always)]
299 + fn choose(&self, w: &Walk) -> (u32, bool) {
300 + if w.ovr == NO_OVR {
301 + let j = self.partition_of(w.p.x);
302 + (j, self.is_dbl_partition(j))
303 + } else if w.ovr == FORCE_DBL {
304 + (0, true)
305 + } else {
306 + (w.ovr, self.is_dbl_partition(w.ovr))
307 + }
308 + }
309 +
310 + /// Denominator of the slope for the chosen step; 0 marks a special case.
311 + #[inline(always)]
312 + fn denominator(&self, w: &Walk, j: u32, is_dbl: bool) -> u64 {
313 + let f = &self.sh.inst.curve.f;
314 + if is_dbl {
315 + f.dbl(w.p.y)
316 + } else {
317 + f.sub(self.sh.mults[j as usize].pt.x, w.p.x)
318 + }
319 + }
320 +
321 + /// Apply one step given the inverse of the denominator. Does not touch cycle bookkeeping.
322 + #[inline(always)]
323 + fn apply(&mut self, w: &mut Walk, j: u32, is_dbl: bool, inv: u64) -> Outcome {
324 + let inst = self.sh.inst;
325 + let f = &inst.curve.f;
326 + let (r, u, v) = if is_dbl {
327 + let x2 = f.sqr(w.p.x);
328 + let num = f.add(f.add(f.add(x2, x2), x2), inst.curve.a);
329 + let lam = f.mul(num, inv);
330 + let r = inst.curve.finish_dbl(&w.p, lam);
331 + self.st.ops.dbl += 1;
332 + self.st.ops.mul += 4;
333 + (r, inst.n.dbl(w.u), inst.n.dbl(w.v))
334 + } else {
335 + let m = &self.sh.mults[j as usize];
336 + let lam = f.mul(f.sub(m.pt.y, w.p.y), inv);
337 + let r = inst.curve.finish_add(&w.p, m.pt.x, lam);
338 + self.st.ops.add += 1;
339 + self.st.ops.mul += 3;
340 + (r, inst.n.add(w.u, m.u), inst.n.add(w.v, m.v))
341 + };
342 + let (r, u, v) = self.canon(r, u, v);
343 + if self.sh.cfg.neg && !is_dbl && self.partition_of(r.x) == j {
344 + // look-ahead: ‖R + M_j‖ could be ‖P‖ (fruitless 2-cycle) → use the next partition
345 + self.st.lookahead_rejections += 1;
346 + w.tries += 1;
347 + if w.tries as usize >= self.sh.cfg.r {
348 + w.ovr = FORCE_DBL;
349 + self.st.doubling_fallbacks += 1;
350 + } else {
351 + w.ovr = (j + 1) % self.sh.cfg.r as u32;
352 + }
353 + return Outcome::Rejected;
354 + }
355 + w.p = r;
356 + w.u = u;
357 + w.v = v;
358 + w.ovr = NO_OVR;
359 + w.tries = 0;
360 + w.steps += 1;
361 + self.st.iterations += 1;
362 + Outcome::Advanced
363 + }
364 +
365 + /// Slow path for a zero denominator (P = ±M_j, or y = 0 for a doubling).
366 + fn special(&mut self, w: &mut Walk, j: u32, is_dbl: bool) -> Option<u64> {
367 + let inst = self.sh.inst;
368 + if is_dbl {
369 + // y = 0 ⇒ 2P = O: impossible in a prime-order group of odd order; restart defensively.
370 + return self.restart(w);
371 + }
372 + let m = self.sh.mults[j as usize];
373 + if w.p.y == m.pt.y {
374 + // P = M_j : u + v d = u_j + v_j d
375 + if let Some(d) = self.try_solve(inst.n.sub(m.u, w.u), inst.n.sub(w.v, m.v)) {
376 + return Some(d);
377 + }
378 + // same scalars: genuine doubling
379 + let r = inst.curve.dbl(&w.p, &mut self.st.ops);
380 + if r.inf {
381 + return self.restart(w);
382 + }
383 + let (r, u, v) = self.canon(r, inst.n.dbl(w.u), inst.n.dbl(w.v));
384 + w.p = r;
385 + w.u = u;
386 + w.v = v;
387 + w.ovr = NO_OVR;
388 + w.tries = 0;
389 + w.steps += 1;
390 + self.st.iterations += 1;
391 + None
392 + } else {
393 + // P = −M_j : u + v d = −u_j − v_j d ⇒ (v + v_j) d = −(u + u_j)
394 + if let Some(d) = self.try_solve(inst.n.neg(inst.n.add(w.u, m.u)), inst.n.add(w.v, m.v)) {
395 + return Some(d);
396 + }
397 + self.st.useless_collisions += 1;
398 + self.restart(w)
399 + }
400 + }
401 +
402 + fn restart(&mut self, w: &mut Walk) -> Option<u64> {
403 + match self.random_walk() {
404 + Ok(nw) => {
405 + *w = nw;
406 + None
407 + }
408 + Err(d) => Some(d),
409 + }
410 + }
411 +
412 + /// One full step of the iteration function with a single (non-batched) inversion.
413 + pub fn step_single(&mut self, w: &mut Walk) -> Option<u64> {
414 + loop {
415 + let (j, is_dbl) = self.choose(w);
416 + let den = self.denominator(w, j, is_dbl);
417 + if den == 0 {
418 + return self.special(w, j, is_dbl);
419 + }
420 + let inv = self.sh.inst.curve.f.inv(den);
421 + self.st.ops.inv += 1;
422 + self.st.ops.mul += 1;
423 + match self.apply(w, j, is_dbl, inv) {
424 + Outcome::Advanced => return None,
425 + Outcome::Rejected => continue,
426 + Outcome::Solved(d) => return Some(d),
427 + }
428 + }
429 + }
430 +
431 + /// Cycle bookkeeping after an advance: detect a fruitless cycle (point equal to the remembered
432 + /// point) and escape it; otherwise remember the current point every `cycle_check` steps.
433 + #[inline(always)]
434 + fn after_advance(&mut self, w: &mut Walk) -> Option<u64> {
435 + if !w.saved.inf && w.p.x == w.saved.x && w.p.y == w.saved.y {
436 + let len = w.steps - w.saved_step;
437 + return self.escape_cycle(w, len);
438 + }
439 + if w.steps % self.sh.cfg.cycle_check == 0 {
440 + w.saved = w.p;
441 + w.saved_step = w.steps;
442 + }
443 + None
444 + }
445 +
446 + /// Escape a fruitless cycle of length `len` containing the current point: walk the cycle once
447 + /// to find its minimum point (smallest Montgomery x), then continue from ‖2·P_min‖.
448 + pub fn escape_cycle(&mut self, w: &mut Walk, len: u64) -> Option<u64> {
449 + let inst = self.sh.inst;
450 + self.st.fruitless_cycles += 1;
451 + let idx = (len as usize).min(64);
452 + self.st.fruitless_by_length[idx] += 1;
453 + let start = w.p;
454 + let mut best = *w;
455 + let mut cur = *w;
456 + for _ in 0..len.max(1) {
457 + if let Some(d) = self.step_single(&mut cur) {
458 + return Some(d);
459 + }
460 + if cur.p.x < best.p.x || (cur.p.x == best.p.x && cur.p.y < best.p.y) {
461 + best = cur;
462 + }
463 + if cur.p == start {
464 + break;
465 + }
466 + }
467 + if best.p.x == w.last_escape_x {
468 + // the escape led back into the same cycle: give up on this trajectory
469 + return self.restart(w);
470 + }
471 + let r = inst.curve.dbl(&best.p, &mut self.st.ops);
472 + if r.inf {
473 + return self.restart(w);
474 + }
475 + let (r, u, v) = self.canon(r, inst.n.dbl(best.u), inst.n.dbl(best.v));
476 + w.p = r;
477 + w.u = u;
478 + w.v = v;
479 + w.ovr = NO_OVR;
480 + w.tries = 0;
481 + w.steps += 1;
482 + w.saved = Pt::INF;
483 + w.last_escape_x = best.p.x;
484 + self.st.iterations += 1;
485 + None
486 + }
487 +
488 + /// Full single step including cycle bookkeeping (used by Floyd / Brent).
489 + pub fn step(&mut self, w: &mut Walk) -> Option<u64> {
490 + if let Some(d) = self.step_single(w) {
491 + return Some(d);
492 + }
493 + self.after_advance(w)
494 + }
495 +
496 + /// Batched step of all walks (one simultaneous inversion). Distinguished points are appended to
497 + /// `dps` and the corresponding walks restarted. Returns Some(d) if the DLP was solved on the way.
498 + pub fn batch_step(&mut self, walks: &mut [Walk], dps: &mut Vec<DpRecord>) -> Option<u64> {
499 + let k = walks.len();
500 + for i in 0..k {
501 + let (j, is_dbl) = self.choose(&walks[i]);
502 + let den = self.denominator(&walks[i], j, is_dbl);
503 + self.js[i] = j;
504 + self.dbls[i] = is_dbl;
505 + self.special[i] = den == 0;
506 + self.dens[i] = if den == 0 { self.sh.inst.curve.f.one } else { den };
507 + }
508 + let (m, inv) = self.sh.inst.curve.f.batch_inv(&mut self.dens[..k], &mut self.scratch[..k]);
509 + self.st.ops.mul += m;
510 + self.st.ops.inv += inv;
511 + for i in 0..k {
512 + let w = &mut walks[i];
513 + if self.special[i] {
514 + let (j, is_dbl) = (self.js[i], self.dbls[i]);
515 + if let Some(d) = self.special(w, j, is_dbl) {
516 + return Some(d);
517 + }
518 + continue;
519 + }
520 + let inv = self.dens[i];
521 + match self.apply(w, self.js[i], self.dbls[i], inv) {
522 + Outcome::Rejected => continue,
523 + Outcome::Solved(d) => return Some(d),
524 + Outcome::Advanced => {}
525 + }
526 + if let Some(d) = self.after_advance(w) {
527 + return Some(d);
528 + }
529 + if is_distinguished(w.p.x, self.sh.dp_mask) {
530 + dps.push(DpRecord { x: w.p.x, y: w.p.y, u: w.u, v: w.v });
531 + self.st.dp_found += 1;
532 + if let Some(d) = self.restart(w) {
533 + return Some(d);
534 + }
535 + } else if w.steps > self.sh.cfg.max_walk_len {
536 + self.st.abandoned_walks += 1;
537 + if let Some(d) = self.restart(w) {
538 + return Some(d);
539 + }
540 + }
541 + }
542 + None
543 + }
544 +
545 + /// Resolve a collision of two walks at the same point (or at P and −P).
546 + fn collide(&mut self, a: &Walk, b: &Walk) -> Option<u64> {
547 + let inst = self.sh.inst;
548 + if a.p.y == b.p.y {
549 + if a.u == b.u && a.v == b.v {
550 + return None;
551 + }
552 + self.try_solve(inst.n.sub(a.u, b.u), inst.n.sub(b.v, a.v))
553 + } else {
554 + self.try_solve(inst.n.neg(inst.n.add(a.u, b.u)), inst.n.add(a.v, b.v))
555 + }
556 + }
557 +
558 + /// Floyd's cycle finding on a single walk: x_i vs x_{2i}.
559 + pub fn floyd(&mut self) -> u64 {
560 + let mut t = match self.random_walk() {
561 + Ok(w) => w,
562 + Err(d) => return d,
563 + };
564 + let mut h = t;
565 + loop {
566 + if let Some(d) = self.step(&mut t) {
567 + return d;
568 + }
569 + if let Some(d) = self.step(&mut h) {
570 + return d;
571 + }
572 + if let Some(d) = self.step(&mut h) {
573 + return d;
574 + }
575 + if t.p.x == h.p.x {
576 + if let Some(d) = self.collide(&t, &h) {
577 + return d;
578 + }
579 + // same scalars (fruitless cycle) or degenerate: escape and continue
580 + let len = if t.p == h.p && t.u == h.u && t.v == h.v { self.cycle_length(&h) } else { None };
581 + if let Some(len) = len {
582 + if let Some(d) = self.escape_cycle(&mut h, len) {
583 + return d;
584 + }
585 + t = h;
586 + } else {
587 + self.st.useless_collisions += 1;
588 + match self.random_walk() {
589 + Ok(w) => {
590 + t = w;
591 + h = t;
592 + }
593 + Err(d) => return d,
594 + }
595 + }
596 + }
597 + }
598 + }
599 +
600 + /// Brent's cycle finding on a single walk (power-of-two teleporting tortoise).
601 + pub fn brent(&mut self) -> u64 {
602 + let mut t = match self.random_walk() {
603 + Ok(w) => w,
604 + Err(d) => return d,
605 + };
606 + let mut h = t;
607 + let mut power: u64 = 1;
608 + let mut lam: u64 = 0;
609 + loop {
610 + if let Some(d) = self.step(&mut h) {
611 + return d;
612 + }
613 + lam += 1;
614 + if t.p.x == h.p.x {
615 + if let Some(d) = self.collide(&t, &h) {
616 + return d;
617 + }
618 + let len = if t.p == h.p && t.u == h.u && t.v == h.v { self.cycle_length(&h) } else { None };
619 + if let Some(len) = len {
620 + if let Some(d) = self.escape_cycle(&mut h, len) {
621 + return d;
622 + }
623 + t = h;
624 + } else {
625 + self.st.useless_collisions += 1;
626 + match self.random_walk() {
627 + Ok(w) => {
628 + t = w;
629 + h = t;
630 + }
631 + Err(d) => return d,
632 + }
633 + }
634 + power = 1;
635 + lam = 0;
636 + continue;
637 + }
638 + if power == lam {
639 + t = h;
640 + power *= 2;
641 + lam = 0;
642 + }
643 + }
644 + }
645 +
646 + /// Length of the pure-f cycle through w.p, if w.p lies on one of length ≤ 4·cycle_check (+1024).
647 + /// Floyd/Brent can also meet an earlier point of the *augmented* trajectory (after an escape),
648 + /// which is not an f-cycle: then None is returned and the caller restarts the walk.
649 + fn cycle_length(&mut self, w: &Walk) -> Option<u64> {
650 + let bound = 4 * self.sh.cfg.cycle_check + 1024;
651 + let mut cur = *w;
652 + let mut len = 0u64;
653 + loop {
654 + self.step_single(&mut cur)?;
655 + len += 1;
656 + if cur.p == w.p {
657 + return Some(len);
658 + }
659 + if len > bound {
660 + return None;
661 + }
662 + }
663 + }
664 +}
665 +
666 +#[derive(Clone, Copy, Debug)]
667 +pub struct DpEntry {
668 + pub y: u64,
669 + pub u: u64,
670 + pub v: u64,
671 +}
672 +
673 +#[derive(Clone, Debug, Serialize, Deserialize)]
674 +pub struct RhoResult {
675 + pub d: Option<u64>,
676 + pub stats: WalkStats,
677 + pub dp_count: u64,
678 + pub dp_bytes: u64,
679 + pub table_entries: u64,
680 + pub threads: usize,
681 + pub wall_s: f64,
682 +}
683 +
684 +/// Expected number of iterations of a random mapping on a set of size n: √(πn/2).
685 +pub fn expected_iterations(n: u64, class_size: u64) -> f64 {
686 + (std::f64::consts::PI * (n as f64) / (2.0 * class_size as f64)).sqrt()
687 +}
688 +
689 +pub fn solve_rho(inst: &Instance, cfg: &RhoConfig) -> RhoResult {
690 + let t0 = Instant::now();
691 + let sh = Shared::new(inst, cfg);
692 + let mut total = WalkStats { fruitless_by_length: vec![0; 65], ..Default::default() };
693 + total.ops.merge(&sh.setup_ops);
694 +
695 + // trivial cases
696 + if inst.q.inf {
697 + return RhoResult { d: Some(0), stats: total, dp_count: 0, dp_bytes: 0, table_entries: 0, threads: 1, wall_s: t0.elapsed().as_secs_f64() };
698 + }
699 +
700 + match cfg.mode {
701 + RhoMode::Floyd | RhoMode::Brent => {
702 + let cpu0 = crate::stats::thread_cpu_s();
703 + let mut ctx = Ctx::new(&sh, 0);
704 + let d = if cfg.mode == RhoMode::Floyd { ctx.floyd() } else { ctx.brent() };
705 + ctx.st.cpu_s = crate::stats::thread_cpu_s() - cpu0;
706 + total.merge(&ctx.st);
707 + RhoResult { d: Some(d), stats: total, dp_count: 0, dp_bytes: 0, table_entries: 0, threads: 1, wall_s: t0.elapsed().as_secs_f64() }
708 + }
709 + RhoMode::Dp => {
710 + let threads = cfg.threads.max(1);
711 + let class = (if cfg.neg { 2 } else { 1 }) * (if cfg.aut3 { 3 } else { 1 });
712 + let expected_dps = expected_iterations(inst.n.n, class) / (1u64 << cfg.dp_bits.min(63)) as f64;
713 + let table: Mutex<hashbrown::HashMap<u64, DpEntry, crate::stats::FxBuild>> =
714 + Mutex::new(hashbrown::HashMap::with_capacity_and_hasher((expected_dps * 2.0) as usize + 1024, Default::default()));
715 + let found: Mutex<Option<u64>> = Mutex::new(None);
716 + let stop = AtomicBool::new(false);
717 + let stats: Mutex<Vec<WalkStats>> = Mutex::new(Vec::new());
718 +
719 + std::thread::scope(|s| {
720 + for t in 0..threads {
721 + let sh = &sh;
722 + let table = &table;
723 + let found = &found;
724 + let stop = &stop;
725 + let stats = &stats;
726 + s.spawn(move || {
727 + let cpu0 = crate::stats::thread_cpu_s();
728 + let mut ctx = Ctx::new(sh, t as u64);
729 + let mut walks: Vec<Walk> = Vec::with_capacity(sh.cfg.walks);
730 + let mut solved: Option<u64> = None;
731 + for _ in 0..sh.cfg.walks.max(1) {
732 + match ctx.random_walk() {
733 + Ok(w) => walks.push(w),
734 + Err(d) => {
735 + solved = Some(d);
736 + break;
737 + }
738 + }
739 + }
740 + let mut dps: Vec<DpRecord> = Vec::new();
741 + while solved.is_none() && !stop.load(Ordering::Relaxed) {
742 + if let Some(d) = ctx.batch_step(&mut walks, &mut dps) {
743 + solved = Some(d);
744 + break;
745 + }
746 + if !dps.is_empty() {
747 + let mut tab = table.lock().unwrap();
748 + for rec in dps.drain(..) {
749 + match tab.get(&rec.x) {
750 + None => {
751 + tab.insert(rec.x, DpEntry { y: rec.y, u: rec.u, v: rec.v });
752 + }
753 + Some(e) => {
754 + let e = *e;
755 + let a = Walk { p: Pt::new(rec.x, e.y), u: e.u, v: e.v, ovr: NO_OVR, tries: 0, steps: 0, saved: Pt::INF, saved_step: 0, last_escape_x: 0 };
756 + let b = Walk { p: Pt::new(rec.x, rec.y), u: rec.u, v: rec.v, ovr: NO_OVR, tries: 0, steps: 0, saved: Pt::INF, saved_step: 0, last_escape_x: 0 };
757 + if a.u == b.u && a.v == b.v && a.p.y == b.p.y {
758 + continue; // identical trajectory data, nothing to learn
759 + }
760 + if let Some(d) = ctx.collide(&a, &b) {
761 + solved = Some(d);
762 + break;
763 + }
764 + ctx.st.useless_collisions += 1;
765 + }
766 + }
767 + }
768 + }
769 + }
770 + if let Some(d) = solved {
771 + let mut f = found.lock().unwrap();
772 + if f.is_none() {
773 + *f = Some(d);
774 + }
775 + stop.store(true, Ordering::Relaxed);
776 + }
777 + ctx.st.cpu_s = crate::stats::thread_cpu_s() - cpu0;
778 + stats.lock().unwrap().push(ctx.st);
779 + });
780 + }
781 + });
782 + for st in stats.lock().unwrap().iter() {
783 + total.merge(st);
784 + }
785 + let tab = table.lock().unwrap();
786 + let entries = tab.len() as u64;
787 + let bytes = (tab.capacity() as u64) * (std::mem::size_of::<(u64, DpEntry)>() as u64 + 1);
788 + let d = *found.lock().unwrap();
789 + RhoResult { d, stats: total, dp_count: entries, dp_bytes: bytes, table_entries: entries, threads, wall_s: t0.elapsed().as_secs_f64() }
790 + }
791 + }
792 +}
added experiments/synthetic_ecdlp/rho_engine/src/stats.rs +62 −0
@@ -0,0 +1,62 @@
1 +//! Timers, memory measurement and the hash function used by the tables.
2 +
3 +use std::hash::{BuildHasherDefault, Hasher};
4 +
5 +/// CPU time consumed by the calling thread (seconds), via CLOCK_THREAD_CPUTIME_ID.
6 +pub fn thread_cpu_s() -> f64 {
7 + let mut ts = libc::timespec { tv_sec: 0, tv_nsec: 0 };
8 + unsafe {
9 + libc::clock_gettime(libc::CLOCK_THREAD_CPUTIME_ID, &mut ts);
10 + }
11 + ts.tv_sec as f64 + ts.tv_nsec as f64 * 1e-9
12 +}
13 +
14 +/// Process CPU time (user + system) via getrusage.
15 +pub fn process_cpu_s() -> f64 {
16 + let mut ru: libc::rusage = unsafe { std::mem::zeroed() };
17 + unsafe {
18 + libc::getrusage(libc::RUSAGE_SELF, &mut ru);
19 + }
20 + let tv = |t: libc::timeval| t.tv_sec as f64 + t.tv_usec as f64 * 1e-6;
21 + tv(ru.ru_utime) + tv(ru.ru_stime)
22 +}
23 +
24 +/// Peak resident set size in bytes (ru_maxrss is bytes on macOS, kilobytes on Linux).
25 +pub fn peak_rss_bytes() -> u64 {
26 + let mut ru: libc::rusage = unsafe { std::mem::zeroed() };
27 + unsafe {
28 + libc::getrusage(libc::RUSAGE_SELF, &mut ru);
29 + }
30 + let v = ru.ru_maxrss as u64;
31 + if cfg!(target_os = "macos") {
32 + v
33 + } else {
34 + v * 1024
35 + }
36 +}
37 +
38 +/// Cheap multiplicative hasher for u64 keys that are already well-mixed field residues.
39 +#[derive(Default, Clone, Copy)]
40 +pub struct FxHasher {
41 + h: u64,
42 +}
43 +
44 +impl Hasher for FxHasher {
45 + #[inline(always)]
46 + fn finish(&self) -> u64 {
47 + self.h
48 + }
49 + #[inline(always)]
50 + fn write(&mut self, bytes: &[u8]) {
51 + for b in bytes {
52 + self.h = (self.h.rotate_left(5) ^ (*b as u64)).wrapping_mul(0x517cc1b727220a95);
53 + }
54 + }
55 + #[inline(always)]
56 + fn write_u64(&mut self, i: u64) {
57 + let z = i.wrapping_mul(0x9E3779B97F4A7C15);
58 + self.h = z ^ (z >> 32);
59 + }
60 +}
61 +
62 +pub type FxBuild = BuildHasherDefault<FxHasher>;
added experiments/synthetic_ecdlp/rho_engine/tests/fixtures.rs +180 −0
@@ -0,0 +1,180 @@
1 +//! (b) Fixture instances generated by the Python reference (`scripts/gen_fixtures.py` → `src/wp34/ec.py`
2 +//! + PARI/GP): 60 prime-order curves of 20–32 bits (10 of them j = 0 per size) with known d.
3 +//! Every algorithm / option combination must recover d, and 1-thread runs must be deterministic.
4 +
5 +use rho_engine::{run, SolveSpec};
6 +use serde::Deserialize;
7 +use std::path::PathBuf;
8 +
9 +#[derive(Deserialize, Clone)]
10 +struct Fx {
11 + label: String,
12 + j0: bool,
13 + p: String,
14 + a: String,
15 + b: String,
16 + n: String,
17 + gx: String,
18 + gy: String,
19 + qx: String,
20 + qy: String,
21 + d: String,
22 + #[serde(default)]
23 + beta: Option<String>,
24 + #[serde(default)]
25 + lambda: Option<String>,
26 +}
27 +
28 +#[derive(Deserialize)]
29 +struct Doc {
30 + instances: Vec<Fx>,
31 +}
32 +
33 +fn fixture_path() -> PathBuf {
34 + let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
35 + p.push("../../../tests/fixtures/ecdlp_instances.json");
36 + p
37 +}
38 +
39 +fn load() -> Vec<Fx> {
40 + let text = std::fs::read_to_string(fixture_path()).expect("fixture tests/fixtures/ecdlp_instances.json (run scripts/gen_fixtures.py)");
41 + let doc: Doc = serde_json::from_str(&text).unwrap();
42 + assert!(doc.instances.len() >= 50);
43 + doc.instances
44 +}
45 +
46 +fn spec(fx: &Fx, alg: &str, seed: u64) -> SolveSpec {
47 + SolveSpec {
48 + p: fx.p.clone(),
49 + a: fx.a.clone(),
50 + b: fx.b.clone(),
51 + n: fx.n.clone(),
52 + gx: fx.gx.clone(),
53 + gy: fx.gy.clone(),
54 + qx: fx.qx.clone(),
55 + qy: fx.qy.clone(),
56 + alg: alg.into(),
57 + seed,
58 + label: Some(fx.label.clone()),
59 + expected_d: Some(fx.d.clone()),
60 + r: 20,
61 + cycle: None,
62 + dp: None,
63 + threads: 1,
64 + walks: None,
65 + mixed: 0,
66 + neg: false,
67 + aut3: false,
68 + beta: fx.beta.clone(),
69 + lambda: fx.lambda.clone(),
70 + cycle_check: 32,
71 + max_walk_len: None,
72 + lo: None,
73 + hi: None,
74 + memory: None,
75 + chains: 64,
76 + }
77 +}
78 +
79 +fn check(s: &SolveSpec, what: &str) -> serde_json::Value {
80 + let out = run(s);
81 + assert_eq!(out.get("ok").and_then(|v| v.as_bool()), Some(true), "{what}: {out}");
82 + assert_eq!(out.get("match").and_then(|v| v.as_bool()), Some(true), "{what}: {out}");
83 + assert_eq!(out["d"].as_str(), s.expected_d.as_deref(), "{what}");
84 + out
85 +}
86 +
87 +#[test]
88 +fn fixtures_every_algorithm_recovers_d() {
89 + let fxs = load();
90 + for fx in &fxs {
91 + let seed = 7;
92 + check(&spec(fx, "bsgs", seed), &format!("{} bsgs", fx.label));
93 + let mut s = spec(fx, "bsgs", seed);
94 + s.memory = Some("1000".into());
95 + s.threads = 2;
96 + check(&s, &format!("{} bsgs --memory 1000 --threads 2", fx.label));
97 +
98 + let mut s = spec(fx, "rho", seed);
99 + s.cycle = Some("floyd".into());
100 + check(&s, &format!("{} rho floyd", fx.label));
101 + s.cycle = Some("brent".into());
102 + check(&s, &format!("{} rho brent", fx.label));
103 + s.cycle = Some("dp".into());
104 + check(&s, &format!("{} rho dp 1 thread", fx.label));
105 + s.threads = 4;
106 + check(&s, &format!("{} rho dp 4 threads", fx.label));
107 + let mut s = spec(fx, "rho", seed);
108 + s.neg = true;
109 + check(&s, &format!("{} rho neg", fx.label));
110 + s.cycle = Some("brent".into());
111 + check(&s, &format!("{} rho neg brent", fx.label));
112 + let mut s = spec(fx, "rho", seed);
113 + s.mixed = 4;
114 + check(&s, &format!("{} rho mixed 4", fx.label));
115 + if fx.j0 {
116 + let mut s = spec(fx, "rho", seed);
117 + s.aut3 = true;
118 + check(&s, &format!("{} rho aut3", fx.label));
119 + s.neg = true;
120 + check(&s, &format!("{} rho neg aut3", fx.label));
121 + s.cycle = Some("floyd".into());
122 + check(&s, &format!("{} rho neg aut3 floyd", fx.label));
123 + s.cycle = None;
124 + s.threads = 4;
125 + check(&s, &format!("{} rho neg aut3 4 threads", fx.label));
126 + }
127 + let s = spec(fx, "kangaroo", seed);
128 + check(&s, &format!("{} kangaroo [0, n)", fx.label));
129 + let mut s = spec(fx, "kangaroo", seed);
130 + s.threads = 4;
131 + check(&s, &format!("{} kangaroo 4 threads", fx.label));
132 + // tight interval around d
133 + let d: u64 = fx.d.parse().unwrap();
134 + let n: u64 = fx.n.parse().unwrap();
135 + let mut s = spec(fx, "kangaroo", seed);
136 + s.lo = Some(d.saturating_sub(50_000).to_string());
137 + s.hi = Some((d + 50_000).min(n - 1).to_string());
138 + check(&s, &format!("{} kangaroo tight", fx.label));
139 + }
140 +}
141 +
142 +#[test]
143 +fn one_thread_runs_are_deterministic() {
144 + let fxs = load();
145 + for fx in fxs.iter().filter(|f| f.label.starts_with("28b") || f.label.starts_with("32b-j0")).take(6) {
146 + for (alg, neg, aut3, cycle) in [("rho", false, false, None), ("rho", true, false, None), ("rho", true, fx.j0, None), ("rho", false, false, Some("brent")), ("kangaroo", false, false, None)] {
147 + let mut s = spec(fx, alg, 99);
148 + s.neg = neg;
149 + s.aut3 = aut3;
150 + s.cycle = cycle.map(String::from);
151 + let a = check(&s, "det a");
152 + let b = check(&s, "det b");
153 + for key in ["iterations", "ops", "dp_count", "fruitless_cycles", "restarts"] {
154 + assert_eq!(a.get(key), b.get(key), "{} {alg} neg={neg} aut3={aut3} key={key}", fx.label);
155 + }
156 + }
157 + }
158 +}
159 +
160 +#[test]
161 +fn wrong_beta_or_lambda_aborts() {
162 + let fxs = load();
163 + let fx = fxs.iter().find(|f| f.j0).unwrap();
164 + let mut s = spec(fx, "rho", 1);
165 + s.aut3 = true;
166 + let beta: u64 = fx.beta.as_ref().unwrap().parse().unwrap();
167 + s.beta = Some((beta + 1).to_string());
168 + let out = run(&s);
169 + assert_eq!(out.get("ok").and_then(|v| v.as_bool()), Some(false));
170 + assert!(out.get("error").is_some());
171 + // right beta but the other cube root of unity as lambda (λ² instead of λ) must also abort
172 + let mut s = spec(fx, "rho", 1);
173 + s.aut3 = true;
174 + let n: u64 = fx.n.parse().unwrap();
175 + let lam: u128 = fx.lambda.as_ref().unwrap().parse().unwrap();
176 + s.lambda = Some(((lam * lam) % n as u128).to_string());
177 + let out = run(&s);
178 + assert_eq!(out.get("ok").and_then(|v| v.as_bool()), Some(false), "{out}");
179 + assert!(out["error"].as_str().unwrap().contains("lambda*G"));
180 +}
added experiments/synthetic_ecdlp/rho_engine/tests/small_curves.rs +169 −0
@@ -0,0 +1,169 @@
1 +//! (a) Tiny curves: enumerate the group by naive point counting, brute-force every discrete log,
2 +//! and check that every algorithm/option combination returns it.
3 +
4 +use rho_engine::bsgs::{solve_bsgs, BsgsConfig};
5 +use rho_engine::curve::{Counters, Curve, Pt};
6 +use rho_engine::field::Field;
7 +use rho_engine::instance::Instance;
8 +use rho_engine::kangaroo::{solve_kangaroo, KangarooConfig};
9 +use rho_engine::rho::{solve_rho, RhoConfig, RhoMode};
10 +
11 +fn is_prime(n: u64) -> bool {
12 + if n < 2 {
13 + return false;
14 + }
15 + let mut d = 2;
16 + while d * d <= n {
17 + if n % d == 0 {
18 + return false;
19 + }
20 + d += 1;
21 + }
22 + true
23 +}
24 +
25 +/// #E(F_p) by Legendre symbols (p tiny).
26 +fn count_points(p: u64, a: u64, b: u64) -> u64 {
27 + let f = Field::new(p);
28 + let mut total = 1;
29 + for x in 0..p {
30 + let rhs = (((x * x % p) * x % p) + a * x % p + b) % p;
31 + if rhs == 0 {
32 + total += 1;
33 + } else if f.legendre(rhs) == 1 {
34 + total += 2;
35 + }
36 + }
37 + total
38 +}
39 +
40 +/// Deterministic list of small prime-order curves: (p, a, b, n, G).
41 +fn small_prime_order_curves() -> Vec<(u64, u64, u64, u64, (u64, u64))> {
42 + let mut out = Vec::new();
43 + let mut seed = 12345u64;
44 + let mut next = || {
45 + seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
46 + seed >> 33
47 + };
48 + for &p in &[101u64, 211, 503, 1009, 2003, 4099, 8191, 16411, 32771] {
49 + let mut found = 0;
50 + let mut tries = 0;
51 + while found < 2 && tries < 5000 {
52 + tries += 1;
53 + let a = next() % p;
54 + let b = next() % p;
55 + if (4 * a * a % p * a + 27 * b * b) % p == 0 {
56 + continue;
57 + }
58 + let n = count_points(p, a, b);
59 + if !is_prime(n) || n == p {
60 + continue;
61 + }
62 + let curve = Curve::new(p, a, b);
63 + let f = &curve.f;
64 + // find a point
65 + let mut g = None;
66 + for x in 0..p {
67 + let rhs = (((x * x % p) * x % p) + a * x % p + b) % p;
68 + if let Some(y) = f.sqrt_plain(rhs) {
69 + g = Some((x, y));
70 + break;
71 + }
72 + }
73 + let g = g.unwrap();
74 + out.push((p, a, b, n, g));
75 + found += 1;
76 + }
77 + }
78 + assert!(out.len() >= 12, "expected enough small curves, got {}", out.len());
79 + out
80 +}
81 +
82 +#[test]
83 +fn small_curves_all_algorithms_brute_force() {
84 + let curves = small_prime_order_curves();
85 + let mut checked = 0usize;
86 + for (p, a, b, n, (gx, gy)) in curves {
87 + let curve = Curve::new(p, a, b);
88 + let g = curve.point_from_std(gx, gy);
89 + let mut c = Counters::default();
90 + // brute-force table of all multiples
91 + let mut mults: Vec<Pt> = Vec::with_capacity(n as usize);
92 + let mut r = Pt::INF;
93 + for _ in 0..n {
94 + mults.push(r);
95 + r = curve.add(&r, &g, &mut c);
96 + }
97 + assert!(r.inf, "n·G must be O");
98 + // sample of d values: all for n ≤ 300, otherwise 25 spread values + extremes
99 + let ds: Vec<u64> = if n <= 300 { (1..n).collect() } else { (0..25).map(|i| 1 + i * (n - 2) / 24).chain([1, 2, n - 1, n - 2, (n + 1) / 2]).collect() };
100 + for d in ds {
101 + let q = mults[d as usize];
102 + let (qx, qy) = curve.point_to_std(&q).unwrap();
103 + let inst = Instance::new(p, a, b, n, gx, gy, qx, qy).unwrap();
104 + // BSGS, default and reduced memory, 1 and 2 threads
105 + let verbose = std::env::var("RHO_TEST_VERBOSE").is_ok();
106 + for (mem, th) in [(None, 1), (Some(3), 2), (Some(1), 1)] {
107 + if verbose { eprintln!("bsgs p={p} n={n} d={d} mem={mem:?} th={th}"); }
108 + let res = solve_bsgs(&inst, &BsgsConfig { memory: mem, threads: th, chains: 4 });
109 + assert_eq!(res.d, Some(d), "bsgs p={p} n={n} d={d} mem={mem:?} threads={th}");
110 + }
111 + // rho: floyd, brent, dp (1 and 2 threads), with neg, mixed
112 + let base = RhoConfig { r: 8, mixed: 0, neg: false, aut3: false, cycle_check: 8, dp_bits: 1, walks: 4, threads: 1, mode: RhoMode::Floyd, seed: d ^ p, max_walk_len: 1 << 12 };
113 + let variants = vec![
114 + RhoConfig { ..base.clone() },
115 + RhoConfig { mode: RhoMode::Brent, ..base.clone() },
116 + RhoConfig { mode: RhoMode::Dp, ..base.clone() },
117 + RhoConfig { mode: RhoMode::Dp, threads: 2, ..base.clone() },
118 + RhoConfig { mode: RhoMode::Dp, neg: true, ..base.clone() },
119 + RhoConfig { mode: RhoMode::Brent, neg: true, r: 3, ..base.clone() },
120 + RhoConfig { mode: RhoMode::Floyd, neg: true, mixed: 2, ..base.clone() },
121 + RhoConfig { mode: RhoMode::Dp, mixed: 3, ..base.clone() },
122 + ];
123 + for cfg in variants {
124 + if verbose { eprintln!("rho p={p} n={n} d={d} {:?} neg={} mixed={} th={}", cfg.mode, cfg.neg, cfg.mixed, cfg.threads); }
125 + let res = solve_rho(&inst, &cfg);
126 + assert_eq!(res.d, Some(d), "rho p={p} n={n} d={d} cfg={cfg:?}");
127 + }
128 + // kangaroo: full interval, and a tight interval around d
129 + let lo = d.saturating_sub(7);
130 + let hi = (d + 9).min(n - 1);
131 + for (lo, hi, th) in [(0, n - 1, 1), (lo, hi, 1), (0, n - 1, 2)] {
132 + if verbose { eprintln!("kangaroo p={p} n={n} d={d} [{lo},{hi}] th={th}"); }
133 + let res = solve_kangaroo(&inst, &KangarooConfig { lo, hi, threads: th, walks: 0, dp_bits: 1, seed: d });
134 + assert_eq!(res.d, Some(d), "kangaroo p={p} n={n} d={d} [{lo},{hi}] threads={th}");
135 + }
136 + checked += 1;
137 + }
138 + }
139 + eprintln!("small curves: {checked} (curve, d) pairs checked");
140 + assert!(checked > 200);
141 +}
142 +
143 +#[test]
144 +fn group_law_matches_naive_scalar_multiplication() {
145 + // cross-check batch-free add/dbl against repeated addition on a mid-size curve
146 + let (p, a, b) = (1_000_003u64, 5u64, 7u64);
147 + let curve = Curve::new(p, a, b);
148 + let f = &curve.f;
149 + let mut g = None;
150 + for x in 1..p {
151 + let rhs = (((x * x % p) * x % p) + a * x % p + b) % p;
152 + if let Some(y) = f.sqrt_plain(rhs) {
153 + g = Some(curve.point_from_std(x, y));
154 + break;
155 + }
156 + }
157 + let g = g.unwrap();
158 + assert!(curve.is_on_curve(&g));
159 + let mut c = Counters::default();
160 + let mut r = Pt::INF;
161 + for k in 0..500u64 {
162 + assert!(curve.eq(&r, &curve.mul(k, &g, &mut c)), "k={k}");
163 + assert!(curve.is_on_curve(&r));
164 + r = curve.add(&r, &g, &mut c);
165 + }
166 + let d2 = curve.dbl(&g, &mut c);
167 + assert!(curve.eq(&d2, &curve.add(&g, &g, &mut c)));
168 + assert!(curve.add(&g, &curve.neg(&g), &mut c).inf);
169 +}
added experiments/synthetic_ecdlp/run_batch_remote.py +45 −0
@@ -0,0 +1,45 @@
1 +"""Run a prepared jobs file through the Rust engine (optionally K engine processes in parallel, each job single-threaded)
2 +and record provenance on the executing host. Stdlib only, Python ≥ 3.9.
3 +
4 +Usage: python3 run_batch_remote.py --engine ./rho_engine --jobs study.jobs.json --out study_<host>.json [--parallel K] [--params params.json]
5 +"""
6 +import argparse, json, pathlib, subprocess, sys, time, os
7 +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2] / "src"))
8 +from wp34.provenance import record
9 +
10 +
11 +def main():
12 + ap = argparse.ArgumentParser()
13 + ap.add_argument("--engine", required=True); ap.add_argument("--jobs", required=True); ap.add_argument("--out", required=True)
14 + ap.add_argument("--params", default=None); ap.add_argument("--parallel", type=int, default=1); ap.add_argument("--seed", type=int, default=34)
15 + a = ap.parse_args(); t0 = time.time()
16 + jobs = json.loads(pathlib.Path(a.jobs).read_text())
17 + out = pathlib.Path(a.out); out.parent.mkdir(parents=True, exist_ok=True)
18 + K = max(1, a.parallel)
19 + chunks = [jobs[i::K] for i in range(K)]
20 + procs, files = [], []
21 + for i, ch in enumerate(chunks):
22 + if not ch:
23 + continue
24 + jf = out.with_suffix(f".chunk{i}.jobs.json"); rf = out.with_suffix(f".chunk{i}.raw.json")
25 + jf.write_text(json.dumps(ch)); files.append((i, rf))
26 + procs.append(subprocess.Popen([a.engine, "batch", "--file", str(jf), "--out", str(rf)], stdout=subprocess.DEVNULL, stderr=open(out.with_suffix(f".chunk{i}.err"), "w")))
27 + for p in procs:
28 + p.wait()
29 + # merge in original order
30 + merged = [None] * len(jobs)
31 + for i, rf in files:
32 + res = json.loads(rf.read_text())
33 + for k, r in enumerate(res):
34 + merged[i + k * K] = r
35 + ok = sum(1 for r in merged if r and r.get("ok"))
36 + params = json.loads(pathlib.Path(a.params).read_text()) if a.params else {}
37 + params.update({"engine": a.engine, "jobs_file": a.jobs, "parallel_processes": K})
38 + record("ecdlp_study", params, {"n_jobs": len(jobs), "n_ok": ok, "runs": merged}, seed=a.seed, out=str(out), started=t0)
39 + for i, rf in files:
40 + rf.unlink(); out.with_suffix(f".chunk{i}.jobs.json").unlink()
41 + print(f"done: {ok}/{len(jobs)} verified in {time.time()-t0:.0f}s on {os.uname().nodename} → {out}")
42 +
43 +
44 +if __name__ == "__main__":
45 + main()
added figures/ec_geometry.pdf +0 −0

Binary file not shown.

added figures/ecdlp_constants.pdf +0 −0

Binary file not shown.

added figures/ecdlp_endomorphism.pdf +0 −0

Binary file not shown.

added figures/ecdlp_parallel.pdf +0 −0

Binary file not shown.

added figures/ecdlp_scaling.pdf +0 −0

Binary file not shown.

added figures/ecdlp_sweep_r.pdf +0 −0

Binary file not shown.

added figures/ecdlp_tmto.pdf +0 −0

Binary file not shown.

added figures/make_sha256_figures.py +189 −0
@@ -0,0 +1,189 @@
1 +"""Regenerate the SHA-256 (Axis C) figures from results/reduced_round_sha256/*.json.
2 +
3 + .venv/bin/python figures/make_sha256_figures.py # all figures
4 +Figures: sha256_diffusion.pdf, sha256_bias.pdf, sha256_sat_scaling.pdf, sha256_collision_scaling.pdf,
5 +sha256_local_collision.pdf (vector PDF, serif fonts).
6 +"""
7 +from __future__ import annotations
8 +import glob, json, pathlib, sys
9 +import numpy as np
10 +import matplotlib
11 +matplotlib.use("Agg")
12 +import matplotlib.pyplot as plt
13 +
14 +ROOT = pathlib.Path(__file__).resolve().parents[1]
15 +RES = ROOT / "results" / "reduced_round_sha256"
16 +FIG = ROOT / "figures"
17 +plt.rcParams.update({"font.family": "serif", "font.serif": ["Times New Roman", "Times", "DejaVu Serif"],
18 + "mathtext.fontset": "stix", "font.size": 9, "axes.labelsize": 9, "legend.fontsize": 7.5,
19 + "xtick.labelsize": 8, "ytick.labelsize": 8, "pdf.fonttype": 42, "axes.spines.top": False,
20 + "axes.spines.right": False, "figure.dpi": 150})
21 +C = {"W0": "#1b4f72", "W5": "#b03a2e", "W15": "#239b56", "IV": "#7d3c98", "RO": "#7f8c8d",
22 + "cadical": "#1b4f72", "kissat": "#b03a2e", "z3": "#239b56"}
23 +
24 +
25 +def load(name):
26 + p = RES / name
27 + return json.loads(p.read_text()) if p.exists() else None
28 +
29 +
30 +def fig_diffusion():
31 + docs = [json.loads(p.read_text()) for p in sorted(RES.glob("diffusion_seed*.json"))]
32 + if not docs:
33 + return
34 + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(7.2, 2.9), gridspec_kw={"width_ratios": [1.35, 1]})
35 + M = np.mean([np.array(d["measurements"]["W0"]["mean_flip_matrix"]) for d in docs], axis=0)
36 + im = ax1.imshow(M[:32].T, aspect="auto", cmap="viridis", vmin=0, vmax=1, origin="lower",
37 + extent=[0.5, 32.5, -0.5, 255.5], interpolation="nearest")
38 + ax1.set_xlabel("step $t$"); ax1.set_ylabel("output bit (0 = MSB of $a$, 255 = LSB of $h$)")
39 + ax1.set_yticks([0, 32, 64, 96, 128, 160, 192, 224, 255])
40 + for y in range(32, 256, 32):
41 + ax1.axhline(y - 0.5, color="w", lw=0.3, alpha=0.6)
42 + ax1.set_title(r"(a) $\Pr[\mathrm{bit\ flips}]$, one-bit difference in $W_0$", fontsize=9)
43 + cb = fig.colorbar(im, ax=ax1, fraction=0.04, pad=0.02); cb.set_label("flip probability")
44 + steps = np.arange(1, 65)
45 + for cfg in ["W0", "W5", "W15", "IV"]:
46 + R = np.array([d["measurements"][cfg]["mean_flip_ratio_by_step"] for d in docs])
47 + ax2.plot(steps, R.mean(axis=0), color=C[cfg], lw=1.2,
48 + label={"W0": "$W_0$ bit", "W5": "$W_5$ bit", "W15": "$W_{15}$ bit", "IV": "IV bit"}[cfg])
49 + ro = np.mean([d["measurements"]["W0"]["random_oracle_control"]["flip_ratio_mean"] for d in docs])
50 + rosd = np.mean([d["measurements"]["W0"]["random_oracle_control"]["flip_ratio_sd"] for d in docs])
51 + ax2.axhline(ro, color=C["RO"], ls="--", lw=1, label="random oracle (%.3f)" % ro)
52 + ax2.axhspan(ro - 2 * rosd, ro + 2 * rosd, color=C["RO"], alpha=0.12, lw=0)
53 + ax2.axhline(0.49, color="k", ls=":", lw=0.7)
54 + ax2.set_xlim(0, 40); ax2.set_ylim(0, 0.56); ax2.set_xlabel("step $t$"); ax2.set_ylabel("mean flip ratio")
55 + ax2.set_title("(b) avalanche by step (N = %d x %d seeds)" % (docs[0]["params"]["n"], len(docs)), fontsize=9)
56 + ax2.legend(loc="lower right", frameon=False)
57 + fig.tight_layout(); fig.savefig(FIG / "sha256_diffusion.pdf"); plt.close(fig)
58 +
59 +
60 +def fig_bias():
61 + docs = [json.loads(p.read_text()) for p in sorted(RES.glob("bias_seed*.json"))]
62 + if not docs:
63 + return
64 + rs = docs[0]["params"]["r_list"]; n = docs[0]["params"]["n"]
65 + fig, axs = plt.subplots(1, 3, figsize=(7.2, 2.5))
66 + mk = ["o", "s", "^"]
67 + for i, d in enumerate(docs):
68 + by = d["measurements"]["by_r"]; ro = d["measurements"]["random_oracle_control"]
69 + axs[0].plot(rs, [by[str(r)]["max_bit_bias"] for r in rs], mk[i % 3], ms=3.5, color=C["W0"], alpha=0.8,
70 + label="seed %d" % d["seed"] if i == 0 or True else None)
71 + axs[0].axhline(ro["max_bit_bias"], color=C["RO"], ls="--", lw=0.8)
72 + axs[1].plot(rs, [by[str(r)]["n_pairs_corr_over_4sigma"] for r in rs], mk[i % 3], ms=3.5, color=C["W5"], alpha=0.8)
73 + axs[1].axhline(ro["n_pairs_corr_over_4sigma"], color=C["RO"], ls="--", lw=0.8)
74 + axs[2].plot(rs, [max(by[str(r)]["chi2_min_p"], 1e-12) for r in rs], mk[i % 3], ms=3.5, color=C["W15"], alpha=0.8)
75 + axs[2].axhline(ro["chi2_min_p"], color=C["RO"], ls="--", lw=0.8)
76 + thr = 4 * 0.5 / n ** 0.5
77 + axs[0].axhline(thr, color="k", ls=":", lw=0.8); axs[0].text(30, thr * 1.15, r"$4\sigma$", fontsize=7)
78 + axs[0].set_yscale("log"); axs[0].set_ylabel(r"$\max_j |\Pr[y_j=1]-\frac{1}{2}|$"); axs[0].set_title("(a) output-bit bias", fontsize=9)
79 + exp = docs[0]["measurements"]["by_r"][str(rs[0])]["expected_pairs_under_null"]
80 + axs[1].axhline(exp, color="k", ls=":", lw=0.8); axs[1].text(30, exp * 1.3, "null exp.", fontsize=7)
81 + axs[1].set_yscale("symlog", linthresh=1); axs[1].set_ylabel(r"# pairs with $|c_{ij}|>4/\sqrt{N}$")
82 + axs[1].set_title("(b) input/output linear correlations", fontsize=9)
83 + axs[2].set_yscale("log"); axs[2].set_ylim(3e-13, 1.5); axs[2].text(1.1, 2e-12, r"$p<10^{-12}$ (clipped)", fontsize=6); axs[2].axhline(0.01 / 32, color="k", ls=":", lw=0.8)
84 + axs[2].set_ylabel(r"min $p$ over 32 output bytes ($\chi^2$)"); axs[2].set_title("(c) byte uniformity", fontsize=9)
85 + for ax in axs:
86 + ax.set_xscale("log", base=2); ax.set_xlabel("steps $r$"); ax.set_xticks([1, 2, 4, 8, 16, 32, 64]); ax.set_xticklabels([1, 2, 4, 8, 16, 32, 64])
87 + axs[0].plot([], [], "--", color=C["RO"], label="random oracle"); axs[0].legend(frameon=False, loc="upper right")
88 + fig.suptitle("Statistical regularity of $r$-step SHA-256 (N = %d messages per seed, 3 seeds)" % n, fontsize=9, y=1.0)
89 + fig.tight_layout(); fig.savefig(FIG / "sha256_bias.pdf", bbox_inches="tight"); plt.close(fig)
90 +
91 +
92 +def _censored_plot(ax, xs, meds, solved_frac, timeout, color, marker, ls, label):
93 + xs = np.array(xs); meds = np.array(meds, dtype=float); sf = np.array(solved_frac)
94 + full = sf >= 0.5
95 + ax.plot(xs[full], meds[full], marker=marker, ls=ls, color=color, ms=4, lw=1, label=label, mfc=color)
96 + if (~full).any():
97 + ax.plot(xs[~full], np.full((~full).sum(), timeout * 1.25), marker=marker, ls="none", color=color, ms=4, mfc="white")
98 +
99 +
100 +def fig_sat_scaling():
101 + d = load("preimage_sat_scaling.json")
102 + if not d:
103 + return
104 + S = d["measurements"]["summary"]; T = d["params"]["timeout_s"]
105 + ks = d["params"]["k_list"]; rs = d["params"]["r_list"]
106 + cmap = plt.get_cmap("viridis")
107 + fig, axs = plt.subplots(1, 3, figsize=(7.2, 2.7), sharey=True)
108 + panels = [("first", "cadical", "(a) CaDiCaL, free bits in $W_0,W_1$"), ("first", "kissat", "(b) Kissat, free bits in $W_0,W_1$"),
109 + ("spread", "cadical", "(c) CaDiCaL, free bits spread over 16 words")]
110 + for ax, (pl, sol, title) in zip(axs, panels):
111 + for i, k in enumerate(ks):
112 + cells = [(r, S.get(f"{sol}:r{r}:k{k}" + ("" if pl == "first" else ":spread"))) for r in rs]
113 + cells = [(r, c) for r, c in cells if c]
114 + if not cells:
115 + continue
116 + _censored_plot(ax, [r for r, _ in cells], [max(c["median_wall_s"], 1e-3) for _, c in cells],
117 + [c["solved_fraction"] for _, c in cells], T, cmap(i / (len(ks) - 1)), "o" if sol == "cadical" else "^",
118 + "-" if sol == "cadical" else "--", "$k$ = %d" % k)
119 + if pl == "first" and sol == "cadical":
120 + for i, k in enumerate([16, 24, 32]):
121 + cells = [(r, S.get(f"z3:r{r}:k{k}")) for r in rs]; cells = [(r, c) for r, c in cells if c]
122 + if cells:
123 + _censored_plot(ax, [r for r, _ in cells], [max(c["median_wall_s"], 1e-3) for _, c in cells],
124 + [c["solved_fraction"] for _, c in cells], T, cmap(i / (len(ks) - 1)), "x", ":", "z3 $k$ = %d" % k if i == 0 else None)
125 + ax.axhline(T, color="k", ls=":", lw=0.7); ax.text(rs[0], T * 1.6, "timeout (open = median unsolved)", fontsize=6.5)
126 + ax.set_yscale("log"); ax.set_xlabel("steps $r$"); ax.set_title(title, fontsize=8.5); ax.set_xticks(rs); ax.tick_params(axis="x", labelsize=6.5)
127 + axs[0].set_ylabel("median wall time (s)"); axs[0].set_ylim(1e-3, T * 3)
128 + axs[0].legend(frameon=False, ncol=2, loc="lower right", fontsize=6)
129 + fig.suptitle("Preimage of $r$-step SHA-256 compression with $k$ free message bits (3 seeds per cell, timeout %d s)" % T, fontsize=9, y=1.0)
130 + fig.tight_layout(); fig.savefig(FIG / "sha256_sat_scaling.pdf", bbox_inches="tight"); plt.close(fig)
131 +
132 +
133 +def fig_collision_scaling():
134 + d = load("collision_sat.json")
135 + if not d:
136 + return
137 + S = d["measurements"]["summary"]; T = d["params"]["timeout_s"]; rs = d["params"]["r_list"]
138 + fig, axs = plt.subplots(1, 2, figsize=(7.2, 2.7), sharey=True)
139 + for ax, sol in zip(axs, ["cadical", "kissat"]):
140 + for (iv, ff), color, mk in [(("fixed", False), C["cadical"], "o"), (("fixed", True), C["cadical"], "s"),
141 + (("free", False), C["kissat"], "o"), (("free", True), C["kissat"], "s")]:
142 + cells = [(r, S.get(f"{sol}:iv_{iv}:ff{int(ff)}:r{r}")) for r in rs]; cells = [(r, c) for r, c in cells if c]
143 + if not cells:
144 + continue
145 + _censored_plot(ax, [r for r, _ in cells], [max(c["median_wall_s"], 1e-3) for _, c in cells],
146 + [c["solved_fraction"] for _, c in cells], T, color, mk, "-" if not ff else "--",
147 + ("standard IV" if iv == "fixed" else "semi-free-start") + (", feed-forward" if ff else ""))
148 + for r, c in cells: # mark proven UNSAT cells
149 + if c.get("n_unsat", 0) > 0 or c.get("all_unsat"):
150 + ax.annotate("UNSAT", (r, max(c["median_wall_s"], 1e-3)), fontsize=5.5, textcoords="offset points", xytext=(0, 4), ha="center")
151 + ax.axhline(T, color="k", ls=":", lw=0.7); ax.set_yscale("log"); ax.set_xlabel("steps $r$"); ax.set_xticks(rs)
152 + ax.set_title({"cadical": "(a) CaDiCaL", "kissat": "(b) Kissat"}[sol], fontsize=9)
153 + axs[0].set_ylabel("median wall time (s)"); axs[0].set_ylim(1e-3, T * 3); axs[0].legend(frameon=False, fontsize=6.5, loc="lower right")
154 + fig.suptitle("Collision search on $r$-step SHA-256 compression (timeout %d s; open markers = majority unsolved)" % T, fontsize=9, y=1.0)
155 + fig.tight_layout(); fig.savefig(FIG / "sha256_collision_scaling.pdf", bbox_inches="tight"); plt.close(fig)
156 +
157 +
158 +def fig_local_collision():
159 + d = load("local_collision.json")
160 + if not d:
161 + return
162 + emp = d["measurements"]["empirical"]; sat = d["measurements"]["sat"]; n = d["params"]["n_per_bit"]
163 + bits = [e["bit"] for e in emp]
164 + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(7.2, 2.6))
165 + s1 = [e["survival_fraction_by_step"][0] for e in emp]; s2 = [max(e["survival_fraction_by_step"][1], 0.5 / n) for e in emp]
166 + ax1.plot(bits, s1, "o-", ms=3, color=C["W0"], label="after step $i$ (difference $(\\delta,\\delta)$ in $a,e$)")
167 + ax1.plot(bits, s2, "s-", ms=3, color=C["W5"], label="after step $i+1$")
168 + pc = [e["p_collision"] for e in emp]
169 + ax1.plot(bits, [max(p, 0.5 / n) for p in pc], "^", ms=3.5, color=C["W15"], label="9-step collision (0 observed $\\Rightarrow$ shown at $1/(2N)$)")
170 + ax1.axhline(3 / n, color=C["RO"], ls="--", lw=0.8, label="rule-of-three 95% upper bound $3/N$")
171 + ax1.set_yscale("log"); ax1.set_xlabel("bit position of $\\delta$ in $W_i$"); ax1.set_ylabel("fraction of %d samples" % n)
172 + ax1.set_title("(a) conformance of the XOR pattern, real step", fontsize=9); ax1.legend(frameon=False, fontsize=6, loc="center", bbox_to_anchor=(0.5, 0.55))
173 + ax1.set_ylim(1e-6, 2)
174 + sb = [s["bit"] for s in sat]; st = [max(s["wall_s"], 1e-3) for s in sat]; ok = [s.get("verified") is True for s in sat]
175 + ax2.bar([b for b, o in zip(sb, ok) if o], [t for t, o in zip(st, ok) if o], color=C["W0"], width=0.8, label="conforming pair found (verified)")
176 + bad = [b for b, o in zip(sb, ok) if not o]
177 + if bad:
178 + ax2.bar(bad, [d["params"]["sat_timeout_s"]] * len(bad), color="white", edgecolor=C["W5"], width=0.8, label="timeout")
179 + ax2.set_yscale("log"); ax2.set_xlabel("bit position of $\\delta$"); ax2.set_ylabel("CaDiCaL wall time (s)")
180 + ax2.set_title("(b) SAT search for a conforming (state, $W_i..W_{i+8}$)", fontsize=9); ax2.legend(frameon=False, fontsize=6.5)
181 + fig.tight_layout(); fig.savefig(FIG / "sha256_local_collision.pdf", bbox_inches="tight"); plt.close(fig)
182 +
183 +
184 +if __name__ == "__main__":
185 + for f in (fig_diffusion, fig_bias, fig_sat_scaling, fig_collision_scaling, fig_local_collision):
186 + try:
187 + f(); print("ok", f.__name__)
188 + except Exception as e: # keep going so a missing result does not block the others
189 + print("FAILED", f.__name__, repr(e))
added figures/make_sig_figures.py +186 −0
@@ -0,0 +1,186 @@
1 +"""Regenerate the Axis B signature figures from results/signature_benchmarks/bench_*.json.
2 +
3 + .venv/bin/python figures/make_sig_figures.py
4 +
5 +Outputs (PDF, serif, publication quality):
6 + figures/sig_batch_verification.pdf - per-signature time vs batch size for every MSM algorithm,
7 + one panel per host, single-verify reference lines (k256 and
8 + libsecp256k1); second row: speedup S(b) with bootstrap 95 % CI.
9 + figures/sig_implementations.pdf - sign / verify ns per op across implementations (pure Python
10 + src/wp34, python-ecdsa, coincurve, k256, libsecp256k1), log scale.
11 +Pass --copy-to-paper to also copy them into paper/figures/.
12 +"""
13 +from __future__ import annotations
14 +import json, shutil
15 +from pathlib import Path
16 +
17 +import matplotlib
18 +matplotlib.use("Agg")
19 +import matplotlib.pyplot as plt # noqa: E402
20 +from matplotlib.ticker import FuncFormatter, LogLocator # noqa: E402
21 +
22 +ROOT = Path(__file__).resolve().parents[1]
23 +RES = ROOT / "results" / "signature_benchmarks"
24 +OUT = ROOT / "figures"
25 +
26 +# fixed categorical order (validated with the dataviz palette checker: light surface, all pairs pass)
27 +C = {"pippenger": "#2a78d6", "straus_wnaf5": "#eb6834", "k256_lincomb_vartime": "#1baf7a",
28 + "k256_lincomb_ct": "#6250d6", "single": "#52514e"}
29 +ALG_LABEL = {"pippenger": "own Pippenger (bucket, mixed adds)", "straus_wnaf5": "own Straus (wNAF-5, interleaved)",
30 + "k256_lincomb_vartime": "k256 lincomb_vartime (GLV + wNAF)", "k256_lincomb_ct": "k256 lincomb (constant time)"}
31 +ALG_ORDER = ["pippenger", "straus_wnaf5", "k256_lincomb_vartime", "k256_lincomb_ct"]
32 +
33 +plt.rcParams.update({
34 + "font.family": "serif", "font.serif": ["Times New Roman", "Times", "DejaVu Serif"], "font.size": 9,
35 + "axes.titlesize": 9.5, "axes.labelsize": 9, "legend.fontsize": 7.5, "xtick.labelsize": 8, "ytick.labelsize": 8,
36 + "axes.spines.top": False, "axes.spines.right": False, "axes.grid": True, "grid.color": "#e4e3df",
37 + "grid.linewidth": 0.5, "axes.edgecolor": "#8a8984", "axes.linewidth": 0.6, "pdf.fonttype": 42,
38 + "figure.dpi": 150, "savefig.bbox": "tight",
39 +})
40 +
41 +
42 +def load():
43 + docs = [json.loads(f.read_text()) for f in sorted(RES.glob("bench_*.json"))]
44 + docs.sort(key=lambda d: d["label"])
45 + return docs
46 +
47 +
48 +def short_label(d):
49 + return f"{d['label']}\n{d['host']['cpu']}, {d['host']['cores']} cores, macOS {d['host']['os'].split('(')[-1].rstrip(')')}"
50 +
51 +
52 +def fig_batch(docs):
53 + n = len(docs)
54 + fig, axes = plt.subplots(2, n, figsize=(3.4 * n + 0.6, 5.6), sharex=True, sharey="row", squeeze=False)
55 + for j, d in enumerate(docs):
56 + m = d["measurements"]["batch_verification"]
57 + ax, ax2 = axes[0][j], axes[1][j]
58 + ref = m["single_verify_reference_ns"]
59 + ax.axhline(ref["libsecp256k1_schnorr_verify"] / 1e3, color=C["single"], lw=1.0, ls="--",
60 + label="single verify, libsecp256k1 (reference)" if j == 0 else None)
61 + ax.axhline(ref["k256_verify_raw"] / 1e3, color=C["single"], lw=1.0, ls=":",
62 + label="single verify, k256 verify_raw (reference)" if j == 0 else None)
63 + for alg in ALG_ORDER:
64 + rows = sorted([r for r in m["results"] if r["algorithm"] == alg], key=lambda r: r["batch_size"])
65 + if not rows:
66 + continue
67 + xs = [r["batch_size"] for r in rows]
68 + ys = [r["per_signature"]["median_ns"] / 1e3 for r in rows]
69 + lo = [r["per_signature"]["q1_ns"] / 1e3 for r in rows]
70 + hi = [r["per_signature"]["q3_ns"] / 1e3 for r in rows]
71 + ax.fill_between(xs, lo, hi, color=C[alg], alpha=0.18, lw=0)
72 + ax.plot(xs, ys, color=C[alg], lw=1.6, marker="o", ms=3.2, label=ALG_LABEL[alg])
73 + sp = [r["speedup_vs_k256_single_verify"] for r in rows]
74 + ax2.fill_between(xs, [s["ci95_low"] for s in sp], [s["ci95_high"] for s in sp], color=C[alg], alpha=0.18, lw=0)
75 + ax2.plot(xs, [s["point"] for s in sp], color=C[alg], lw=1.6, marker="o", ms=3.2)
76 + if alg == "pippenger":
77 + sp2 = [r["speedup_vs_libsecp256k1_single_verify"] for r in rows]
78 + ax2.plot(xs, [s["point"] for s in sp2], color=C[alg], lw=1.2, ls="--", marker="s", ms=2.8,
79 + label="own Pippenger vs libsecp256k1 single verify")
80 + last = rows[-1]
81 + ax.annotate(f"{last['per_signature']['median_ns']/1e3:.1f} µs/sig", (xs[-1], ys[-1]), xytext=(-6, 6),
82 + textcoords="offset points", ha="right", va="bottom", fontsize=7, color=C[alg])
83 + ax2.axhline(1.0, color=C["single"], lw=0.8, ls="-")
84 + ax.set_xscale("log", base=2)
85 + ax.set_yscale("log")
86 + ax.set_ylim(5, 160)
87 + ax.set_title(short_label(d), loc="left")
88 + ax.yaxis.set_major_formatter(FuncFormatter(lambda v, _: f"{v:g}"))
89 + ax.yaxis.set_minor_locator(LogLocator(base=10, subs=(2, 5)))
90 + ax.yaxis.set_minor_formatter(FuncFormatter(lambda v, _: f"{v:g}"))
91 + ax2.set_xlabel("batch size $b$ (signatures per batch)")
92 + ax2.set_xticks([1, 4, 16, 64, 256, 1024, 2048])
93 + ax2.set_xticklabels(["1", "4", "16", "64", "256", "1024", "2048"])
94 + ax2.set_ylim(0, 3.6)
95 + if j == 0:
96 + ax.set_ylabel("time per signature (µs, median; band = IQR)")
97 + ax2.set_ylabel("speed-up $S(b)$ vs single k256 verify\n(band = bootstrap 95 % CI)")
98 + else:
99 + ax.tick_params(labelleft=False)
100 + ax2.tick_params(labelleft=False)
101 + h1, l1 = axes[0][0].get_legend_handles_labels()
102 + h2, l2 = axes[1][0].get_legend_handles_labels()
103 + fig.legend(h1 + h2, l1 + l2, loc="lower center", ncol=3, frameon=False, bbox_to_anchor=(0.5, -0.06))
104 + fig.suptitle("BIP340 batch verification on secp256k1 (k256 group arithmetic; 128-bit random weights, $a_1 = 1$; synthetic keys, seed 34)",
105 + fontsize=9.5, y=1.0)
106 + fig.tight_layout(h_pad=0.6)
107 + fig.savefig(OUT / "sig_batch_verification.pdf")
108 + plt.close(fig)
109 +
110 +
111 +def fig_impl(docs):
112 + impls = [ # (label, ecdsa_sign, ecdsa_verify, schnorr_sign, schnorr_verify) -> keys in sign_verify or python baselines
113 + ("pure Python\nsrc/wp34", "py:wp34_pure_python_ecdsa_sign", "py:wp34_pure_python_ecdsa_verify",
114 + "py:wp34_pure_python_schnorr_sign", "py:wp34_pure_python_schnorr_verify"),
115 + ("python-ecdsa\n(pure Python)", "py:python_ecdsa_pkg_ecdsa_sign", "py:python_ecdsa_pkg_ecdsa_verify", None, None),
116 + ("coincurve\n(CFFI, libsecp256k1)", "py:coincurve_ecdsa_sign_der", "py:coincurve_ecdsa_verify_der",
117 + "py:coincurve_schnorr_sign", "py:coincurve_schnorr_verify"),
118 + ("k256 0.14\n(Rust)", "rs:ecdsa_sign_k256", "rs:ecdsa_verify_k256", "rs:schnorr_sign_k256", "rs:schnorr_verify_k256"),
119 + ("libsecp256k1\n(C, via Rust)", "rs:ecdsa_sign_libsecp256k1", "rs:ecdsa_verify_libsecp256k1",
120 + "rs:schnorr_sign_libsecp256k1", "rs:schnorr_verify_libsecp256k1"),
121 + ]
122 + series = [("ECDSA sign", 1, "#2a78d6", ""), ("ECDSA verify", 2, "#2a78d6", "////"),
123 + ("Schnorr sign", 3, "#eb6834", ""), ("Schnorr verify", 4, "#eb6834", "////")]
124 + n = len(docs)
125 + fig, axes = plt.subplots(1, n, figsize=(4.6 * n + 0.4, 3.7), sharey=True, squeeze=False)
126 + for j, d in enumerate(docs):
127 + ax = axes[0][j]
128 + sv = d["measurements"]["sign_verify"]
129 + py = (d["measurements"].get("python_baselines") or {}).get("results") or {}
130 +
131 + def val(key):
132 + if key is None:
133 + return None
134 + src, k = key.split(":", 1)
135 + t = sv.get(k) if src == "rs" else py.get(k)
136 + return None if t is None else t["median_ns"]
137 +
138 + w = 0.2
139 + for s_i, (sname, col, color, hatch) in enumerate(series):
140 + xs, ys = [], []
141 + for i, imp in enumerate(impls):
142 + v = val(imp[col])
143 + if v is not None:
144 + xs.append(i + (s_i - 1.5) * w)
145 + ys.append(v)
146 + bars = ax.bar(xs, ys, width=w * 0.92, color=color, hatch=hatch, edgecolor="white", linewidth=0.6,
147 + label=sname if j == 0 else None, alpha=0.95 if not hatch else 0.75)
148 + for b_, v in zip(bars, ys):
149 + txt = f"{v/1e3:.0f}" if v >= 1e5 else f"{v/1e3:.1f}"
150 + ax.text(b_.get_x() + b_.get_width() / 2, v * 1.12, txt, ha="center", va="bottom", fontsize=5.8, rotation=90, color="#0b0b0b")
151 + for i, imp in enumerate(impls):
152 + if all(val(k) is None for k in imp[1:]):
153 + ax.text(i, 2.5e4, "not installed\non this host", ha="center", va="center", fontsize=6.5, color="#52514e")
154 + ax.set_yscale("log")
155 + ax.set_ylim(3e3, 5e7)
156 + ax.set_xticks(range(len(impls)))
157 + ax.set_xticklabels([i[0] for i in impls], fontsize=6.8)
158 + ax.set_title(short_label(d), loc="left")
159 + ax.yaxis.set_major_formatter(FuncFormatter(lambda v, _: f"{v/1e3:g}"))
160 + if j == 0:
161 + ax.set_ylabel("time per operation (µs, median of ≥ 7 reps; log scale)")
162 + ax.grid(axis="x", visible=False)
163 + fig.legend(loc="lower center", ncol=4, frameon=False, bbox_to_anchor=(0.5, -0.06))
164 + fig.suptitle("Implementation spread for secp256k1 signatures (32-byte prehash; Rust: 2000 ops/rep, Python: 20 ops/rep; labels in µs)",
165 + fontsize=9.5, y=1.0)
166 + fig.tight_layout()
167 + fig.savefig(OUT / "sig_implementations.pdf")
168 + plt.close(fig)
169 +
170 +
171 +def main():
172 + import sys
173 + docs = load()
174 + if not docs:
175 + raise SystemExit("no results/signature_benchmarks/bench_*.json")
176 + fig_batch(docs)
177 + fig_impl(docs)
178 + paper = ROOT / "paper" / "figures"
179 + if "--copy-to-paper" in sys.argv and paper.exists():
180 + for f in ("sig_batch_verification.pdf", "sig_implementations.pdf"):
181 + shutil.copy(OUT / f, paper / f)
182 + print("wrote", OUT / "sig_batch_verification.pdf", OUT / "sig_implementations.pdf")
183 +
184 +
185 +if __name__ == "__main__":
186 + main()
added figures/sha256_bias.pdf +0 −0

Binary file not shown.

added figures/sha256_diffusion.pdf +0 −0

Binary file not shown.

added figures/sha256_local_collision.pdf +0 −0

Binary file not shown.

added figures/sig_batch_verification.pdf +0 −0

Binary file not shown.

added figures/sig_implementations.pdf +0 −0

Binary file not shown.

added formal/.gitignore +1 −0
@@ -0,0 +1 @@
1 +/.lake
added formal/README.md +213 −0
@@ -0,0 +1,213 @@
1 +# WP34 — Stage 5: machine-checked statements (Lean 4, core only)
2 +
3 +Lean 4 project `wp34` (library `Wp34`). **No Mathlib**: only the Lean core library (`BitVec`,
4 +`Nat`, `List`) and the core tactics `bv_decide`, `decide`, `omega`, `simp`, `ac_rfl`.
5 +Toolchain pinned in `lean-toolchain`: `leanprover/lean4:v4.35.0`
6 +(Lean 4.35.0, commit `eb6cafb7`, arm64-apple-darwin; Lake 5.0.0).
7 +
8 +`bv_decide` bit-blasts a `BitVec`/`Bool` goal to an And-Inverter Graph (AIG), hands the CNF to the
9 +**CaDiCaL SAT solver bundled with Lean** and asks it for an **LRAT unsatisfiability certificate**,
10 +which is then re-checked by Lean's verified LRAT checker. The solver is never trusted; the checker
11 +is executed on the native (compiled) code path, which Lean 4.35 records as a per-theorem axiom
12 +named `<theorem>._native.bv_decide.ax_…` (same trust base as `Lean.ofReduceBool`: Lean kernel +
13 +Lean compiler). Everything else depends only on `propext`, `Quot.sound`, `Classical.choice`.
14 +The complete `#print axioms` output for the headline theorems is in `build.log` (module `Wp34.Audit`).
15 +
16 +```
17 +cd formal
18 +export PATH=$HOME/.elan/bin:$PATH
19 +lake clean && lake build # 32 s wall on an Apple M5 Max, exit code 0 (see build.log)
20 +```
21 +
22 +Tail of `build.log` (2026-11-01, `lake clean && lake build`):
23 +
24 +```
25 +✔ [2/9] Built Wp34.Group (315ms)
26 +✔ [3/9] Built Wp34.Sha256Words (2.5s)
27 +✔ [4/9] Built Wp34.Schedule (1.5s)
28 +✔ [5/9] Built Wp34.Sha256Step (27s)
29 +✔ [6/9] Built Wp34.LocalCollision (1.7s)
30 +✔ [8/9] Built Wp34 (193ms)
31 +# exit code 0 — wall time 32 s
32 +```
33 +
34 +(The `[7/9] Wp34.Audit` line is absent from the tail because Lake prints that module's
35 +`#print axioms` messages instead of a `✔` line; it is built — see `build.log`.)
36 +Build artefacts: `.lake/` ≈ 470 MB (the LRAT certificates are stored inside the `.olean` files;
37 +`Sha256Step.olean` 150 MB, `LocalCollision.olean` 34 MB, `Sha256Words.olean` 31 MB).
38 +
39 +Reproducibility gate: `tests/test_formal_lean.py` (static: no `sorry`, no `axiom`, no
40 +`native_decide`, no Mathlib, toolchain pinned; `WP34_LEAN_BUILD=1` additionally runs `lake build`).
41 +Helper scripts: `scripts/gf2_rank.py` (GF(2) rank/kernel of Σ0, Σ1, σ0, σ1 and generation of the
42 +explicit inverses), `scripts/local_collision.py` (simulation of the linearised local collisions and
43 +the real-step counter-example), `scripts/aig_sizes.py` (per-theorem AIG sizes → `scripts/aig_sizes.tsv`).
44 +
45 +## What is machine-checked and what is not
46 +
47 +**Machine-checked** (every `theorem` below compiles with `lake build`; no `sorry`; no user `axiom`
48 +except the *fields of the structure* `CyclicGroup`, which are the explicit axioms of the abstract
49 +group model in §5 and are listed there).
50 +
51 +**Not machine-checked / informal**: the historical attributions (Gilbert–Handschuh 2003,
52 +Chabaud–Joux 1998) and the probabilistic reading of the local collision (the 2⁻³⁹ estimate) are
53 +literature statements, not theorems; the Python simulations in `scripts/` are sanity checks, not
54 +proofs; the "SAT instance size" column is a measurement, not a proof artefact.
55 +
56 +Column "AIG" = number of AIG nodes of the bit-blasted instance given to CaDiCaL (from
57 +`scripts/aig_sizes.tsv`); "—" = no SAT call. Build time is per module (whole file).
58 +
59 +### 1. SHA-256 word functions — `Wp34/Sha256Words.lean` (2.5 s)
60 +
61 +Definitions transcribed from `src/wp34/sha256.py`: `rotr`, `Sigma0`, `Sigma1`, `sigma0`,
62 +`sigma1`, `Ch`, `Maj` on `Word := BitVec 32`.
63 +
64 +| Theorem | Informal statement | Method | AIG |
65 +|---|---|---|---|
66 +| `Ch_opt` | `Ch x y z = ((y ^ z) & x) ^ z` | `bv_decide` | 608 |
67 +| `Maj_opt_or` | `Maj x y z = (x & (y \| z)) \| (y & z)` | `bv_decide` | 608 |
68 +| `Maj_opt_xor` | `Maj x y z = ((x ^ y) & (y ^ z)) ^ y` | `bv_decide` | 768 |
69 +| `Maj_comm` | `Maj` symmetric in its arguments | `bv_decide` | 704 |
70 +| `Sigma0_xor`, `Sigma1_xor`, `sigma0_xor`, `sigma1_xor` | GF(2)-linearity `f (x ^ y) = f x ^ f y` | `bv_decide` | 960 / 960 / 933 / 870 |
71 +| `Sigma0_zero`, …, `sigma1_zero` | `f 0 = 0` | `bv_decide` (closed by preprocessing) | — |
72 +| `Sigma0_injective`, `Sigma1_injective` | `Function.Injective` | `bv_decide` | 704 / 704 |
73 +| **`sigma0_injective`, `sigma1_injective`** | σ0, σ1 are injective on 32 bits (UNSAT certificate) | `bv_decide` | 686 / 644 |
74 +| `shr3_not_injective` | control: `x >>> 3` is not injective (`0`, `1`) | `decide` | — |
75 +| `sigma0_rotations_not_injective` | control: `rotr x 7 ^ rotr x 18` kills `0xFFFFFFFF` | `decide` | — |
76 +| `sigma1_rotations_not_injective` | control: `rotr x 17 ^ rotr x 19` kills `0xFFFFFFFF` | `decide` | — |
77 +| `rotr_xor` | `rotr (x ^ y) k = rotr x k ^ rotr y k` for every `k` | bit-level `simp` (`getLsbD_rotateRight`) | — |
78 +| `bitMask_xor`, `xor_and_right` | helpers for the matrix-form inverses | `simp` / `bv_decide` | — / 512 |
79 +| `Sigma0Inv_xor`, `Sigma1Inv_xor`, `sigma0Inv_xor`, `sigma1Inv_xor` | the explicit inverses are GF(2)-linear | `simp [rotr_xor]` + `ac_rfl` | — |
80 +| `inv_of_quarters` | two linear maps whose composite fixes each byte compose to `id` | `bv_decide` (byte split) + rewriting | 33 |
81 +| `Sigma0Inv_Sigma0_b0..b3`, … (32 lemmas) | `g (f (x & byte_i)) = x & byte_i` for the 8 (map, inverse) pairs | `bv_decide` | 508 – 4817 |
82 +| `Sigma0Inv_Sigma0`, `Sigma0_Sigma0Inv`, `Sigma1Inv_Sigma1`, `Sigma1_Sigma1Inv` | Σ0⁻¹ = XOR of 17 rotations `{0,1,2,5,7,8,9,10,12,16,17,19,22,23,25,29,30}`, Σ1⁻¹ = XOR of 17 rotations `{2,3,5,7,9,11,12,13,18,21,22,23,24,26,27,29,30}` | `inv_of_quarters` | — |
83 +| `sigma0Inv_sigma0`, `sigma0_sigma0Inv`, `sigma1Inv_sigma1`, `sigma1_sigma1Inv` | σ0⁻¹, σ1⁻¹ given as explicit 32×32 GF(2) matrices (column tables) | `inv_of_quarters` | — |
84 +| **`Sigma0_bijective`, `Sigma1_bijective`, `sigma0_bijective`, `sigma1_bijective`** | injective ∧ surjective (`Bijective`, defined locally since core lacks `Function.Bijective`) | from the above | — |
85 +
86 +Result for item 1(e): **σ0 and σ1 are bijections of `BitVec 32`** — the logical shift does *not*
87 +make them singular (GF(2) rank 32, trivial kernel, `scripts/gf2_rank.py`), and `bv_decide` found no
88 +counter-example (UNSAT, certificate checked). The task brief expected them to be non-bijective;
89 +that expectation was wrong, and the controls show *why* the intuition fails: each of the two parts
90 +of σ0 (`x >>> 3` alone; `rotr 7 ^ rotr 18` alone, which has kernel `{0, 0xFFFFFFFF}` since
91 +`ρ⁷ + ρ¹⁸ = ρ⁷(1 + ρ¹¹)` is not a unit) is non-injective, but their sum is a unit.
92 +
93 +### 2. Step function bijection — `Wp34/Sha256Step.lean` (27 s)
94 +
95 +`State` = 8 registers; `step` / `stepInv` transcribed from `step` / `step_inverse` in `sha256.py`.
96 +
97 +| Theorem | Informal statement | Method | AIG |
98 +|---|---|---|---|
99 +| `add4_sub4` | `h + A + B + k + w − A − B − k − w = h` | `bv_decide` (120 s cap, ~2 s) | 2048 |
100 +| `sub4_add4` | `x − A − B − k − w + A + B + k + w = x` | `bv_decide` | 2420 |
101 +| **`stepInv_step`** | `stepInv (step s k w) k w = s` | `generalize` Σ0+Maj, Σ1, Ch; `simp [add_sub_cancel, add4_sub4]` | — |
102 +| **`step_stepInv`** | `step (stepInv n k w) k w = n` | `generalize`; `simp [sub4_add4, sub_add_cancel]` | — |
103 +| `step_injective`, `step_surjective`, **`step_bijective`** | the step is a bijection of the 256-bit state for fixed `(k, w)` | from the two above | — |
104 +| `stepsInv_steps`, `steps_stepsInv`, **`steps_bijective`** | any number of rounds with known `(K_t, W_t)` is a bijection | induction on the list | — |
105 +
106 +The direct 320-bit `bv_decide` call (`cases s; simp only [...]; bv_decide`) **timed out** (10 s
107 +default) and `bv_omega` on the abstracted goal did not terminate (killed after 35 CPU-minutes); the
108 +per-component split suggested in the brief is what is implemented, with the two addition-chain
109 +lemmas proven by SAT.
110 +
111 +### 3. Message schedule — `Wp34/Schedule.lean` (1.5 s)
112 +
113 +| Theorem | Informal statement | Method | AIG |
114 +|---|---|---|---|
115 +| **`scheduleStep_inv`** | `W_{t−16} = W_t − σ1(W_{t−2}) − W_{t−7} − σ0(W_{t−15})` | `bv_decide` | 1921 |
116 +| `scheduleStep_bijective` | `W_{t−16} ↦ W_t` is a bijection for fixed other inputs | `bv_decide` + rewriting | 2200 |
117 +| `schedule`, `schedule_lt`, `schedule_ge` | the expanded schedule as a well-founded recursion on `t` | definition / `simp` | — |
118 +| **`schedule_backward`** | for the real schedule, `W_{t−16}` is determined by `W_{t−15..t}` (hence the 64 words by any 16 consecutive ones) | rewriting | — |
119 +
120 +### 4. XOR-linearised 9-step local collision — `Wp34/LocalCollision.lean` (1.7 s)
121 +
122 +`linStep boolf` replaces every `+` of `step` by `^^^` and `Ch`, `Maj` by `boolf`. Two
123 +linearisations are formalised (the choice is stated explicitly in the file header):
124 +`absorb` (`Ch = Maj = 0`: the Boolean functions absorb all differences — the difference-domain
125 +reading of the Gilbert–Handschuh 2003 conditions) and `xor3` (`Ch = Maj = x ^ y ^ z`: the
126 +Chabaud–Joux 1998 linearisation). Σ0, Σ1 are kept exactly (they are linear).
127 +Correction vectors derived in the file header (the derivation is a comment, not a theorem):
128 +
129 +* `corrAbsorb δ = (δ, Σ1δ^Σ0δ, Σ1(Σ0δ), 0, δ, Σ0δ^Σ1δ, 0, 0, δ)`;
130 +* `corrXor3 δ = (δ, Σ1δ^Σ0δ, Σ1(Σ0δ^δ)^Σ0δ^δ, Σ1δ^Σ0δ, Σ1δ^Σ0δ, Σ0δ^Σ1δ, δ, 0, δ)`.
131 +
132 +Note: the brief's sketch "δW_{i+1} = Σ1(δ) through the e-register" is incomplete — at step i+1 the
133 +`a` register also carries δ and injects Σ0(δ) through T2, so δW_{i+1} must be Σ1(δ) ⊕ Σ0(δ);
134 +`bv_decide` confirms the vectors above and the simulation in `scripts/local_collision.py` agrees.
135 +
136 +| Theorem | Informal statement | Method | AIG |
137 +|---|---|---|---|
138 +| `State.xor_zero`, `Words9.xor_zero` | `s ⊕ 0 = s` | `bv_decide` | 257 / 289 |
139 +| `linStep_xor_absorb`, `linStep_xor_xor3` | one linearised step is a XOR-homomorphism in `(state, k, w)` | `bv_decide` | 4000 / 6368 |
140 +| `lin9_xor_absorb`, `lin9_xor_xor3` | nine linearised steps are a XOR-homomorphism | `simp` rewriting | — |
141 +| `corrAbsorb_xor`, `corrXor3_xor` | the correction vectors are linear in δ | `simp [Sigma0_xor, Sigma1_xor]` + `ac_rfl` | — |
142 +| `diff_absorb_b0..b3`, `diff_xor3_b0..b3` | difference-only statement with δ restricted to one byte | `bv_decide` | 5049 / 8769 |
143 +| `byte_split` | `δ = ⊕ of its four bytes` | `bv_decide` | 33 |
144 +| **`localCollision_absorb_diff`, `localCollision_xor3_diff`** | `lin9 f 0 0 (corr δ) = 0` for every δ (difference propagation) | linearity + bytes | — |
145 +| **`localCollision_absorb`, `localCollision_xor3`** | `lin9 f s K W = lin9 f s K (W ⊕ corr δ)` for **every** `s`, `K`, `W`, `δ` (864 bits universally quantified) | `lin9_xor_*` + `*_diff` | — |
146 +| **`absorb_corrections_not_real_collision`** | negative control: with the *real* modular step, zero state/constants/words and δ = 1, the two 9-step outputs differ | `decide` (kernel evaluation) | — |
147 +
148 +The full-value statements were first attempted directly with `bv_decide` and **timed out** (they
149 +are pure parity identities; see §Failures). They are proven exactly as the brief's fallback
150 +suggests — via the linear difference-propagation version — and then *lifted* to the full-value
151 +statement by the proven linearity of `lin9`, so the general statement is machine-checked after all.
152 +
153 +### 5. Abstract signature correctness — `Wp34/Group.lean` (0.3 s)
154 +
155 +`structure CyclicGroup (G : Type)` — **every field is an axiom of the model**:
156 +`zero`, `add`, `neg`, `nsmul : Nat → G → G`, `xcoord : G → Nat`, `gen`, `order`;
157 +`add_assoc`, `add_comm`, `zero_add`, `add_neg` (commutative group); `nsmul_zero`,
158 +`nsmul_succ` (recursive definition of `n•P`); `one_lt_order`, `nsmul_order : order•gen = 0`;
159 +`xcoord_neg : xcoord (−P) = xcoord P`. Nothing else is assumed; in particular there is **no**
160 +field about modular inverses, about `xcoord` being injective up to sign, or about `order` being
161 +prime. Modular arithmetic is done on `Nat` with core lemmas (`Nat.mul_mod`, `Nat.div_add_mod`, …).
162 +
163 +Derived (theorems, manual proofs): `add_zero`, `neg_add`, `eq_neg_of_add_eq_zero`, `neg_add_rev`,
164 +`nsmul_add` (`(m+k)•a = m•a + k•a`), `nsmul_zero_point`, `nsmul_mul` (`(m·k)•a = m•(k•a)`),
165 +`add_add_add_comm`, `nsmul_add_point`, `nsmul_neg`, `nsmul_one`, `InSubgroup`, `gen_inSubgroup`,
166 +`nsmul_inSubgroup`, `nsmul_order_of_mem` (`order•P = 0` on the subgroup), `nsmul_mod`
167 +(scalars act modulo `order`), `nsmul_congr_mod`, `nsmul_order_sub` (`(n−u)•P = −(u•P)`),
168 +`sub_mul_sub_mod` (`(n−s)(n−w) ≡ s·w (mod n)`).
169 +
170 +| Theorem | Informal statement | Method |
171 +|---|---|---|
172 +| **`schnorr_correct`** | `P = d•G`, `R = k•G`, `s = (k + e·d) mod n` ⟹ `s•G = R + e•P` (`schnorrVerify`) | `nsmul_mod`, `nsmul_add`, `nsmul_mul` |
173 +| `schnorr_correct_nat` | same with `s = k + e·d` unreduced | rewriting |
174 +| **`ecdsa_malleable`** | `ecdsaVerify P e r s → ecdsaVerify P e r (n − s)` for `P` in the subgroup; the verification point becomes its negative and `xcoord` is unchanged | manual (witness `w' = n − (w mod n)`, `sub_mul_sub_mod`, `neg_add_rev`, `xcoord_neg`) |
175 +| **`ecdsa_correct`** | `r = x(k•G) mod n`, `k·s ≡ e + r·d`, `s·w ≡ 1` ⟹ `ecdsaVerify (d•G) e r s` | manual (`Nat.mul_mod`, `ac_rfl`, `nsmul_congr_mod`) |
176 +
177 +`ecdsaVerify P e r s := 0 < r < n ∧ 0 < s < n ∧ ∃ w, s·w mod n = 1 ∧ xcoord((e·w)•G + (r·w)•P) mod n = r`.
178 +Scope: this is the *abstract* ECDSA/Schnorr verification equation in a cyclic group with an
179 +`xcoord` map; BIP340's x-only keys/even-y normalisation and the "low-s" rule are not modelled.
180 +
181 +## Failures and timeouts (honest record)
182 +
183 +| Attempt | Outcome | Resolution |
184 +|---|---|---|
185 +| `Sigma0Inv (Sigma0 x) = x` by direct `bv_decide` (32 free bits, 51 rotation terms per bit) | SAT timeout at 10 s; also at 240 s with `acNf := true` (256 s wall) | linearity (`rotr_xor` proven bit-wise) + byte split (`inv_of_quarters`) |
186 +| `Sigma0Inv_xor` by `bv_decide` | timeout at 60 s (34-literal parity) | `simp [rotr_xor]; ac_rfl` |
187 +| 16-bit halves instead of bytes | proven, but each LRAT certificate ≈ 100 MB in the `.olean` (`Sha256Words.olean` was 1.3 GB) | bytes: ≈ 0.6 MB each, file now 31 MB |
188 +| `stepInv_step` by direct 320-bit `bv_decide` | timeout at 10 s | abstraction of Σ0+Maj, Σ1, Ch + two 160-bit SAT lemmas + rewriting |
189 +| `stepInv_step` by `generalize` + `bv_omega` | did not terminate (killed after 35 CPU-min) | as above |
190 +| `localCollision_absorb` / `_xor3` (864-bit) by direct `bv_decide` | timeout at 10 s | linearity of `lin9` + difference version |
191 +| `localCollision_xor3_diff` (32-bit δ) by direct `bv_decide` | timeout at 10 s (`absorb` version passed) | byte split in δ |
192 +| `corrXor3_xor` by `bv_decide` | timeout at 10 s (24-literal parity) | `simp [Sigma0_xor, Sigma1_xor]; ac_rfl` |
193 +| Re-elaborating `LocalCollision.lean` with `trace.Meta.Tactic.bv` on | heartbeat timeouts in the trace printer for the last 4 theorems (no SAT there); normal build unaffected | reported as-is in `scripts/aig_sizes.py` |
194 +
195 +Lesson: CDCL SAT (CaDiCaL) is instantaneous on the mixed and/or/add goals of SHA-256 but
196 +systematically fails on pure-parity (XOR-chain) identities, which is where linear algebra over GF(2)
197 +— here encoded as rewriting with proven linearity lemmas — is the right tool.
198 +
199 +## Classification
200 +
201 +Every statement above is **`known_reproduced`**: the Ch/Maj identities, linearity and
202 +invertibility of the sigma functions, invertibility of the step function and of the message
203 +schedule, the linearised local collision, and Schnorr/ECDSA correctness and malleability are all
204 +standard facts. The contribution of this stage is their *machine-checked* status in Lean 4 core
205 +with a 32-second build, the explicit inverses of Σ0, Σ1, σ0, σ1, and the documentation of which
206 +formulations are and are not tractable for `bv_decide`.
207 +
208 +## References (informal; keys from `paper/references.bib` where they exist)
209 +
210 +* FIPS 180-4, Secure Hash Standard — `nist2015fips180` (definitions of Σ0, Σ1, σ0, σ1, Ch, Maj, the step and the schedule).
211 +* F. Chabaud, A. Joux, "Differential collisions in SHA-0", CRYPTO 1998 — `chabaud1998differential` (XOR-linearisation technique, `xor3` model).
212 +* H. Gilbert, H. Handschuh, "Security analysis of SHA-256 and sisters", SAC 2003 — the 9-step local collision and its absorbing conditions (`absorb` model). **No entry in `references.bib` yet** — to be added and verified at Stage 6; not cited in the Lean sources.
213 +* FIPS 186-5 (ECDSA) — `nist2023fips186`; BIP340 (Schnorr) — abstract model only.
added formal/Wp34.lean +7 −0
@@ -0,0 +1,7 @@
1 +-- WP34 (UQO) — Stage 5 formal verification, Lean 4 core only (no Mathlib).
2 +import Wp34.Sha256Words
3 +import Wp34.Sha256Step
4 +import Wp34.Schedule
5 +import Wp34.LocalCollision
6 +import Wp34.Group
7 +import Wp34.Audit
added formal/Wp34/Audit.lean +46 −0
@@ -0,0 +1,46 @@
1 +/-
2 + WP34 — axiom audit. `#print axioms` lists every axiom a theorem ultimately depends on; the
3 + output is part of `lake build`'s log (`formal/build.log`). Expected: only Lean-core axioms
4 + (`propext`, `Classical.choice`, `Quot.sound`) and, for `bv_decide` proofs,
5 + `Lean.ofReduceBool` (the kernel trusts the compiled LRAT certificate checker).
6 +-/
7 +import Wp34.Sha256Words
8 +import Wp34.Sha256Step
9 +import Wp34.Schedule
10 +import Wp34.LocalCollision
11 +import Wp34.Group
12 +
13 +namespace Wp34
14 +
15 +#print axioms Ch_opt
16 +#print axioms Maj_opt_or
17 +#print axioms Maj_opt_xor
18 +#print axioms Sigma0_xor
19 +#print axioms Sigma0_injective
20 +#print axioms sigma0_injective
21 +#print axioms sigma1_injective
22 +#print axioms rotr_xor
23 +#print axioms Sigma0Inv_xor
24 +#print axioms Sigma0Inv_Sigma0
25 +#print axioms sigma0_bijective
26 +#print axioms sigma1_bijective
27 +#print axioms shr3_not_injective
28 +#print axioms stepInv_step
29 +#print axioms step_stepInv
30 +#print axioms step_bijective
31 +#print axioms steps_bijective
32 +#print axioms add4_sub4
33 +#print axioms sub4_add4
34 +#print axioms scheduleStep_inv
35 +#print axioms schedule_backward
36 +#print axioms localCollision_absorb
37 +#print axioms localCollision_xor3
38 +#print axioms localCollision_absorb_diff
39 +#print axioms lin9_xor_absorb
40 +#print axioms linStep_xor_absorb
41 +#print axioms absorb_corrections_not_real_collision
42 +#print axioms CyclicGroup.schnorr_correct
43 +#print axioms CyclicGroup.ecdsa_malleable
44 +#print axioms CyclicGroup.ecdsa_correct
45 +
46 +end Wp34
added formal/Wp34/Group.lean +252 −0
@@ -0,0 +1,252 @@
1 +/-
2 + WP34 — Abstract signature correctness in an axiomatised cyclic group (no Mathlib).
3 +
4 + `CyclicGroup G` bundles an additively written commutative group with scalar multiplication by
5 + naturals, a distinguished generator `gen` of order `order`, and an `xcoord` map invariant under
6 + negation (the model of a prime-order elliptic-curve group such as secp256k1 with the
7 + x-coordinate projection). EVERY FIELD OF THE STRUCTURE IS AN AXIOM OF THE MODEL; everything
8 + stated with `theorem` below is derived from those fields and Lean-core arithmetic on `Nat`.
9 +-/
10 +
11 +namespace Wp34
12 +
13 +structure CyclicGroup (G : Type) where
14 + zero : G
15 + add : G → G → G
16 + neg : G → G
17 + nsmul : Nat → G → G
18 + xcoord : G → Nat
19 + gen : G
20 + order : Nat
21 + -- axioms: commutative group
22 + add_assoc : ∀ a b c, add (add a b) c = add a (add b c)
23 + add_comm : ∀ a b, add a b = add b a
24 + zero_add : ∀ a, add zero a = a
25 + add_neg : ∀ a, add a (neg a) = zero
26 + -- axioms: scalar multiplication by naturals (recursive definition, as fields)
27 + nsmul_zero : ∀ a, nsmul 0 a = zero
28 + nsmul_succ : ∀ k a, nsmul (k + 1) a = add (nsmul k a) a
29 + -- axioms: the generator has order `order` (> 1)
30 + one_lt_order : 1 < order
31 + nsmul_order : nsmul order gen = zero
32 + -- axiom: the x-coordinate does not see the sign of a point
33 + xcoord_neg : ∀ P, xcoord (neg P) = xcoord P
34 +
35 +namespace CyclicGroup
36 +
37 +variable {G : Type} (C : CyclicGroup G)
38 +
39 +/-! ### Derived group facts (theorems, not axioms) -/
40 +
41 +theorem add_zero (a : G) : C.add a C.zero = a := by
42 + rw [C.add_comm, C.zero_add]
43 +
44 +theorem neg_add (a : G) : C.add (C.neg a) a = C.zero := by
45 + rw [C.add_comm, C.add_neg]
46 +
47 +/-- Uniqueness of inverses: `a + b = 0 → a = −b`. -/
48 +theorem eq_neg_of_add_eq_zero {a b : G} (h : C.add a b = C.zero) : a = C.neg b := by
49 + calc a = C.add a C.zero := (C.add_zero a).symm
50 + _ = C.add a (C.add b (C.neg b)) := by rw [C.add_neg]
51 + _ = C.add (C.add a b) (C.neg b) := by rw [C.add_assoc]
52 + _ = C.add C.zero (C.neg b) := by rw [h]
53 + _ = C.neg b := C.zero_add _
54 +
55 +theorem neg_add_rev (a b : G) : C.neg (C.add a b) = C.add (C.neg a) (C.neg b) := by
56 + symm
57 + apply C.eq_neg_of_add_eq_zero
58 + calc C.add (C.add (C.neg a) (C.neg b)) (C.add a b)
59 + = C.add (C.neg a) (C.add (C.neg b) (C.add b a)) := by
60 + rw [C.add_assoc, C.add_comm a b]
61 + _ = C.add (C.neg a) (C.add (C.add (C.neg b) b) a) := by rw [C.add_assoc]
62 + _ = C.add (C.neg a) a := by rw [C.neg_add, C.zero_add]
63 + _ = C.zero := C.neg_add a
64 +
65 +theorem nsmul_add (m k : Nat) (a : G) : C.nsmul (m + k) a = C.add (C.nsmul m a) (C.nsmul k a) := by
66 + induction k with
67 + | zero => rw [Nat.add_zero, C.nsmul_zero, C.add_zero]
68 + | succ k ih => rw [← Nat.add_assoc, C.nsmul_succ, ih, C.nsmul_succ, C.add_assoc]
69 +
70 +theorem nsmul_zero_point (k : Nat) : C.nsmul k C.zero = C.zero := by
71 + induction k with
72 + | zero => exact C.nsmul_zero _
73 + | succ k ih => rw [C.nsmul_succ, ih, C.zero_add]
74 +
75 +theorem nsmul_mul (m k : Nat) (a : G) : C.nsmul (m * k) a = C.nsmul m (C.nsmul k a) := by
76 + induction m with
77 + | zero => rw [Nat.zero_mul, C.nsmul_zero, C.nsmul_zero]
78 + | succ m ih => rw [Nat.succ_mul, C.nsmul_add, ih, C.nsmul_succ]
79 +
80 +theorem add_add_add_comm (a b c d : G) :
81 + C.add (C.add a b) (C.add c d) = C.add (C.add a c) (C.add b d) := by
82 + calc C.add (C.add a b) (C.add c d) = C.add a (C.add b (C.add c d)) := C.add_assoc _ _ _
83 + _ = C.add a (C.add (C.add b c) d) := by rw [C.add_assoc b c d]
84 + _ = C.add a (C.add (C.add c b) d) := by rw [C.add_comm b c]
85 + _ = C.add a (C.add c (C.add b d)) := by rw [C.add_assoc c b d]
86 + _ = C.add (C.add a c) (C.add b d) := (C.add_assoc _ _ _).symm
87 +
88 +theorem nsmul_add_point (k : Nat) (a b : G) :
89 + C.nsmul k (C.add a b) = C.add (C.nsmul k a) (C.nsmul k b) := by
90 + induction k with
91 + | zero => rw [C.nsmul_zero, C.nsmul_zero, C.nsmul_zero, C.zero_add]
92 + | succ k ih => rw [C.nsmul_succ, C.nsmul_succ, C.nsmul_succ, ih, C.add_add_add_comm]
93 +
94 +theorem nsmul_neg (k : Nat) (a : G) : C.nsmul k (C.neg a) = C.neg (C.nsmul k a) := by
95 + apply C.eq_neg_of_add_eq_zero
96 + rw [← C.nsmul_add_point, C.neg_add, C.nsmul_zero_point]
97 +
98 +/-- Membership in the cyclic subgroup generated by `gen` (`P = d•G` for some `d`). -/
99 +def InSubgroup (P : G) : Prop := ∃ d, P = C.nsmul d C.gen
100 +
101 +theorem nsmul_one (a : G) : C.nsmul 1 a = a := by
102 + rw [show (1 : Nat) = 0 + 1 from rfl, C.nsmul_succ, C.nsmul_zero, C.zero_add]
103 +
104 +theorem gen_inSubgroup : C.InSubgroup C.gen := ⟨1, (C.nsmul_one _).symm⟩
105 +
106 +theorem nsmul_inSubgroup (k : Nat) (P : G) (hP : C.InSubgroup P) : C.InSubgroup (C.nsmul k P) := by
107 + obtain ⟨d, rfl⟩ := hP
108 + exact ⟨k * d, (C.nsmul_mul k d C.gen).symm⟩
109 +
110 +/-- Every point of the subgroup is killed by `order`. -/
111 +theorem nsmul_order_of_mem (P : G) (hP : C.InSubgroup P) : C.nsmul C.order P = C.zero := by
112 + obtain ⟨d, rfl⟩ := hP
113 + rw [← C.nsmul_mul, Nat.mul_comm, C.nsmul_mul, C.nsmul_order, C.nsmul_zero_point]
114 +
115 +/-- Scalars act modulo `order` on the subgroup. -/
116 +theorem nsmul_mod (k : Nat) (P : G) (hP : C.InSubgroup P) :
117 + C.nsmul (k % C.order) P = C.nsmul k P := by
118 + conv => rhs; rw [← Nat.div_add_mod k C.order]
119 + rw [C.nsmul_add, Nat.mul_comm C.order (k / C.order), C.nsmul_mul, C.nsmul_order_of_mem P hP,
120 + C.nsmul_zero_point, C.zero_add]
121 +
122 +theorem nsmul_congr_mod (k k' : Nat) (P : G) (hP : C.InSubgroup P) (h : k % C.order = k' % C.order) :
123 + C.nsmul k P = C.nsmul k' P := by
124 + rw [← C.nsmul_mod k P hP, h, C.nsmul_mod k' P hP]
125 +
126 +/-- `(order − u)•P = −(u•P)` for `u ≤ order` and `P` in the subgroup. -/
127 +theorem nsmul_order_sub (u : Nat) (hu : u ≤ C.order) (P : G) (hP : C.InSubgroup P) :
128 + C.nsmul (C.order - u) P = C.neg (C.nsmul u P) := by
129 + apply C.eq_neg_of_add_eq_zero
130 + rw [← C.nsmul_add, Nat.sub_add_cancel hu, C.nsmul_order_of_mem P hP]
131 +
132 +/-! ### Schnorr signatures (BIP340-style, abstract)
133 +
134 +Key pair `(d, P = d•G)`, nonce `k` with commitment `R = k•G`, challenge `e` (any natural — in
135 +BIP340 it is `H(R ‖ P ‖ m)`), response `s = (k + e·d) mod n`.
136 +Verification: `s•G = R + e•P`. -/
137 +
138 +/-- Schnorr verification equation. -/
139 +def schnorrVerify (P R : G) (e s : Nat) : Prop :=
140 + C.nsmul s C.gen = C.add R (C.nsmul e P)
141 +
142 +/-- **Schnorr correctness**: an honestly generated signature verifies. -/
143 +theorem schnorr_correct (d k e : Nat) :
144 + C.schnorrVerify (C.nsmul d C.gen) (C.nsmul k C.gen) e ((k + e * d) % C.order) := by
145 + unfold schnorrVerify
146 + rw [C.nsmul_mod _ _ C.gen_inSubgroup, C.nsmul_add, C.nsmul_mul]
147 +
148 +/-- Variant without the final reduction (`s = k + e·d` as a natural number). -/
149 +theorem schnorr_correct_nat (d k e : Nat) :
150 + C.schnorrVerify (C.nsmul d C.gen) (C.nsmul k C.gen) e (k + e * d) := by
151 + unfold schnorrVerify
152 + rw [C.nsmul_add, C.nsmul_mul]
153 +
154 +/-! ### ECDSA (abstract)
155 +
156 +Signature `(r, s)` on hash `e` under public key `P`. Verification computes `w = s⁻¹ mod n`,
157 +`u₁ = e·w`, `u₂ = r·w`, `R' = u₁•G + u₂•P` and checks `x(R') mod n = r`. The modular inverse is
158 +given as a witness `w` with `s·w ≡ 1 (mod n)`; scalars `e·w`, `r·w` need not be reduced because
159 +`nsmul_mod` shows scalars act modulo `n` on the subgroup. -/
160 +
161 +/-- ECDSA verification predicate (`0 < r, s < n` as in the standard). -/
162 +def ecdsaVerify (P : G) (e r s : Nat) : Prop :=
163 + 0 < r ∧ r < C.order ∧ 0 < s ∧ s < C.order ∧
164 + ∃ w, (s * w) % C.order = 1 ∧
165 + (C.xcoord (C.add (C.nsmul (e * w) C.gen) (C.nsmul (r * w) P))) % C.order = r
166 +
167 +/-- Helper: `(n − s)(n − w) ≡ s·w (mod n)` for `s, w ≤ n` (pure `Nat` arithmetic). -/
168 +theorem sub_mul_sub_mod (n s w : Nat) (hs : s ≤ n) (hw : w ≤ n) :
169 + ((n - s) * (n - w)) % n = (s * w) % n := by
170 + obtain ⟨p, hp⟩ := Nat.exists_eq_add_of_le hs
171 + obtain ⟨q, hq⟩ := Nat.exists_eq_add_of_le hw
172 + have h1 : n - s = p := by omega
173 + have h2 : n - w = q := by omega
174 + rw [h1, h2]
175 + -- p*q + n*(w + s) = s*w + n*n, hence p*q ≡ s*w (mod n)
176 + have key : p * q + n * (w + s) = s * w + n * n := by
177 + have e1 : n * n = n * w + s * q + p * q := by
178 + calc n * n = n * (w + q) := by rw [← hq]
179 + _ = n * w + n * q := Nat.mul_add _ _ _
180 + _ = n * w + (s + p) * q := by rw [← hp]
181 + _ = n * w + (s * q + p * q) := by rw [Nat.add_mul]
182 + _ = n * w + s * q + p * q := by rw [Nat.add_assoc]
183 + have e2 : n * (w + s) = n * w + s * q + s * w := by
184 + calc n * (w + s) = n * w + n * s := Nat.mul_add _ _ _
185 + _ = n * w + s * n := by rw [Nat.mul_comm n s]
186 + _ = n * w + s * (w + q) := by rw [← hq]
187 + _ = n * w + (s * w + s * q) := by rw [Nat.mul_add]
188 + _ = n * w + s * q + s * w := by omega
189 + rw [e1, e2]; omega
190 + have : (p * q + n * (w + s)) % n = (s * w + n * n) % n := by rw [key]
191 + rwa [Nat.add_mul_mod_self_left, Nat.add_mul_mod_self_left] at this
192 +
193 +/-- **ECDSA malleability**: if `(r, s)` verifies then so does `(r, n − s)`, using only the
194 +group axioms and `xcoord (−Q) = xcoord Q`. -/
195 +theorem ecdsa_malleable (P : G) (hP : C.InSubgroup P) (e r s : Nat)
196 + (h : C.ecdsaVerify P e r s) : C.ecdsaVerify P e r (C.order - s) := by
197 + obtain ⟨hr0, hrn, hs0, hsn, w, hw, hx⟩ := h
198 + have hn : 0 < C.order := Nat.lt_trans Nat.zero_lt_one C.one_lt_order
199 + have hG : C.InSubgroup C.gen := C.gen_inSubgroup
200 + -- reduce the inverse witness modulo n: w0 = w mod n, 0 < w0 < n, s·w0 ≡ 1
201 + obtain ⟨w0, hw0def⟩ : ∃ w0, w % C.order = w0 := ⟨_, rfl⟩
202 + have hw0n : w0 < C.order := hw0def ▸ Nat.mod_lt _ hn
203 + have hw0 : (s * w0) % C.order = 1 := by
204 + rw [Nat.mul_mod, ← hw0def, Nat.mod_mod, ← Nat.mul_mod, hw]
205 + have hw0pos : 0 < w0 := by
206 + rcases Nat.eq_zero_or_pos w0 with h0 | h0
207 + · rw [h0, Nat.mul_zero, Nat.zero_mod] at hw0; omega
208 + · exact h0
209 + -- (n − w0) + w is a multiple of n
210 + have hsum : C.order - w0 + w = C.order * (w / C.order + 1) := by
211 + have := Nat.div_add_mod w C.order
212 + rw [hw0def] at this
213 + rw [Nat.mul_add, Nat.mul_one]; omega
214 + -- u•(n − w0) + u•w = 0 for every u and every Q in the subgroup
215 + have hneg : ∀ (u : Nat) (Q : G), C.InSubgroup Q →
216 + C.nsmul (u * (C.order - w0)) Q = C.neg (C.nsmul (u * w) Q) := by
217 + intro u Q hQ
218 + apply C.eq_neg_of_add_eq_zero
219 + have h1 : u * (C.order - w0) + u * w = C.order * ((w / C.order + 1) * u) := by
220 + rw [← Nat.mul_add, hsum]; ac_rfl
221 + rw [← C.nsmul_add, h1, C.nsmul_mul, C.nsmul_order_of_mem _ (C.nsmul_inSubgroup _ _ hQ)]
222 + refine ⟨hr0, hrn, by omega, by omega, C.order - w0, ?_, ?_⟩
223 + · -- (n − s)(n − w0) ≡ s·w0 ≡ 1
224 + rw [sub_mul_sub_mod _ _ _ (Nat.le_of_lt hsn) (Nat.le_of_lt hw0n), hw0]
225 + · -- the verification point is negated, so its x-coordinate is unchanged
226 + rw [hneg e C.gen hG, hneg r P hP, ← C.neg_add_rev, C.xcoord_neg]
227 + exact hx
228 +
229 +/-- **ECDSA correctness**: an honestly generated signature verifies. Hypotheses encode
230 +`r = x(k•G) mod n` and `s ≡ k⁻¹(e + r·d)`, i.e. `k·s ≡ e + r·d (mod n)`, with a witness `w` for
231 +`s⁻¹`. -/
232 +theorem ecdsa_correct (d k e r s w : Nat)
233 + (hr0 : 0 < r) (hrn : r < C.order) (hs0 : 0 < s) (hsn : s < C.order)
234 + (hr : (C.xcoord (C.nsmul k C.gen)) % C.order = r)
235 + (hs : (k * s) % C.order = (e + r * d) % C.order)
236 + (hw : (s * w) % C.order = 1) :
237 + C.ecdsaVerify (C.nsmul d C.gen) e r s := by
238 + refine ⟨hr0, hrn, hs0, hsn, w, hw, ?_⟩
239 + have hG : C.InSubgroup C.gen := C.gen_inSubgroup
240 + have hpt : C.add (C.nsmul (e * w) C.gen) (C.nsmul (r * w) (C.nsmul d C.gen)) =
241 + C.nsmul k C.gen := by
242 + rw [← C.nsmul_mul, ← C.nsmul_add]
243 + apply C.nsmul_congr_mod _ _ _ hG
244 + -- e·w + r·w·d = w·(e + r·d) ≡ w·k·s = k·(s·w) ≡ k
245 + have e1 : e * w + r * w * d = w * (e + r * d) := by rw [Nat.mul_add]; ac_rfl
246 + have e2 : w * (k * s) = k * (s * w) := by ac_rfl
247 + rw [e1, Nat.mul_mod, ← hs, ← Nat.mul_mod, e2, Nat.mul_mod, hw, Nat.mul_one, Nat.mod_mod]
248 + rw [hpt, hr]
249 +
250 +end CyclicGroup
251 +
252 +end Wp34
added formal/Wp34/LocalCollision.lean +262 −0
@@ -0,0 +1,262 @@
1 +/-
2 + WP34 — XOR-linearised SHA-256 and the 9-step local collision
3 + (Chabaud–Joux 1998 linearisation technique; Gilbert–Handschuh 2003 local collision for SHA-256).
4 +
5 + Linearised model. Every modular addition of the step function is replaced by XOR; Σ0 and Σ1
6 + are already GF(2)-linear (`Sigma0_xor`, `Sigma1_xor`). The two Boolean functions are replaced
7 + by a GF(2)-linear substitute `boolf`; we prove the local collision for two standard choices:
8 +
9 + * `absorb` : Ch, Maj ↦ 0. This is the difference-domain reading of Gilbert–Handschuh 2003
10 + (SAC 2003, "Security analysis of SHA-256 and sisters", §4): their 9-step local collision is
11 + computed under the conditions that the Boolean functions *absorb* every incoming difference
12 + (a difference in exactly one input of Ch/Maj is cancelled when the other two inputs are
13 + equal on the active bits / when the relevant bit of the selector has the right value); the
14 + corrections then only have to cancel the Σ0 / Σ1 images, which are linear. In a linear model
15 + "Ch and Maj absorb all differences" is exactly "Ch = Maj = 0".
16 + * `xor3` : Ch, Maj ↦ x ^ y ^ z. This is the Chabaud–Joux 1998 (CRYPTO) linearisation used
17 + for SHA-0, where the IF / MAJ functions are replaced by the XOR of their inputs. The
18 + corrections differ (derived below) but a 9-step local collision still exists.
19 +
20 + Derivation (model `absorb`; δ is the perturbation, ΔW the correction injected in W):
21 + step i : ΔW = δ → Δa = δ, Δe = δ
22 + step i+1 : a has δ ⇒ ΔT2 = Σ0(δ); e has δ ⇒ Σ1(δ) in ΔT1.
23 + Choose ΔW = Σ1(δ) ^ Σ0(δ) so that ΔT1 = Σ0(δ) = ΔT2 ⇒ Δa_new = 0, Δe_new = Σ0(δ)
24 + (the Σ1(δ) term alone — "through the e-register" — would leave Σ0(δ) in `a`).
25 + step i+2 : e has Σ0(δ) ⇒ ΔW = Σ1(Σ0(δ)) cancels ΔT1; registers: c = δ, f = Σ0(δ), g = δ
26 + step i+3 : ΔW = 0; registers: d = δ, g = Σ0(δ), h = δ
27 + step i+4 : h has δ ⇒ ΔW = δ (ΔT1 = 0); d has δ ⇒ Δe_new = δ. registers: e = δ, h = Σ0(δ)
28 + step i+5 : h = Σ0(δ), e = δ ⇒ ΔW = Σ0(δ) ^ Σ1(δ); registers: f = δ
29 + step i+6 : ΔW = 0; registers: g = δ
30 + step i+7 : ΔW = 0; registers: h = δ
31 + step i+8 : ΔW = δ ⇒ all registers zero.
32 + Correction vector (absorb): (δ, Σ1δ^Σ0δ, Σ1(Σ0δ), 0, δ, Σ0δ^Σ1δ, 0, 0, δ).
33 + Correction vector (xor3) : (δ, Σ1δ^Σ0δ, Σ1(Σ0δ^δ)^Σ0δ^δ, Σ1δ^Σ0δ, Σ1δ^Σ0δ, Σ0δ^Σ1δ, δ, 0, δ)
34 + (same bookkeeping with ΔCh = ΔMaj = XOR of the register differences).
35 +
36 + Both vectors were first checked by simulation (`scripts/local_collision.py`); the theorems
37 + below are the universal statements, machine-checked by `bv_decide` for every state, every
38 + round-constant vector, every message-word vector and every δ.
39 +-/
40 +import Wp34.Sha256Step
41 +
42 +namespace Wp34
43 +
44 +/-- Nine consecutive words (round constants `K_i..K_{i+8}` or message words `W_i..W_{i+8}`). -/
45 +structure Words9 where
46 + w0 : Word
47 + w1 : Word
48 + w2 : Word
49 + w3 : Word
50 + w4 : Word
51 + w5 : Word
52 + w6 : Word
53 + w7 : Word
54 + w8 : Word
55 + deriving DecidableEq, Repr
56 +
57 +/-- Component-wise XOR (injecting a difference vector into the message words). -/
58 +def Words9.xor (u v : Words9) : Words9 :=
59 + ⟨u.w0 ^^^ v.w0, u.w1 ^^^ v.w1, u.w2 ^^^ v.w2, u.w3 ^^^ v.w3, u.w4 ^^^ v.w4,
60 + u.w5 ^^^ v.w5, u.w6 ^^^ v.w6, u.w7 ^^^ v.w7, u.w8 ^^^ v.w8⟩
61 +
62 +/-- GF(2)-linearised step: every `+` of `step` becomes `^^^`, `Ch`/`Maj` become `boolf`. -/
63 +def linStep (boolf : Word → Word → Word → Word) (s : State) (k w : Word) : State :=
64 + let T1 := s.h ^^^ Sigma1 s.e ^^^ boolf s.e s.f s.g ^^^ k ^^^ w
65 + let T2 := Sigma0 s.a ^^^ boolf s.a s.b s.c
66 + { a := T1 ^^^ T2, b := s.a, c := s.b, d := s.c, e := s.d ^^^ T1, f := s.e, g := s.f, h := s.g }
67 +
68 +/-- Linearisation "the Boolean functions absorb every difference" (Gilbert–Handschuh 2003). -/
69 +def absorb (_x _y _z : Word) : Word := 0
70 +/-- Linearisation "Boolean function ↦ XOR of its inputs" (Chabaud–Joux 1998). -/
71 +def xor3 (x y z : Word) : Word := x ^^^ y ^^^ z
72 +
73 +/-- Nine linearised steps `i, i+1, …, i+8`. -/
74 +def lin9 (boolf : Word → Word → Word → Word) (s : State) (K W : Words9) : State :=
75 + linStep boolf (linStep boolf (linStep boolf (linStep boolf (linStep boolf (linStep boolf
76 + (linStep boolf (linStep boolf (linStep boolf s K.w0 W.w0) K.w1 W.w1) K.w2 W.w2) K.w3 W.w3)
77 + K.w4 W.w4) K.w5 W.w5) K.w6 W.w6) K.w7 W.w7) K.w8 W.w8
78 +
79 +/-- Nine real (modular, non-linear) SHA-256 steps, for the negative control below. -/
80 +def real9 (s : State) (K W : Words9) : State :=
81 + step (step (step (step (step (step (step (step (step s K.w0 W.w0) K.w1 W.w1) K.w2 W.w2)
82 + K.w3 W.w3) K.w4 W.w4) K.w5 W.w5) K.w6 W.w6) K.w7 W.w7) K.w8 W.w8
83 +
84 +/-- Correction vector of the 9-step local collision in the `absorb` model. -/
85 +def corrAbsorb (δ : Word) : Words9 :=
86 + ⟨δ, Sigma1 δ ^^^ Sigma0 δ, Sigma1 (Sigma0 δ), 0, δ, Sigma0 δ ^^^ Sigma1 δ, 0, 0, δ⟩
87 +
88 +/-- Correction vector of the 9-step local collision in the `xor3` model. -/
89 +def corrXor3 (δ : Word) : Words9 :=
90 + ⟨δ, Sigma1 δ ^^^ Sigma0 δ, Sigma1 (Sigma0 δ ^^^ δ) ^^^ Sigma0 δ ^^^ δ,
91 + Sigma1 δ ^^^ Sigma0 δ, Sigma1 δ ^^^ Sigma0 δ, Sigma0 δ ^^^ Sigma1 δ, δ, 0, δ⟩
92 +
93 +/-! #### Proof strategy
94 +
95 +The 9-step statements are pure parity (XOR) identities: a direct `bv_decide` call times out
96 +(CDCL solvers do not reason about XOR chains; see README). Instead we exploit linearity, exactly
97 +as the Chabaud–Joux method does on paper:
98 +1. `linStep_xor_*` — one linearised step is a XOR-homomorphism in `(state, k, w)` (SAT, small);
99 +2. `lin9_xor_*` — hence nine steps are (rewriting, no SAT);
100 +3. `corr*_xor` — the correction vector is linear in δ (SAT, small);
101 +4. `diff_*_b0..b3` — the difference-only statement for δ restricted to each byte (SAT with 8 free
102 + variables);
103 +5. `localCollision_*_diff` — the difference-only statement for every δ, by 3 + 4;
104 +6. `localCollision_*` — the full-value statement, by 2 + 5.
105 +-/
106 +
107 +/-- Component-wise XOR of states. -/
108 +def State.xor (s t : State) : State :=
109 + ⟨s.a ^^^ t.a, s.b ^^^ t.b, s.c ^^^ t.c, s.d ^^^ t.d, s.e ^^^ t.e, s.f ^^^ t.f, s.g ^^^ t.g,
110 + s.h ^^^ t.h⟩
111 +/-- The all-zero state. -/
112 +def State.zero : State := ⟨0, 0, 0, 0, 0, 0, 0, 0⟩
113 +/-- The all-zero word vector. -/
114 +def Words9.zero : Words9 := ⟨0, 0, 0, 0, 0, 0, 0, 0, 0⟩
115 +
116 +theorem State.xor_zero (s : State) : s.xor State.zero = s := by
117 + cases s; simp only [State.xor, State.zero, State.mk.injEq]; bv_decide
118 +theorem Words9.xor_zero (K : Words9) : K.xor Words9.zero = K := by
119 + cases K; simp only [Words9.xor, Words9.zero, Words9.mk.injEq]; bv_decide
120 +
121 +/-- One linearised step (`absorb` model) is a XOR-homomorphism in `(state, k, w)`. -/
122 +theorem linStep_xor_absorb (s t : State) (k k' w w' : Word) :
123 + linStep absorb (s.xor t) (k ^^^ k') (w ^^^ w') =
124 + (linStep absorb s k w).xor (linStep absorb t k' w') := by
125 + cases s; cases t
126 + simp only [linStep, absorb, State.xor, State.mk.injEq, Sigma0, Sigma1, rotr]
127 + bv_decide
128 +
129 +/-- One linearised step (`xor3` model) is a XOR-homomorphism in `(state, k, w)`. -/
130 +theorem linStep_xor_xor3 (s t : State) (k k' w w' : Word) :
131 + linStep xor3 (s.xor t) (k ^^^ k') (w ^^^ w') =
132 + (linStep xor3 s k w).xor (linStep xor3 t k' w') := by
133 + cases s; cases t
134 + simp only [linStep, xor3, State.xor, State.mk.injEq, Sigma0, Sigma1, rotr]
135 + bv_decide
136 +
137 +/-- Nine linearised steps are a XOR-homomorphism (pure rewriting with `linStep_xor_absorb`). -/
138 +theorem lin9_xor_absorb (s t : State) (K K' W W' : Words9) :
139 + lin9 absorb (s.xor t) (K.xor K') (W.xor W') =
140 + (lin9 absorb s K W).xor (lin9 absorb t K' W') := by
141 + cases K; cases K'; cases W; cases W'
142 + simp only [lin9, Words9.xor, linStep_xor_absorb]
143 +
144 +theorem lin9_xor_xor3 (s t : State) (K K' W W' : Words9) :
145 + lin9 xor3 (s.xor t) (K.xor K') (W.xor W') =
146 + (lin9 xor3 s K W).xor (lin9 xor3 t K' W') := by
147 + cases K; cases K'; cases W; cases W'
148 + simp only [lin9, Words9.xor, linStep_xor_xor3]
149 +
150 +/-- The correction vectors are GF(2)-linear in δ (rewriting with `Sigma0_xor`/`Sigma1_xor`
151 +then associativity–commutativity of XOR; no SAT — the `xor3` instance is a 24-literal parity
152 +problem on which `bv_decide` times out). -/
153 +theorem corrAbsorb_xor (x y : Word) : corrAbsorb (x ^^^ y) = (corrAbsorb x).xor (corrAbsorb y) := by
154 + simp only [corrAbsorb, Words9.xor, Words9.mk.injEq, Sigma0_xor, Sigma1_xor]
155 + repeat' apply And.intro
156 + all_goals (first | trivial | ac_rfl)
157 +theorem corrXor3_xor (x y : Word) : corrXor3 (x ^^^ y) = (corrXor3 x).xor (corrXor3 y) := by
158 + simp only [corrXor3, Words9.xor, Words9.mk.injEq, Sigma0_xor, Sigma1_xor]
159 + repeat' apply And.intro
160 + all_goals (first | trivial | ac_rfl)
161 +
162 +/-- The difference map `δ ↦ lin9 f 0 0 (corr δ)` is linear in δ. -/
163 +theorem diff_xor_absorb (x y : Word) :
164 + lin9 absorb State.zero Words9.zero (corrAbsorb (x ^^^ y)) =
165 + (lin9 absorb State.zero Words9.zero (corrAbsorb x)).xor
166 + (lin9 absorb State.zero Words9.zero (corrAbsorb y)) := by
167 + rw [corrAbsorb_xor, ← lin9_xor_absorb, State.xor_zero, Words9.xor_zero]
168 +theorem diff_xor_xor3 (x y : Word) :
169 + lin9 xor3 State.zero Words9.zero (corrXor3 (x ^^^ y)) =
170 + (lin9 xor3 State.zero Words9.zero (corrXor3 x)).xor
171 + (lin9 xor3 State.zero Words9.zero (corrXor3 y)) := by
172 + rw [corrXor3_xor, ← lin9_xor_xor3, State.xor_zero, Words9.xor_zero]
173 +
174 +-- Difference-only statement for δ restricted to one byte (8 free variables each).
175 +theorem diff_absorb_b0 (δ : Word) :
176 + lin9 absorb State.zero Words9.zero (corrAbsorb (δ &&& 0x000000FF#32)) = State.zero := by
177 + simp only [lin9, linStep, absorb, corrAbsorb, State.zero, Words9.zero, State.mk.injEq,
178 + Sigma0, Sigma1, rotr]
179 + bv_decide
180 +theorem diff_absorb_b1 (δ : Word) :
181 + lin9 absorb State.zero Words9.zero (corrAbsorb (δ &&& 0x0000FF00#32)) = State.zero := by
182 + simp only [lin9, linStep, absorb, corrAbsorb, State.zero, Words9.zero, State.mk.injEq,
183 + Sigma0, Sigma1, rotr]
184 + bv_decide
185 +theorem diff_absorb_b2 (δ : Word) :
186 + lin9 absorb State.zero Words9.zero (corrAbsorb (δ &&& 0x00FF0000#32)) = State.zero := by
187 + simp only [lin9, linStep, absorb, corrAbsorb, State.zero, Words9.zero, State.mk.injEq,
188 + Sigma0, Sigma1, rotr]
189 + bv_decide
190 +theorem diff_absorb_b3 (δ : Word) :
191 + lin9 absorb State.zero Words9.zero (corrAbsorb (δ &&& 0xFF000000#32)) = State.zero := by
192 + simp only [lin9, linStep, absorb, corrAbsorb, State.zero, Words9.zero, State.mk.injEq,
193 + Sigma0, Sigma1, rotr]
194 + bv_decide
195 +theorem diff_xor3_b0 (δ : Word) :
196 + lin9 xor3 State.zero Words9.zero (corrXor3 (δ &&& 0x000000FF#32)) = State.zero := by
197 + simp only [lin9, linStep, xor3, corrXor3, State.zero, Words9.zero, State.mk.injEq,
198 + Sigma0, Sigma1, rotr]
199 + bv_decide
200 +theorem diff_xor3_b1 (δ : Word) :
201 + lin9 xor3 State.zero Words9.zero (corrXor3 (δ &&& 0x0000FF00#32)) = State.zero := by
202 + simp only [lin9, linStep, xor3, corrXor3, State.zero, Words9.zero, State.mk.injEq,
203 + Sigma0, Sigma1, rotr]
204 + bv_decide
205 +theorem diff_xor3_b2 (δ : Word) :
206 + lin9 xor3 State.zero Words9.zero (corrXor3 (δ &&& 0x00FF0000#32)) = State.zero := by
207 + simp only [lin9, linStep, xor3, corrXor3, State.zero, Words9.zero, State.mk.injEq,
208 + Sigma0, Sigma1, rotr]
209 + bv_decide
210 +theorem diff_xor3_b3 (δ : Word) :
211 + lin9 xor3 State.zero Words9.zero (corrXor3 (δ &&& 0xFF000000#32)) = State.zero := by
212 + simp only [lin9, linStep, xor3, corrXor3, State.zero, Words9.zero, State.mk.injEq,
213 + Sigma0, Sigma1, rotr]
214 + bv_decide
215 +
216 +theorem byte_split (δ : Word) :
217 + δ = (δ &&& 0x000000FF#32) ^^^ (δ &&& 0x0000FF00#32) ^^^ (δ &&& 0x00FF0000#32)
218 + ^^^ (δ &&& 0xFF000000#32) := by bv_decide
219 +
220 +/-- **9-step local collision, `absorb` model, difference-only statement**: injecting the
221 +correction vector into the zero state with zero constants yields the zero difference, for every δ. -/
222 +theorem localCollision_absorb_diff (δ : Word) :
223 + lin9 absorb State.zero Words9.zero (corrAbsorb δ) = State.zero := by
224 + rw [byte_split δ, diff_xor_absorb, diff_xor_absorb, diff_xor_absorb,
225 + diff_absorb_b0, diff_absorb_b1, diff_absorb_b2, diff_absorb_b3, State.xor_zero,
226 + State.xor_zero, State.xor_zero]
227 +
228 +/-- **9-step local collision, `xor3` (Chabaud–Joux) model, difference-only statement.** -/
229 +theorem localCollision_xor3_diff (δ : Word) :
230 + lin9 xor3 State.zero Words9.zero (corrXor3 δ) = State.zero := by
231 + rw [byte_split δ, diff_xor_xor3, diff_xor_xor3, diff_xor_xor3,
232 + diff_xor3_b0, diff_xor3_b1, diff_xor3_b2, diff_xor3_b3, State.xor_zero,
233 + State.xor_zero, State.xor_zero]
234 +
235 +/-- **9-step local collision, `absorb` model, full-value statement**: for every state `s`,
236 +every round-constant vector `K`, every message-word vector `W` and every difference `δ`, the
237 +linearised state after nine steps is unchanged when `W` is replaced by `W ⊕ corrAbsorb δ`
238 +(an 8·32 + 9·32 + 9·32 + 32 = 864-bit universally quantified statement). -/
239 +theorem localCollision_absorb (s : State) (K W : Words9) (δ : Word) :
240 + lin9 absorb s K W = lin9 absorb s K (W.xor (corrAbsorb δ)) := by
241 + have h := lin9_xor_absorb s State.zero K Words9.zero W (corrAbsorb δ)
242 + rw [State.xor_zero, Words9.xor_zero, localCollision_absorb_diff, State.xor_zero] at h
243 + exact h.symm
244 +
245 +/-- **9-step local collision, `xor3` (Chabaud–Joux) model, full-value statement.** -/
246 +theorem localCollision_xor3 (s : State) (K W : Words9) (δ : Word) :
247 + lin9 xor3 s K W = lin9 xor3 s K (W.xor (corrXor3 δ)) := by
248 + have h := lin9_xor_xor3 s State.zero K Words9.zero W (corrXor3 δ)
249 + rw [State.xor_zero, Words9.xor_zero, localCollision_xor3_diff, State.xor_zero] at h
250 + exact h.symm
251 +
252 +/-- **Negative control (machine-checked counter-example).** The linearised local collision is
253 +NOT a collision of the real step function: with the zero state, zero constants, zero words and
254 +δ = 1, the real 9-step outputs differ (the XOR characteristic holds on the modular function only
255 +with some probability — Gilbert–Handschuh estimate 2⁻³⁹ for a one-bit δ with modular
256 +corrections; `scripts/local_collision.py` observes 0/20000 with XOR corrections). -/
257 +theorem absorb_corrections_not_real_collision :
258 + real9 State.zero Words9.zero Words9.zero ≠
259 + real9 State.zero Words9.zero (Words9.zero.xor (corrAbsorb 1)) := by
260 + decide
261 +
262 +end Wp34
added formal/Wp34/Schedule.lean +55 −0
@@ -0,0 +1,55 @@
1 +/-
2 + WP34 — SHA-256 message schedule (FIPS 180-4 §6.2.2 step 1):
3 + W_t = σ1(W_{t−2}) + W_{t−7} + σ0(W_{t−15}) + W_{t−16} for 16 ≤ t < 64.
4 + Each schedule step is invertible in its oldest input: W_{t−16} is determined by W_{t−15..t}.
5 +-/
6 +import Wp34.Sha256Words
7 +
8 +namespace Wp34
9 +
10 +/-- One schedule step, argument order `(W_{t−16}, W_{t−15}, W_{t−7}, W_{t−2})`
11 +(`message_schedule` in the Python reference). -/
12 +def scheduleStep (w16 w15 w7 w2 : Word) : Word := sigma1 w2 + w7 + sigma0 w15 + w16
13 +
14 +/-- `W_{t−16} = W_t − σ1(W_{t−2}) − W_{t−7} − σ0(W_{t−15})`. -/
15 +theorem scheduleStep_inv (w16 w15 w7 w2 : Word) :
16 + scheduleStep w16 w15 w7 w2 - sigma1 w2 - w7 - sigma0 w15 = w16 := by
17 + unfold scheduleStep sigma0 sigma1 rotr; bv_decide
18 +
19 +/-- For fixed `(W_{t−15}, W_{t−7}, W_{t−2})`, the map `W_{t−16} ↦ W_t` is a bijection. -/
20 +theorem scheduleStep_bijective (w15 w7 w2 : Word) :
21 + Bijective (fun w16 => scheduleStep w16 w15 w7 w2) :=
22 + ⟨fun x y h => by
23 + have hx := scheduleStep_inv x w15 w7 w2
24 + have hy := scheduleStep_inv y w15 w7 w2
25 + simp only at h
26 + rw [h] at hx; exact hx.symm.trans hy,
27 + fun wt => ⟨wt - sigma1 w2 - w7 - sigma0 w15, by
28 + unfold scheduleStep sigma0 sigma1 rotr; bv_decide⟩⟩
29 +
30 +/-- The expanded message schedule as a function of the 16 message words `M`
31 +(well-founded recursion on `t`). -/
32 +def schedule (M : Fin 16 → Word) : Nat → Word
33 + | t =>
34 + if h : t < 16 then M ⟨t, h⟩
35 + else scheduleStep (schedule M (t - 16)) (schedule M (t - 15)) (schedule M (t - 7))
36 + (schedule M (t - 2))
37 + decreasing_by all_goals omega
38 +
39 +theorem schedule_lt (M : Fin 16 → Word) (t : Nat) (h : t < 16) : schedule M t = M ⟨t, h⟩ := by
40 + rw [schedule]; simp [h]
41 +
42 +theorem schedule_ge (M : Fin 16 → Word) (t : Nat) (h : 16 ≤ t) :
43 + schedule M t = scheduleStep (schedule M (t - 16)) (schedule M (t - 15))
44 + (schedule M (t - 7)) (schedule M (t - 2)) := by
45 + rw [schedule]; simp [Nat.not_lt.mpr h]
46 +
47 +/-- Backward schedule: `W_{t−16}` is determined by `(W_{t−15}, W_{t−7}, W_{t−2}, W_t)`.
48 +Consequently the 64-word expanded schedule is determined by ANY 16 consecutive words. -/
49 +theorem schedule_backward (M : Fin 16 → Word) (t : Nat) (h : 16 ≤ t) :
50 + schedule M (t - 16) =
51 + schedule M t - sigma1 (schedule M (t - 2)) - schedule M (t - 7)
52 + - sigma0 (schedule M (t - 15)) := by
53 + rw [schedule_ge M t h, scheduleStep_inv]
54 +
55 +end Wp34
added formal/Wp34/Sha256Step.lean +122 −0
@@ -0,0 +1,122 @@
1 +/-
2 + WP34 — the SHA-256 step function is a bijection of the 256-bit state for fixed (K_t, W_t).
3 + `step` and `stepInv` are transcribed from `step` / `step_inverse` in `src/wp34/sha256.py`.
4 +-/
5 +import Wp34.Sha256Words
6 +
7 +namespace Wp34
8 +
9 +/-- The eight 32-bit working registers `(a, b, c, d, e, f, g, h)`. -/
10 +structure State where
11 + a : Word
12 + b : Word
13 + c : Word
14 + d : Word
15 + e : Word
16 + f : Word
17 + g : Word
18 + h : Word
19 + deriving DecidableEq, Repr
20 +
21 +/-- One SHA-256 step (FIPS 180-4 §6.2.2 step 3) with round constant `k` and message word `w`. -/
22 +def step (s : State) (k w : Word) : State :=
23 + let T1 := s.h + Sigma1 s.e + Ch s.e s.f s.g + k + w
24 + let T2 := Sigma0 s.a + Maj s.a s.b s.c
25 + { a := T1 + T2, b := s.a, c := s.b, d := s.c, e := s.d + T1, f := s.e, g := s.f, h := s.g }
26 +
27 +/-- Inverse step (`step_inverse` in the Python reference): the new state is
28 +`(a₁, a, b, c, e₁, e, f, g)`; recover `T2` from `(a, b, c)`, then `T1 = a₁ − T2`, `d = e₁ − T1`,
29 +`h = T1 − Σ1(e) − Ch(e,f,g) − k − w`. -/
30 +def stepInv (n : State) (k w : Word) : State :=
31 + let T2 := Sigma0 n.b + Maj n.b n.c n.d
32 + let T1 := n.a - T2
33 + { a := n.b, b := n.c, c := n.d, d := n.e - T1,
34 + e := n.f, f := n.g, g := n.h, h := T1 - Sigma1 n.f - Ch n.f n.g n.h - k - w }
35 +
36 +/-! #### Proof strategy
37 +
38 +The direct `bv_decide` call on the 320-bit goal exceeds the SAT timeout (see README): the
39 +instance mixes the non-linear Σ/Ch/Maj sub-terms with the re-association of five chained 32-bit
40 +additions. The statement is *linear* in the sub-terms `T2 = Σ0(a) + Maj(a,b,c)`, `Σ1(e)` and
41 +`Ch(e,f,g)`, so we abstract them with `generalize`; the two surviving components are then closed
42 +by two small SAT lemmas on the pure addition chains (`add4_sub4`, `sub4_add4`, 160-bit
43 +instances, ~1.5 s each) and core's `BitVec.add_sub_cancel` / `sub_add_cancel`. The other six
44 +components are definitional. -/
45 +
46 +/-- `h + A + B + k + w − A − B − k − w = h` (SAT). -/
47 +theorem add4_sub4 (h A B k w : Word) : h + A + B + k + w - A - B - k - w = h := by
48 + bv_decide (config := { timeout := 120 })
49 +
50 +/-- `x − A − B − k − w + A + B + k + w = x` (SAT). -/
51 +theorem sub4_add4 (x A B k w : Word) : x - A - B - k - w + A + B + k + w = x := by
52 + bv_decide (config := { timeout := 120 })
53 +
54 +/-- `stepInv` is a left inverse of `step`: every SHA-256 step can be undone given `(k, w)`. -/
55 +theorem stepInv_step (s : State) (k w : Word) : stepInv (step s k w) k w = s := by
56 + obtain ⟨a, b, c, d, e, f, g, h⟩ := s
57 + simp only [step, stepInv, State.mk.injEq]
58 + generalize Sigma0 a + Maj a b c = T2
59 + generalize Sigma1 e = A
60 + generalize Ch e f g = B
61 + simp only [BitVec.add_sub_cancel, add4_sub4, and_self]
62 +
63 +/-- `stepInv` is a right inverse of `step`. -/
64 +theorem step_stepInv (n : State) (k w : Word) : step (stepInv n k w) k w = n := by
65 + obtain ⟨a₁, a, b, c, e₁, e, f, g⟩ := n
66 + simp only [step, stepInv, State.mk.injEq]
67 + generalize Sigma0 a + Maj a b c = T2
68 + generalize Sigma1 e = A
69 + generalize Ch e f g = B
70 + simp only [sub4_add4, BitVec.sub_add_cancel, and_self]
71 +
72 +/-- For fixed `(k, w)` the step function is injective on the state. -/
73 +theorem step_injective (k w : Word) : Function.Injective (fun s => step s k w) := by
74 + intro s s' h
75 + have := congrArg (fun t => stepInv t k w) h
76 + simpa only [stepInv_step] using this
77 +
78 +/-- For fixed `(k, w)` the step function is surjective on the state. -/
79 +theorem step_surjective (k w : Word) : ∀ n, ∃ s, step s k w = n :=
80 + fun n => ⟨stepInv n k w, step_stepInv n k w⟩
81 +
82 +/-- The step function is a bijection of the 256-bit state for every fixed `(k, w)`. -/
83 +theorem step_bijective (k w : Word) : Bijective (fun s => step s k w) :=
84 + ⟨step_injective k w, step_surjective k w⟩
85 +
86 +/-! ### Iterated steps: any number of rounds with known `(K_t, W_t)` is invertible. -/
87 +
88 +/-- Apply the steps for the list of `(k, w)` pairs, first element first. -/
89 +def steps (s : State) : List (Word × Word) → State
90 + | [] => s
91 + | (k, w) :: rest => steps (step s k w) rest
92 +
93 +/-- Undo the steps, last element first. -/
94 +def stepsInv (n : State) : List (Word × Word) → State
95 + | [] => n
96 + | (k, w) :: rest => stepInv (stepsInv n rest) k w
97 +
98 +theorem stepsInv_steps (kws : List (Word × Word)) : ∀ s, stepsInv (steps s kws) kws = s := by
99 + induction kws with
100 + | nil => intro s; rfl
101 + | cons kw rest ih =>
102 + intro s
103 + obtain ⟨k, w⟩ := kw
104 + simp only [steps, stepsInv, ih, stepInv_step]
105 +
106 +theorem steps_stepsInv (kws : List (Word × Word)) : ∀ n, steps (stepsInv n kws) kws = n := by
107 + induction kws with
108 + | nil => intro n; rfl
109 + | cons kw rest ih =>
110 + intro n
111 + obtain ⟨k, w⟩ := kw
112 + simp only [steps, stepsInv, step_stepInv, ih]
113 +
114 +/-- The `r`-round compression core (before the Davies–Meyer feed-forward) is a bijection of the
115 +state for every fixed sequence of round constants and message words. -/
116 +theorem steps_bijective (kws : List (Word × Word)) : Bijective (fun s => steps s kws) :=
117 + ⟨fun s s' h => by
118 + have := congrArg (fun t => stepsInv t kws) h
119 + simpa only [stepsInv_steps] using this,
120 + fun n => ⟨stepsInv n kws, steps_stepsInv kws n⟩⟩
121 +
122 +end Wp34
added formal/Wp34/Sha256Words.lean +370 −0
@@ -0,0 +1,370 @@
1 +/-
2 + WP34 — Stage 5 formal verification.
3 + SHA-256 (FIPS 180-4) word functions on `BitVec 32`, transcribed from `src/wp34/sha256.py`.
4 +
5 + Everything in this file is machine-checked by Lean 4 core only (no Mathlib).
6 + The `bv_decide` tactic bit-blasts the goal, calls the bundled CaDiCaL SAT solver and
7 + re-checks the LRAT certificate it returns inside Lean's kernel.
8 +-/
9 +import Std.Tactic.BVDecide
10 +
11 +namespace Wp34
12 +
13 +/-- A SHA-256 word. -/
14 +abbrev Word := BitVec 32
15 +
16 +/-- Right rotation (`rotr` in `sha256.py`). -/
17 +def rotr (x : Word) (k : Nat) : Word := x.rotateRight k
18 +
19 +-- The four sigma functions and the two Boolean functions, exactly as in `sha256.py`.
20 +def Sigma0 (x : Word) : Word := rotr x 2 ^^^ rotr x 13 ^^^ rotr x 22
21 +def Sigma1 (x : Word) : Word := rotr x 6 ^^^ rotr x 11 ^^^ rotr x 25
22 +def sigma0 (x : Word) : Word := rotr x 7 ^^^ rotr x 18 ^^^ (x >>> 3)
23 +def sigma1 (x : Word) : Word := rotr x 17 ^^^ rotr x 19 ^^^ (x >>> 10)
24 +def Ch (x y z : Word) : Word := (x &&& y) ^^^ (~~~x &&& z)
25 +def Maj (x y z : Word) : Word := (x &&& y) ^^^ (x &&& z) ^^^ (y &&& z)
26 +
27 +/-! ### 1(a)–(b) Optimised forms of `Ch` and `Maj` (used by most implementations) -/
28 +
29 +/-- `Ch x y z = ((y ^ z) & x) ^ z` — one fewer operation and no complement. -/
30 +theorem Ch_opt (x y z : Word) : Ch x y z = ((y ^^^ z) &&& x) ^^^ z := by
31 + unfold Ch; bv_decide
32 +
33 +/-- `Maj x y z = (x & (y | z)) | (y & z)`. -/
34 +theorem Maj_opt_or (x y z : Word) : Maj x y z = (x &&& (y ||| z)) ||| (y &&& z) := by
35 + unfold Maj; bv_decide
36 +
37 +/-- `Maj x y z = ((x ^ y) & (y ^ z)) ^ y`. -/
38 +theorem Maj_opt_xor (x y z : Word) : Maj x y z = ((x ^^^ y) &&& (y ^^^ z)) ^^^ y := by
39 + unfold Maj; bv_decide
40 +
41 +/-- `Maj` is symmetric in its arguments (used implicitly by the local-collision analysis). -/
42 +theorem Maj_comm (x y z : Word) : Maj x y z = Maj y x z ∧ Maj x y z = Maj x z y := by
43 + unfold Maj; bv_decide
44 +
45 +/-! ### 1(c) GF(2)-linearity of Σ0, Σ1, σ0, σ1 -/
46 +
47 +theorem Sigma0_xor (x y : Word) : Sigma0 (x ^^^ y) = Sigma0 x ^^^ Sigma0 y := by
48 + unfold Sigma0 rotr; bv_decide
49 +theorem Sigma1_xor (x y : Word) : Sigma1 (x ^^^ y) = Sigma1 x ^^^ Sigma1 y := by
50 + unfold Sigma1 rotr; bv_decide
51 +theorem sigma0_xor (x y : Word) : sigma0 (x ^^^ y) = sigma0 x ^^^ sigma0 y := by
52 + unfold sigma0 rotr; bv_decide
53 +theorem sigma1_xor (x y : Word) : sigma1 (x ^^^ y) = sigma1 x ^^^ sigma1 y := by
54 + unfold sigma1 rotr; bv_decide
55 +
56 +theorem Sigma0_zero : Sigma0 0 = 0 := by unfold Sigma0 rotr; bv_decide
57 +theorem Sigma1_zero : Sigma1 0 = 0 := by unfold Sigma1 rotr; bv_decide
58 +theorem sigma0_zero : sigma0 0 = 0 := by unfold sigma0 rotr; bv_decide
59 +theorem sigma1_zero : sigma1 0 = 0 := by unfold sigma1 rotr; bv_decide
60 +
61 +/-! ### 1(d)–(e) Injectivity and explicit inverses
62 +
63 +`Function.Injective` is Lean-core. Core has no `Function.Bijective`/`Surjective`, so we state
64 +bijectivity as "injective and has an explicit right inverse" through the definition below. -/
65 +
66 +/-- Bijectivity without Mathlib: injective and surjective. -/
67 +def Bijective {α β : Type} (f : α → β) : Prop := Function.Injective f ∧ ∀ y, ∃ x, f x = y
68 +
69 +theorem Sigma0_injective : Function.Injective Sigma0 := fun x y h => by
70 + unfold Sigma0 rotr at h; bv_decide
71 +theorem Sigma1_injective : Function.Injective Sigma1 := fun x y h => by
72 + unfold Sigma1 rotr at h; bv_decide
73 +/-- σ0 contains a logical shift, yet it IS injective on 32 bits (GF(2) rank 32, see
74 +`scripts/gf2_rank.py`); `bv_decide` finds no counter-example (UNSAT, certificate checked). -/
75 +theorem sigma0_injective : Function.Injective sigma0 := fun x y h => by
76 + unfold sigma0 rotr at h; bv_decide
77 +/-- Same for σ1. -/
78 +theorem sigma1_injective : Function.Injective sigma1 := fun x y h => by
79 + unfold sigma1 rotr at h; bv_decide
80 +
81 +/-- Control: the shift term alone is NOT injective (`0 >>> 3 = 1 >>> 3`). -/
82 +theorem shr3_not_injective : ¬ Function.Injective (fun x : Word => x >>> 3) := fun h =>
83 + absurd (h (a₁ := 0) (a₂ := 1) (by decide)) (by decide)
84 +
85 +/-- Control: the rotation part of σ0 alone is NOT injective either:
86 +`rotr x 7 ^ rotr x 18 = x ^ rotr x 11` up to rotation, whose kernel is `{0, 0xFFFFFFFF}`. -/
87 +theorem sigma0_rotations_not_injective :
88 + ¬ Function.Injective (fun x : Word => rotr x 7 ^^^ rotr x 18) := fun h =>
89 + absurd (h (a₁ := 0) (a₂ := 0xFFFFFFFF) (by decide)) (by decide)
90 +
91 +/-- Control: the rotation part of σ1 alone is NOT injective (`x ^ rotr x 2` kills `0xFFFFFFFF`). -/
92 +theorem sigma1_rotations_not_injective :
93 + ¬ Function.Injective (fun x : Word => rotr x 17 ^^^ rotr x 19) := fun h =>
94 + absurd (h (a₁ := 0) (a₂ := 0xFFFFFFFF) (by decide)) (by decide)
95 +
96 +/-- All-ones mask when bit `i` of `x` is set, zero otherwise (row selector for a GF(2) matrix). -/
97 +def bitMask (x : Word) (i : Nat) : Word := if x.getLsbD i then BitVec.allOnes 32 else 0
98 +
99 +-- Explicit inverses, computed over GF(2) by `scripts/gf2_rank.py --lean` and verified below.
100 +-- Σ0⁻¹ and Σ1⁻¹ are themselves XORs of 17 rotations (the inverse of a unit of
101 +-- GF(2)[ρ]/(ρ³²−1) is a polynomial in ρ); σ0⁻¹ and σ1⁻¹ are general 32×32 GF(2) matrices,
102 +-- written column by column (`bitMask x i &&& cᵢ` = column `i` if bit `i` of `x` is set).
103 +
104 +def Sigma0Inv (x : Word) : Word :=
105 + rotr x 0 ^^^ rotr x 1 ^^^ rotr x 2 ^^^ rotr x 5 ^^^ rotr x 7 ^^^ rotr x 8 ^^^ rotr x 9 ^^^
106 + rotr x 10 ^^^ rotr x 12 ^^^ rotr x 16 ^^^ rotr x 17 ^^^ rotr x 19 ^^^ rotr x 22 ^^^
107 + rotr x 23 ^^^ rotr x 25 ^^^ rotr x 29 ^^^ rotr x 30
108 +
109 +def Sigma1Inv (x : Word) : Word :=
110 + rotr x 2 ^^^ rotr x 3 ^^^ rotr x 5 ^^^ rotr x 7 ^^^ rotr x 9 ^^^ rotr x 11 ^^^ rotr x 12 ^^^
111 + rotr x 13 ^^^ rotr x 18 ^^^ rotr x 21 ^^^ rotr x 22 ^^^ rotr x 23 ^^^ rotr x 24 ^^^
112 + rotr x 26 ^^^ rotr x 27 ^^^ rotr x 29 ^^^ rotr x 30
113 +
114 +def sigma0Inv (x : Word) : Word :=
115 + bitMask x 0 &&& 0x185744e9#32 ^^^
116 + bitMask x 1 &&& 0x30ae89d2#32 ^^^
117 + bitMask x 2 &&& 0x615d13a4#32 ^^^
118 + bitMask x 3 &&& 0xdaed63a1#32 ^^^
119 + bitMask x 4 &&& 0x9cd03a8e#32 ^^^
120 + bitMask x 5 &&& 0x08fdcc39#32 ^^^
121 + bitMask x 6 &&& 0x11fb9872#32 ^^^
122 + bitMask x 7 &&& 0x23f730e4#32 ^^^
123 + bitMask x 8 &&& 0x5fb92521#32 ^^^
124 + bitMask x 9 &&& 0xbf724a42#32 ^^^
125 + bitMask x 10 &&& 0x57ee6948#32 ^^^
126 + bitMask x 11 &&& 0xafdcd290#32 ^^^
127 + bitMask x 12 &&& 0x76b358ec#32 ^^^
128 + bitMask x 13 &&& 0xf531f531#32 ^^^
129 + bitMask x 14 &&& 0xc36917ae#32 ^^^
130 + bitMask x 15 &&& 0xb78f9679#32 ^^^
131 + bitMask x 16 &&& 0x4615d13e#32 ^^^
132 + bitMask x 17 &&& 0x947ce695#32 ^^^
133 + bitMask x 18 &&& 0x19a4740f#32 ^^^
134 + bitMask x 19 &&& 0x2b1facf7#32 ^^^
135 + bitMask x 20 &&& 0x4e681d07#32 ^^^
136 + bitMask x 21 &&& 0x84877ee7#32 ^^^
137 + bitMask x 22 &&& 0x385344eb#32 ^^^
138 + bitMask x 23 &&& 0x70a689d6#32 ^^^
139 + bitMask x 24 &&& 0xf91a5745#32 ^^^
140 + bitMask x 25 &&& 0xc36917af#32 ^^^
141 + bitMask x 26 &&& 0xb78f967b#32 ^^^
142 + bitMask x 27 &&& 0x4615d13a#32 ^^^
143 + bitMask x 28 &&& 0x8c2ba274#32 ^^^
144 + bitMask x 29 &&& 0x290afdcd#32 ^^^
145 + bitMask x 30 &&& 0x4a42bf73#32 ^^^
146 + bitMask x 31 &&& 0x94857ee6#32
147 +
148 +def sigma1Inv (x : Word) : Word :=
149 + bitMask x 0 &&& 0x2ccfffed#32 ^^^
150 + bitMask x 1 &&& 0x75500037#32 ^^^
151 + bitMask x 2 &&& 0xeaa0006e#32 ^^^
152 + bitMask x 3 &&& 0x589d5570#32 ^^^
153 + bitMask x 4 &&& 0xb13aaae0#32 ^^^
154 + bitMask x 5 &&& 0xc367ff81#32 ^^^
155 + bitMask x 6 &&& 0x27dd5543#32 ^^^
156 + bitMask x 7 &&& 0x4fbaaa86#32 ^^^
157 + bitMask x 8 &&& 0xb3baaae1#32 ^^^
158 + bitMask x 9 &&& 0xc667ff83#32 ^^^
159 + bitMask x 10 &&& 0x2ddd5547#32 ^^^
160 + bitMask x 11 &&& 0x5bbaaa8e#32 ^^^
161 + bitMask x 12 &&& 0x9bbaaaf1#32 ^^^
162 + bitMask x 13 &&& 0x9667ffa3#32 ^^^
163 + bitMask x 14 &&& 0x8ddd5507#32 ^^^
164 + bitMask x 15 &&& 0x9667ffa2#32 ^^^
165 + bitMask x 16 &&& 0x8ddd5505#32 ^^^
166 + bitMask x 17 &&& 0x9667ffa6#32 ^^^
167 + bitMask x 18 &&& 0x8ddd550d#32 ^^^
168 + bitMask x 19 &&& 0x9667ffb6#32 ^^^
169 + bitMask x 20 &&& 0x8ddd552d#32 ^^^
170 + bitMask x 21 &&& 0x9667fff6#32 ^^^
171 + bitMask x 22 &&& 0x8ddd55ad#32 ^^^
172 + bitMask x 23 &&& 0x9667fef6#32 ^^^
173 + bitMask x 24 &&& 0x8ddd57ad#32 ^^^
174 + bitMask x 25 &&& 0xbaa8051b#32 ^^^
175 + bitMask x 26 &&& 0xf88d5f9a#32 ^^^
176 + bitMask x 27 &&& 0x50081575#32 ^^^
177 + bitMask x 28 &&& 0xa0102aea#32 ^^^
178 + bitMask x 29 &&& 0xe132ff95#32 ^^^
179 + bitMask x 30 &&& 0x6377556b#32 ^^^
180 + bitMask x 31 &&& 0xc6eeaad6#32
181 +
182 +/-! #### Proof strategy for the inverse identities
183 +
184 +`Sigma0Inv (Sigma0 x) = x` is a pure parity (XOR) identity in 32 variables: each output bit is the
185 +XOR of 51 input-bit occurrences that cancel pairwise. CDCL SAT solvers handle such parity
186 +instances badly (the direct `bv_decide` call exceeds a 10 s timeout, see README). We therefore
187 +(i) prove GF(2)-linearity of the inverses by the general lemmas `rotr_xor` / `bitMask_xor`
188 +(bit-level, no SAT) and `ac_rfl`; (ii) check the identity on the two 16-bit halves of `x` with
189 +`bv_decide` (16 free variables: trivial for SAT); (iii) glue with `inv_of_halves`. -/
190 +
191 +/-- Rotation distributes over XOR (bit-level proof from core's `getLsbD_rotateRight`). -/
192 +theorem rotr_xor (x y : Word) (k : Nat) : rotr (x ^^^ y) k = rotr x k ^^^ rotr y k := by
193 + apply BitVec.eq_of_getLsbD_eq
194 + intro i
195 + simp only [rotr, BitVec.getLsbD_rotateRight, BitVec.getLsbD_xor]
196 + by_cases h : i < 32 - k % 32 <;> by_cases h' : i < 32 <;> simp [h, h']
197 +
198 +theorem bitMask_xor (x y : Word) (i : Nat) :
199 + bitMask (x ^^^ y) i = bitMask x i ^^^ bitMask y i := by
200 + unfold bitMask
201 + simp only [BitVec.getLsbD_xor]
202 + cases x.getLsbD i <;> cases y.getLsbD i <;> simp
203 +
204 +theorem xor_and_right (a b c : Word) : (a ^^^ b) &&& c = (a &&& c) ^^^ (b &&& c) := by
205 + bv_decide
206 +
207 +theorem Sigma0Inv_xor (x y : Word) : Sigma0Inv (x ^^^ y) = Sigma0Inv x ^^^ Sigma0Inv y := by
208 + simp only [Sigma0Inv, rotr_xor]; ac_rfl
209 +theorem Sigma1Inv_xor (x y : Word) : Sigma1Inv (x ^^^ y) = Sigma1Inv x ^^^ Sigma1Inv y := by
210 + simp only [Sigma1Inv, rotr_xor]; ac_rfl
211 +theorem sigma0Inv_xor (x y : Word) : sigma0Inv (x ^^^ y) = sigma0Inv x ^^^ sigma0Inv y := by
212 + simp only [sigma0Inv, bitMask_xor, xor_and_right]; ac_rfl
213 +theorem sigma1Inv_xor (x y : Word) : sigma1Inv (x ^^^ y) = sigma1Inv x ^^^ sigma1Inv y := by
214 + simp only [sigma1Inv, bitMask_xor, xor_and_right]; ac_rfl
215 +
216 +/-- Two GF(2)-linear maps whose composite fixes each of the four bytes of `x` compose to the
217 +identity. (Bytes rather than 16-bit halves: an 8-variable SAT instance yields a ~0.6 MB LRAT
218 +certificate, a 16-variable one ~100 MB, and the certificates are stored in the `.olean`.) -/
219 +theorem inv_of_quarters (f g : Word → Word)
220 + (hf : ∀ x y, f (x ^^^ y) = f x ^^^ f y) (hg : ∀ x y, g (x ^^^ y) = g x ^^^ g y)
221 + (h0 : ∀ x, g (f (x &&& 0x000000FF#32)) = x &&& 0x000000FF#32)
222 + (h1 : ∀ x, g (f (x &&& 0x0000FF00#32)) = x &&& 0x0000FF00#32)
223 + (h2 : ∀ x, g (f (x &&& 0x00FF0000#32)) = x &&& 0x00FF0000#32)
224 + (h3 : ∀ x, g (f (x &&& 0xFF000000#32)) = x &&& 0xFF000000#32) :
225 + ∀ x, g (f x) = x := by
226 + intro x
227 + have hsplit : x = (x &&& 0x000000FF#32) ^^^ (x &&& 0x0000FF00#32) ^^^ (x &&& 0x00FF0000#32)
228 + ^^^ (x &&& 0xFF000000#32) := by bv_decide
229 + calc g (f x) = g (f ((x &&& 0x000000FF#32) ^^^ (x &&& 0x0000FF00#32) ^^^ (x &&& 0x00FF0000#32)
230 + ^^^ (x &&& 0xFF000000#32))) := by rw [← hsplit]
231 + _ = g (f (x &&& 0x000000FF#32)) ^^^ g (f (x &&& 0x0000FF00#32)) ^^^ g (f (x &&& 0x00FF0000#32))
232 + ^^^ g (f (x &&& 0xFF000000#32)) := by rw [hf, hf, hf, hg, hg, hg]
233 + _ = x := by rw [h0, h1, h2, h3, ← hsplit]
234 +
235 +-- Byte instances (each a small SAT problem with 8 free variables).
236 +theorem Sigma0Inv_Sigma0_b0 (x : Word) :
237 + Sigma0Inv (Sigma0 (x &&& 0x000000FF#32)) = x &&& 0x000000FF#32 := by
238 + unfold Sigma0Inv Sigma0 rotr; bv_decide
239 +theorem Sigma0Inv_Sigma0_b1 (x : Word) :
240 + Sigma0Inv (Sigma0 (x &&& 0x0000FF00#32)) = x &&& 0x0000FF00#32 := by
241 + unfold Sigma0Inv Sigma0 rotr; bv_decide
242 +theorem Sigma0Inv_Sigma0_b2 (x : Word) :
243 + Sigma0Inv (Sigma0 (x &&& 0x00FF0000#32)) = x &&& 0x00FF0000#32 := by
244 + unfold Sigma0Inv Sigma0 rotr; bv_decide
245 +theorem Sigma0Inv_Sigma0_b3 (x : Word) :
246 + Sigma0Inv (Sigma0 (x &&& 0xFF000000#32)) = x &&& 0xFF000000#32 := by
247 + unfold Sigma0Inv Sigma0 rotr; bv_decide
248 +theorem Sigma0_Sigma0Inv_b0 (x : Word) :
249 + Sigma0 (Sigma0Inv (x &&& 0x000000FF#32)) = x &&& 0x000000FF#32 := by
250 + unfold Sigma0Inv Sigma0 rotr; bv_decide
251 +theorem Sigma0_Sigma0Inv_b1 (x : Word) :
252 + Sigma0 (Sigma0Inv (x &&& 0x0000FF00#32)) = x &&& 0x0000FF00#32 := by
253 + unfold Sigma0Inv Sigma0 rotr; bv_decide
254 +theorem Sigma0_Sigma0Inv_b2 (x : Word) :
255 + Sigma0 (Sigma0Inv (x &&& 0x00FF0000#32)) = x &&& 0x00FF0000#32 := by
256 + unfold Sigma0Inv Sigma0 rotr; bv_decide
257 +theorem Sigma0_Sigma0Inv_b3 (x : Word) :
258 + Sigma0 (Sigma0Inv (x &&& 0xFF000000#32)) = x &&& 0xFF000000#32 := by
259 + unfold Sigma0Inv Sigma0 rotr; bv_decide
260 +theorem Sigma1Inv_Sigma1_b0 (x : Word) :
261 + Sigma1Inv (Sigma1 (x &&& 0x000000FF#32)) = x &&& 0x000000FF#32 := by
262 + unfold Sigma1Inv Sigma1 rotr; bv_decide
263 +theorem Sigma1Inv_Sigma1_b1 (x : Word) :
264 + Sigma1Inv (Sigma1 (x &&& 0x0000FF00#32)) = x &&& 0x0000FF00#32 := by
265 + unfold Sigma1Inv Sigma1 rotr; bv_decide
266 +theorem Sigma1Inv_Sigma1_b2 (x : Word) :
267 + Sigma1Inv (Sigma1 (x &&& 0x00FF0000#32)) = x &&& 0x00FF0000#32 := by
268 + unfold Sigma1Inv Sigma1 rotr; bv_decide
269 +theorem Sigma1Inv_Sigma1_b3 (x : Word) :
270 + Sigma1Inv (Sigma1 (x &&& 0xFF000000#32)) = x &&& 0xFF000000#32 := by
271 + unfold Sigma1Inv Sigma1 rotr; bv_decide
272 +theorem Sigma1_Sigma1Inv_b0 (x : Word) :
273 + Sigma1 (Sigma1Inv (x &&& 0x000000FF#32)) = x &&& 0x000000FF#32 := by
274 + unfold Sigma1Inv Sigma1 rotr; bv_decide
275 +theorem Sigma1_Sigma1Inv_b1 (x : Word) :
276 + Sigma1 (Sigma1Inv (x &&& 0x0000FF00#32)) = x &&& 0x0000FF00#32 := by
277 + unfold Sigma1Inv Sigma1 rotr; bv_decide
278 +theorem Sigma1_Sigma1Inv_b2 (x : Word) :
279 + Sigma1 (Sigma1Inv (x &&& 0x00FF0000#32)) = x &&& 0x00FF0000#32 := by
280 + unfold Sigma1Inv Sigma1 rotr; bv_decide
281 +theorem Sigma1_Sigma1Inv_b3 (x : Word) :
282 + Sigma1 (Sigma1Inv (x &&& 0xFF000000#32)) = x &&& 0xFF000000#32 := by
283 + unfold Sigma1Inv Sigma1 rotr; bv_decide
284 +theorem sigma0Inv_sigma0_b0 (x : Word) :
285 + sigma0Inv (sigma0 (x &&& 0x000000FF#32)) = x &&& 0x000000FF#32 := by
286 + unfold sigma0Inv sigma0 bitMask rotr; bv_decide
287 +theorem sigma0Inv_sigma0_b1 (x : Word) :
288 + sigma0Inv (sigma0 (x &&& 0x0000FF00#32)) = x &&& 0x0000FF00#32 := by
289 + unfold sigma0Inv sigma0 bitMask rotr; bv_decide
290 +theorem sigma0Inv_sigma0_b2 (x : Word) :
291 + sigma0Inv (sigma0 (x &&& 0x00FF0000#32)) = x &&& 0x00FF0000#32 := by
292 + unfold sigma0Inv sigma0 bitMask rotr; bv_decide
293 +theorem sigma0Inv_sigma0_b3 (x : Word) :
294 + sigma0Inv (sigma0 (x &&& 0xFF000000#32)) = x &&& 0xFF000000#32 := by
295 + unfold sigma0Inv sigma0 bitMask rotr; bv_decide
296 +theorem sigma0_sigma0Inv_b0 (x : Word) :
297 + sigma0 (sigma0Inv (x &&& 0x000000FF#32)) = x &&& 0x000000FF#32 := by
298 + unfold sigma0Inv sigma0 bitMask rotr; bv_decide
299 +theorem sigma0_sigma0Inv_b1 (x : Word) :
300 + sigma0 (sigma0Inv (x &&& 0x0000FF00#32)) = x &&& 0x0000FF00#32 := by
301 + unfold sigma0Inv sigma0 bitMask rotr; bv_decide
302 +theorem sigma0_sigma0Inv_b2 (x : Word) :
303 + sigma0 (sigma0Inv (x &&& 0x00FF0000#32)) = x &&& 0x00FF0000#32 := by
304 + unfold sigma0Inv sigma0 bitMask rotr; bv_decide
305 +theorem sigma0_sigma0Inv_b3 (x : Word) :
306 + sigma0 (sigma0Inv (x &&& 0xFF000000#32)) = x &&& 0xFF000000#32 := by
307 + unfold sigma0Inv sigma0 bitMask rotr; bv_decide
308 +theorem sigma1Inv_sigma1_b0 (x : Word) :
309 + sigma1Inv (sigma1 (x &&& 0x000000FF#32)) = x &&& 0x000000FF#32 := by
310 + unfold sigma1Inv sigma1 bitMask rotr; bv_decide
311 +theorem sigma1Inv_sigma1_b1 (x : Word) :
312 + sigma1Inv (sigma1 (x &&& 0x0000FF00#32)) = x &&& 0x0000FF00#32 := by
313 + unfold sigma1Inv sigma1 bitMask rotr; bv_decide
314 +theorem sigma1Inv_sigma1_b2 (x : Word) :
315 + sigma1Inv (sigma1 (x &&& 0x00FF0000#32)) = x &&& 0x00FF0000#32 := by
316 + unfold sigma1Inv sigma1 bitMask rotr; bv_decide
317 +theorem sigma1Inv_sigma1_b3 (x : Word) :
318 + sigma1Inv (sigma1 (x &&& 0xFF000000#32)) = x &&& 0xFF000000#32 := by
319 + unfold sigma1Inv sigma1 bitMask rotr; bv_decide
320 +theorem sigma1_sigma1Inv_b0 (x : Word) :
321 + sigma1 (sigma1Inv (x &&& 0x000000FF#32)) = x &&& 0x000000FF#32 := by
322 + unfold sigma1Inv sigma1 bitMask rotr; bv_decide
323 +theorem sigma1_sigma1Inv_b1 (x : Word) :
324 + sigma1 (sigma1Inv (x &&& 0x0000FF00#32)) = x &&& 0x0000FF00#32 := by
325 + unfold sigma1Inv sigma1 bitMask rotr; bv_decide
326 +theorem sigma1_sigma1Inv_b2 (x : Word) :
327 + sigma1 (sigma1Inv (x &&& 0x00FF0000#32)) = x &&& 0x00FF0000#32 := by
328 + unfold sigma1Inv sigma1 bitMask rotr; bv_decide
329 +theorem sigma1_sigma1Inv_b3 (x : Word) :
330 + sigma1 (sigma1Inv (x &&& 0xFF000000#32)) = x &&& 0xFF000000#32 := by
331 + unfold sigma1Inv sigma1 bitMask rotr; bv_decide
332 +
333 +/-- `Sigma0Inv ∘ Sigma0 = id`. -/
334 +theorem Sigma0Inv_Sigma0 : ∀ x, Sigma0Inv (Sigma0 x) = x :=
335 + inv_of_quarters Sigma0 Sigma0Inv Sigma0_xor Sigma0Inv_xor Sigma0Inv_Sigma0_b0 Sigma0Inv_Sigma0_b1 Sigma0Inv_Sigma0_b2 Sigma0Inv_Sigma0_b3
336 +/-- `Sigma0 ∘ Sigma0Inv = id`. -/
337 +theorem Sigma0_Sigma0Inv : ∀ x, Sigma0 (Sigma0Inv x) = x :=
338 + inv_of_quarters Sigma0Inv Sigma0 Sigma0Inv_xor Sigma0_xor Sigma0_Sigma0Inv_b0 Sigma0_Sigma0Inv_b1 Sigma0_Sigma0Inv_b2 Sigma0_Sigma0Inv_b3
339 +/-- `Sigma1Inv ∘ Sigma1 = id`. -/
340 +theorem Sigma1Inv_Sigma1 : ∀ x, Sigma1Inv (Sigma1 x) = x :=
341 + inv_of_quarters Sigma1 Sigma1Inv Sigma1_xor Sigma1Inv_xor Sigma1Inv_Sigma1_b0 Sigma1Inv_Sigma1_b1 Sigma1Inv_Sigma1_b2 Sigma1Inv_Sigma1_b3
342 +/-- `Sigma1 ∘ Sigma1Inv = id`. -/
343 +theorem Sigma1_Sigma1Inv : ∀ x, Sigma1 (Sigma1Inv x) = x :=
344 + inv_of_quarters Sigma1Inv Sigma1 Sigma1Inv_xor Sigma1_xor Sigma1_Sigma1Inv_b0 Sigma1_Sigma1Inv_b1 Sigma1_Sigma1Inv_b2 Sigma1_Sigma1Inv_b3
345 +/-- `sigma0Inv ∘ sigma0 = id`. -/
346 +theorem sigma0Inv_sigma0 : ∀ x, sigma0Inv (sigma0 x) = x :=
347 + inv_of_quarters sigma0 sigma0Inv sigma0_xor sigma0Inv_xor sigma0Inv_sigma0_b0 sigma0Inv_sigma0_b1 sigma0Inv_sigma0_b2 sigma0Inv_sigma0_b3
348 +/-- `sigma0 ∘ sigma0Inv = id`. -/
349 +theorem sigma0_sigma0Inv : ∀ x, sigma0 (sigma0Inv x) = x :=
350 + inv_of_quarters sigma0Inv sigma0 sigma0Inv_xor sigma0_xor sigma0_sigma0Inv_b0 sigma0_sigma0Inv_b1 sigma0_sigma0Inv_b2 sigma0_sigma0Inv_b3
351 +/-- `sigma1Inv ∘ sigma1 = id`. -/
352 +theorem sigma1Inv_sigma1 : ∀ x, sigma1Inv (sigma1 x) = x :=
353 + inv_of_quarters sigma1 sigma1Inv sigma1_xor sigma1Inv_xor sigma1Inv_sigma1_b0 sigma1Inv_sigma1_b1 sigma1Inv_sigma1_b2 sigma1Inv_sigma1_b3
354 +/-- `sigma1 ∘ sigma1Inv = id`. -/
355 +theorem sigma1_sigma1Inv : ∀ x, sigma1 (sigma1Inv x) = x :=
356 + inv_of_quarters sigma1Inv sigma1 sigma1Inv_xor sigma1_xor sigma1_sigma1Inv_b0 sigma1_sigma1Inv_b1 sigma1_sigma1Inv_b2 sigma1_sigma1Inv_b3
357 +
358 +/-- Σ0 is a bijection of `BitVec 32` (explicit inverse `Sigma0Inv`). -/
359 +theorem Sigma0_bijective : Bijective Sigma0 :=
360 + ⟨Sigma0_injective, fun y => ⟨Sigma0Inv y, Sigma0_Sigma0Inv y⟩⟩
361 +theorem Sigma1_bijective : Bijective Sigma1 :=
362 + ⟨Sigma1_injective, fun y => ⟨Sigma1Inv y, Sigma1_Sigma1Inv y⟩⟩
363 +/-- σ0 is a bijection of `BitVec 32` (explicit inverse `sigma0Inv`): the shift does not
364 +destroy invertibility; the kernel of the GF(2) map is trivial. -/
365 +theorem sigma0_bijective : Bijective sigma0 :=
366 + ⟨sigma0_injective, fun y => ⟨sigma0Inv y, sigma0_sigma0Inv y⟩⟩
367 +theorem sigma1_bijective : Bijective sigma1 :=
368 + ⟨sigma1_injective, fun y => ⟨sigma1Inv y, sigma1_sigma1Inv y⟩⟩
369 +
370 +end Wp34
added formal/build.log +133 −0
@@ -0,0 +1,133 @@
1 +# lake clean && lake build — 2026-11-01 01:56:37 EST — Lean (version 4.35.0, arm64-apple-darwin24.6.0, commit eb6cafb7290e8bd5f2937522d8d804f58f02bafc, Release) — Apple M5 Max
2 +✔ [2/9] Built Wp34.Group (315ms)
3 +✔ [3/9] Built Wp34.Sha256Words (2.5s)
4 +✔ [4/9] Built Wp34.Schedule (1.5s)
5 +✔ [5/9] Built Wp34.Sha256Step (27s)
6 +✔ [6/9] Built Wp34.LocalCollision (1.7s)
7 +ℹ [7/9] Built Wp34.Audit (201ms)
8 +info: Wp34/Audit.lean:15:0: 'Wp34.Ch_opt' depends on axioms: [propext, Classical.choice, Quot.sound, Ch_opt._native.bv_decide.ax_1_5]
9 +info: Wp34/Audit.lean:16:0: 'Wp34.Maj_opt_or' depends on axioms: [propext, Classical.choice, Quot.sound, Maj_opt_or._native.bv_decide.ax_1_5]
10 +info: Wp34/Audit.lean:17:0: 'Wp34.Maj_opt_xor' depends on axioms: [propext, Classical.choice, Quot.sound, Maj_opt_xor._native.bv_decide.ax_1_5]
11 +info: Wp34/Audit.lean:18:0: 'Wp34.Sigma0_xor' depends on axioms: [propext, Classical.choice, Quot.sound, Sigma0_xor._native.bv_decide.ax_1_5]
12 +info: Wp34/Audit.lean:19:0: 'Wp34.Sigma0_injective' depends on axioms: [propext,
13 + Classical.choice,
14 + Quot.sound,
15 + Sigma0_injective._native.bv_decide.ax_1_5]
16 +info: Wp34/Audit.lean:20:0: 'Wp34.sigma0_injective' depends on axioms: [propext,
17 + Classical.choice,
18 + Quot.sound,
19 + sigma0_injective._native.bv_decide.ax_1_5]
20 +info: Wp34/Audit.lean:21:0: 'Wp34.sigma1_injective' depends on axioms: [propext,
21 + Classical.choice,
22 + Quot.sound,
23 + sigma1_injective._native.bv_decide.ax_1_5]
24 +info: Wp34/Audit.lean:22:0: 'Wp34.rotr_xor' depends on axioms: [propext, Quot.sound]
25 +info: Wp34/Audit.lean:23:0: 'Wp34.Sigma0Inv_xor' depends on axioms: [propext, Quot.sound]
26 +info: Wp34/Audit.lean:24:0: 'Wp34.Sigma0Inv_Sigma0' depends on axioms: [propext,
27 + Classical.choice,
28 + Quot.sound,
29 + Sigma0Inv_Sigma0_b0._native.bv_decide.ax_1_5,
30 + Sigma0Inv_Sigma0_b1._native.bv_decide.ax_1_5,
31 + Sigma0Inv_Sigma0_b2._native.bv_decide.ax_1_5,
32 + Sigma0Inv_Sigma0_b3._native.bv_decide.ax_1_5,
33 + Sigma0_xor._native.bv_decide.ax_1_5,
34 + inv_of_quarters._native.bv_decide.ax_1_5]
35 +info: Wp34/Audit.lean:25:0: 'Wp34.sigma0_bijective' depends on axioms: [propext,
36 + Classical.choice,
37 + Quot.sound,
38 + inv_of_quarters._native.bv_decide.ax_1_5,
39 + sigma0_injective._native.bv_decide.ax_1_5,
40 + sigma0_sigma0Inv_b0._native.bv_decide.ax_1_5,
41 + sigma0_sigma0Inv_b1._native.bv_decide.ax_1_5,
42 + sigma0_sigma0Inv_b2._native.bv_decide.ax_1_5,
43 + sigma0_sigma0Inv_b3._native.bv_decide.ax_1_5,
44 + sigma0_xor._native.bv_decide.ax_1_5,
45 + xor_and_right._native.bv_decide.ax_1_5]
46 +info: Wp34/Audit.lean:26:0: 'Wp34.sigma1_bijective' depends on axioms: [propext,
47 + Classical.choice,
48 + Quot.sound,
49 + inv_of_quarters._native.bv_decide.ax_1_5,
50 + sigma1_injective._native.bv_decide.ax_1_5,
51 + sigma1_sigma1Inv_b0._native.bv_decide.ax_1_5,
52 + sigma1_sigma1Inv_b1._native.bv_decide.ax_1_5,
53 + sigma1_sigma1Inv_b2._native.bv_decide.ax_1_5,
54 + sigma1_sigma1Inv_b3._native.bv_decide.ax_1_5,
55 + sigma1_xor._native.bv_decide.ax_1_5,
56 + xor_and_right._native.bv_decide.ax_1_5]
57 +info: Wp34/Audit.lean:27:0: 'Wp34.shr3_not_injective' depends on axioms: [propext]
58 +info: Wp34/Audit.lean:28:0: 'Wp34.stepInv_step' depends on axioms: [propext, Classical.choice, Quot.sound, add4_sub4._native.bv_decide.ax_1_5]
59 +info: Wp34/Audit.lean:29:0: 'Wp34.step_stepInv' depends on axioms: [propext, Classical.choice, Quot.sound, sub4_add4._native.bv_decide.ax_1_5]
60 +info: Wp34/Audit.lean:30:0: 'Wp34.step_bijective' depends on axioms: [propext,
61 + Classical.choice,
62 + Quot.sound,
63 + add4_sub4._native.bv_decide.ax_1_5,
64 + sub4_add4._native.bv_decide.ax_1_5]
65 +info: Wp34/Audit.lean:31:0: 'Wp34.steps_bijective' depends on axioms: [propext,
66 + Classical.choice,
67 + Quot.sound,
68 + add4_sub4._native.bv_decide.ax_1_5,
69 + sub4_add4._native.bv_decide.ax_1_5]
70 +info: Wp34/Audit.lean:32:0: 'Wp34.add4_sub4' depends on axioms: [propext, Classical.choice, Quot.sound, add4_sub4._native.bv_decide.ax_1_5]
71 +info: Wp34/Audit.lean:33:0: 'Wp34.sub4_add4' depends on axioms: [propext, Classical.choice, Quot.sound, sub4_add4._native.bv_decide.ax_1_5]
72 +info: Wp34/Audit.lean:34:0: 'Wp34.scheduleStep_inv' depends on axioms: [propext,
73 + Classical.choice,
74 + Quot.sound,
75 + scheduleStep_inv._native.bv_decide.ax_1_5]
76 +info: Wp34/Audit.lean:35:0: 'Wp34.schedule_backward' depends on axioms: [propext,
77 + Classical.choice,
78 + Quot.sound,
79 + scheduleStep_inv._native.bv_decide.ax_1_5]
80 +info: Wp34/Audit.lean:36:0: 'Wp34.localCollision_absorb' depends on axioms: [propext,
81 + Classical.choice,
82 + Quot.sound,
83 + Sigma0_xor._native.bv_decide.ax_1_5,
84 + Sigma1_xor._native.bv_decide.ax_1_5,
85 + byte_split._native.bv_decide.ax_1_5,
86 + diff_absorb_b0._native.bv_decide.ax_1_13,
87 + diff_absorb_b1._native.bv_decide.ax_1_13,
88 + diff_absorb_b2._native.bv_decide.ax_1_13,
89 + diff_absorb_b3._native.bv_decide.ax_1_13,
90 + linStep_xor_absorb._native.bv_decide.ax_1_10,
91 + State.xor_zero._native.bv_decide.ax_1_7,
92 + Words9.xor_zero._native.bv_decide.ax_1_7]
93 +info: Wp34/Audit.lean:37:0: 'Wp34.localCollision_xor3' depends on axioms: [propext,
94 + Classical.choice,
95 + Quot.sound,
96 + Sigma0_xor._native.bv_decide.ax_1_5,
97 + Sigma1_xor._native.bv_decide.ax_1_5,
98 + byte_split._native.bv_decide.ax_1_5,
99 + diff_xor3_b0._native.bv_decide.ax_1_13,
100 + diff_xor3_b1._native.bv_decide.ax_1_13,
101 + diff_xor3_b2._native.bv_decide.ax_1_13,
102 + diff_xor3_b3._native.bv_decide.ax_1_13,
103 + linStep_xor_xor3._native.bv_decide.ax_1_10,
104 + State.xor_zero._native.bv_decide.ax_1_7,
105 + Words9.xor_zero._native.bv_decide.ax_1_7]
106 +info: Wp34/Audit.lean:38:0: 'Wp34.localCollision_absorb_diff' depends on axioms: [propext,
107 + Classical.choice,
108 + Quot.sound,
109 + Sigma0_xor._native.bv_decide.ax_1_5,
110 + Sigma1_xor._native.bv_decide.ax_1_5,
111 + byte_split._native.bv_decide.ax_1_5,
112 + diff_absorb_b0._native.bv_decide.ax_1_13,
113 + diff_absorb_b1._native.bv_decide.ax_1_13,
114 + diff_absorb_b2._native.bv_decide.ax_1_13,
115 + diff_absorb_b3._native.bv_decide.ax_1_13,
116 + linStep_xor_absorb._native.bv_decide.ax_1_10,
117 + State.xor_zero._native.bv_decide.ax_1_7,
118 + Words9.xor_zero._native.bv_decide.ax_1_7]
119 +info: Wp34/Audit.lean:39:0: 'Wp34.lin9_xor_absorb' depends on axioms: [propext,
120 + Classical.choice,
121 + Quot.sound,
122 + linStep_xor_absorb._native.bv_decide.ax_1_10]
123 +info: Wp34/Audit.lean:40:0: 'Wp34.linStep_xor_absorb' depends on axioms: [propext,
124 + Classical.choice,
125 + Quot.sound,
126 + linStep_xor_absorb._native.bv_decide.ax_1_10]
127 +info: Wp34/Audit.lean:41:0: 'Wp34.absorb_corrections_not_real_collision' depends on axioms: [propext, Quot.sound]
128 +info: Wp34/Audit.lean:42:0: 'Wp34.CyclicGroup.schnorr_correct' depends on axioms: [propext]
129 +info: Wp34/Audit.lean:43:0: 'Wp34.CyclicGroup.ecdsa_malleable' depends on axioms: [propext, Quot.sound]
130 +info: Wp34/Audit.lean:44:0: 'Wp34.CyclicGroup.ecdsa_correct' depends on axioms: [propext, Quot.sound]
131 +✔ [8/9] Built Wp34 (193ms)
132 +Build completed successfully (9 jobs).
133 +# exit code 0 — wall time 32 s
added formal/lake-manifest.json +6 −0
@@ -0,0 +1,6 @@
1 +{"version": "1.3.0",
2 + "packagesDir": ".lake/packages",
3 + "packages": [],
4 + "name": "wp34",
5 + "lakeDir": ".lake",
6 + "fixedToolchain": false}
added formal/lakefile.toml +6 −0
@@ -0,0 +1,6 @@
1 +name = "wp34"
2 +version = "0.1.0"
3 +defaultTargets = ["Wp34"]
4 +
5 +[[lean_lib]]
6 +name = "Wp34"
added formal/lean-toolchain +1 −0
@@ -0,0 +1 @@
1 +leanprover/lean4:v4.35.0
added formal/scripts/aig_sizes.py +50 −0
@@ -0,0 +1,50 @@
1 +"""Per-theorem SAT instance sizes for the bv_decide proofs.
2 +
3 +Re-elaborates each module with `trace.Meta.Tactic.bv` enabled and `--json` output: every trace
4 +block is one JSON message carrying its source position, so each "AIG has N nodes" value (size of
5 +the bit-blasted And-Inverter Graph handed to CaDiCaL) is attributed to the enclosing theorem
6 +exactly. `bv_decide` calls closed by preprocessing alone emit no AIG. Requires a prior
7 +`lake build` (imports must be compiled).
8 +
9 +Run (from formal/): ../.venv/bin/python scripts/aig_sizes.py
10 +"""
11 +from __future__ import annotations
12 +import json, os, re, subprocess, sys, time
13 +from pathlib import Path
14 +
15 +HERE = Path(__file__).resolve().parent.parent
16 +MODULES = ["Sha256Words", "Sha256Step", "Schedule", "LocalCollision"]
17 +
18 +def theorem_at(src_lines, lineno):
19 + name = None
20 + for i in range(lineno - 1, -1, -1):
21 + m = re.match(r"\s*(?:theorem|lemma|def)\s+([\w.']+)", src_lines[i])
22 + if m:
23 + return m.group(1)
24 + return name
25 +
26 +def main():
27 + env = dict(os.environ)
28 + env["PATH"] = str(Path.home() / ".elan" / "bin") + os.pathsep + env.get("PATH", "")
29 + rows = []
30 + for mod in MODULES:
31 + path = HERE / "Wp34" / f"{mod}.lean"
32 + src = path.read_text().splitlines()
33 + t0 = time.time()
34 + r = subprocess.run(["lake", "env", "lean", "--json", "-D", "trace.Meta.Tactic.bv=true", str(path)],
35 + cwd=HERE, env=env, capture_output=True, text=True)
36 + dt = time.time() - t0
37 + for line in r.stdout.splitlines():
38 + try:
39 + msg = json.loads(line)
40 + except json.JSONDecodeError:
41 + continue
42 + for m in re.finditer(r"AIG has (\d+) nodes", msg.get("data", "")):
43 + rows.append((mod, theorem_at(src, msg["pos"]["line"]), int(m.group(1))))
44 + print(f"# {mod}: re-elaborated with trace in {dt:.1f} s, exit {r.returncode}", file=sys.stderr)
45 + print("module\ttheorem\taig_nodes")
46 + for mod, name, n in rows:
47 + print(f"{mod}\t{name}\t{n}")
48 +
49 +if __name__ == "__main__":
50 + main()
added formal/scripts/aig_sizes.tsv +64 −0
@@ -0,0 +1,64 @@
1 +module theorem aig_nodes
2 +Sha256Words Ch_opt 608
3 +Sha256Words Maj_opt_or 608
4 +Sha256Words Maj_opt_xor 768
5 +Sha256Words Maj_comm 704
6 +Sha256Words Sigma0_xor 960
7 +Sha256Words Sigma1_xor 960
8 +Sha256Words sigma0_xor 933
9 +Sha256Words sigma1_xor 870
10 +Sha256Words Sigma0_injective 704
11 +Sha256Words Sigma1_injective 704
12 +Sha256Words sigma0_injective 686
13 +Sha256Words sigma1_injective 644
14 +Sha256Words xor_and_right 512
15 +Sha256Words inv_of_quarters 33
16 +Sha256Words Sigma0Inv_Sigma0_b0 1021
17 +Sha256Words Sigma0Inv_Sigma0_b1 1021
18 +Sha256Words Sigma0Inv_Sigma0_b2 1021
19 +Sha256Words Sigma0Inv_Sigma0_b3 1021
20 +Sha256Words Sigma0_Sigma0Inv_b0 508
21 +Sha256Words Sigma0_Sigma0Inv_b1 508
22 +Sha256Words Sigma0_Sigma0Inv_b2 508
23 +Sha256Words Sigma0_Sigma0Inv_b3 508
24 +Sha256Words Sigma1Inv_Sigma1_b0 973
25 +Sha256Words Sigma1Inv_Sigma1_b1 973
26 +Sha256Words Sigma1Inv_Sigma1_b2 973
27 +Sha256Words Sigma1Inv_Sigma1_b3 973
28 +Sha256Words Sigma1_Sigma1Inv_b0 508
29 +Sha256Words Sigma1_Sigma1Inv_b1 508
30 +Sha256Words Sigma1_Sigma1Inv_b2 508
31 +Sha256Words Sigma1_Sigma1Inv_b3 508
32 +Sha256Words sigma0Inv_sigma0_b0 4670
33 +Sha256Words sigma0Inv_sigma0_b1 4679
34 +Sha256Words sigma0Inv_sigma0_b2 4679
35 +Sha256Words sigma0Inv_sigma0_b3 4679
36 +Sha256Words sigma0_sigma0Inv_b0 1328
37 +Sha256Words sigma0_sigma0Inv_b1 1376
38 +Sha256Words sigma0_sigma0Inv_b2 1343
39 +Sha256Words sigma0_sigma0Inv_b3 1364
40 +Sha256Words sigma1Inv_sigma1_b0 4793
41 +Sha256Words sigma1Inv_sigma1_b1 4811
42 +Sha256Words sigma1Inv_sigma1_b2 4817
43 +Sha256Words sigma1Inv_sigma1_b3 4817
44 +Sha256Words sigma1_sigma1Inv_b0 1328
45 +Sha256Words sigma1_sigma1Inv_b1 1391
46 +Sha256Words sigma1_sigma1Inv_b2 1400
47 +Sha256Words sigma1_sigma1Inv_b3 1334
48 +Sha256Step add4_sub4 2048
49 +Sha256Step sub4_add4 2420
50 +Schedule scheduleStep_inv 1921
51 +Schedule scheduleStep_bijective 2200
52 +LocalCollision State.xor_zero 257
53 +LocalCollision Words9.xor_zero 289
54 +LocalCollision linStep_xor_absorb 4000
55 +LocalCollision linStep_xor_xor3 6368
56 +LocalCollision diff_absorb_b0 5049
57 +LocalCollision diff_absorb_b1 5049
58 +LocalCollision diff_absorb_b2 5049
59 +LocalCollision diff_absorb_b3 5049
60 +LocalCollision diff_xor3_b0 8769
61 +LocalCollision diff_xor3_b1 8769
62 +LocalCollision diff_xor3_b2 8769
63 +LocalCollision diff_xor3_b3 8769
64 +LocalCollision byte_split 33
added formal/scripts/gf2_rank.py +103 −0
@@ -0,0 +1,103 @@
1 +"""GF(2) rank / kernel of the SHA-256 sigma functions on 32-bit words.
2 +
3 +Each of Sigma0, Sigma1, sigma0, sigma1 is GF(2)-linear (XOR of rotations / shifts), hence a
4 +32x32 matrix over GF(2). This script computes the rank, the kernel dimension and an explicit
5 +kernel basis (every non-zero kernel vector is a counter-example to injectivity). It is used
6 +to decide which theorem to state in formal/Wp34/Sha256Words.lean before asking bv_decide.
7 +
8 +Run: .venv/bin/python formal/scripts/gf2_rank.py
9 +"""
10 +from __future__ import annotations
11 +import sys, os
12 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "src"))
13 +from wp34.sha256 import Sigma0, Sigma1, sigma0, sigma1, MASK # noqa: E402
14 +
15 +def columns(f):
16 + return [f(1 << i) & MASK for i in range(32)] # image of basis vector e_i
17 +
18 +def rank_and_kernel(f):
19 + cols = columns(f)
20 + # Gaussian elimination on the columns as 32-bit integers, tracking combinations for the kernel
21 + basis: list[tuple[int, int]] = [] # (pivot-reduced vector, combination mask over e_i)
22 + kernel: list[int] = []
23 + for i, v in enumerate(cols):
24 + comb = 1 << i
25 + for bv, bc in basis:
26 + if v & (1 << (bv.bit_length() - 1)):
27 + v ^= bv
28 + comb ^= bc
29 + if v == 0:
30 + kernel.append(comb)
31 + else:
32 + basis.append((v, comb))
33 + basis.sort(key=lambda t: -t[0])
34 + return len(basis), kernel
35 +
36 +def main():
37 + for name, f in [("Sigma0", Sigma0), ("Sigma1", Sigma1), ("sigma0", sigma0), ("sigma1", sigma1)]:
38 + r, ker = rank_and_kernel(f)
39 + print(f"{name}: rank = {r}, kernel dimension = {32 - r}")
40 + for k in ker:
41 + assert f(k) & MASK == 0, (name, hex(k))
42 + print(f" kernel vector x = {k:#010x} ({name}(x) = 0, x != 0)")
43 + if not ker:
44 + # check that a random pair with equal image does not exist (bijective)
45 + print(" injective (kernel trivial) -> bijective on 32 bits")
46 +
47 +if __name__ == "__main__" and "--lean" not in sys.argv:
48 + main()
49 +
50 +
51 +# ---- Lean code generation (explicit inverses, pasted into formal/Wp34/Sha256Words.lean) ----
52 +def inverse_columns(f):
53 + """Columns of the inverse matrix: inv(e_i) for i = 0..31 (Gauss-Jordan over GF(2))."""
54 + rows = [0] * 32
55 + for i in range(32):
56 + y = f(1 << i) & MASK
57 + for j in range(32):
58 + if (y >> j) & 1:
59 + rows[j] |= 1 << i
60 + I = [1 << j for j in range(32)]
61 + for bit in range(32):
62 + piv = next(i for i in range(bit, 32) if rows[i] >> bit & 1)
63 + rows[bit], rows[piv] = rows[piv], rows[bit]; I[bit], I[piv] = I[piv], I[bit]
64 + for i in range(32):
65 + if i != bit and rows[i] >> bit & 1:
66 + rows[i] ^= rows[bit]; I[i] ^= I[bit]
67 + assert rows == [1 << j for j in range(32)]
68 + def apply(x):
69 + return sum(((bin(I[j] & x).count("1") & 1) << j) for j in range(32))
70 + return [apply(1 << i) for i in range(32)], apply
71 +
72 +def rotation_set(apply):
73 + """If inv is a XOR of rotations, return the rotation amounts (checked), else None."""
74 + from wp34.sha256 import rotr
75 + inv1 = apply(1)
76 + S = sorted(((32 - k) % 32) for k in range(32) if inv1 >> k & 1)
77 + import random
78 + for _ in range(2000):
79 + x = random.getrandbits(32); y = 0
80 + for j in S: y ^= rotr(x, j)
81 + if y != apply(x):
82 + return None
83 + return S
84 +
85 +def lean_code():
86 + from wp34.sha256 import rotr
87 + out = []
88 + for name, f in [("Sigma0", Sigma0), ("Sigma1", Sigma1), ("sigma0", sigma0), ("sigma1", sigma1)]:
89 + cols, apply = inverse_columns(f)
90 + for _ in range(2000):
91 + x = __import__("random").getrandbits(32)
92 + assert apply(f(x) & MASK) == x and f(apply(x)) & MASK == x
93 + S = rotation_set(apply)
94 + if S is not None:
95 + body = " ^^^ ".join(f"rotr x {j}" for j in S)
96 + out.append(f"def {name}Inv (x : Word) : Word :=\n {body}")
97 + else:
98 + body = " ^^^\n ".join(f"bitMask x {i} &&& {c:#010x}#32" for i, c in enumerate(cols))
99 + out.append(f"def {name}Inv (x : Word) : Word :=\n {body}")
100 + return "\n\n".join(out)
101 +
102 +if __name__ == "__main__" and "--lean" in sys.argv:
103 + print(lean_code())
added formal/scripts/local_collision.py +77 −0
@@ -0,0 +1,77 @@
1 +"""Python side of formal/Wp34/LocalCollision.lean.
2 +
3 +1. Simulates two GF(2)-linearised SHA-256 step functions and checks, on random inputs, the
4 + 9-step local-collision correction vectors that are then proven for ALL inputs by bv_decide:
5 + * model "absorb" : Ch and Maj replaced by the zero function (Gilbert-Handschuh 2003
6 + conditions "the Boolean functions absorb the difference"), '+' replaced by XOR;
7 + * model "xor3" : Ch and Maj replaced by x ^ y ^ z (Chabaud-Joux 1998 style linearisation),
8 + '+' replaced by XOR.
9 +2. Finds an explicit counter-example showing that the "absorb" correction vector is NOT a
10 + collision for the real (modular, non-linear) step function -- the linearised local collision
11 + is a differential characteristic that holds only with some probability on the real function.
12 +
13 +Run: .venv/bin/python formal/scripts/local_collision.py
14 +"""
15 +from __future__ import annotations
16 +import os, random, sys
17 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "src"))
18 +from wp34.sha256 import Sigma0, Sigma1, Ch, Maj, step, MASK # noqa: E402
19 +
20 +def lin_step(s, k, w, boolf):
21 + a, b, c, d, e, f, g, h = s
22 + T1 = h ^ Sigma1(e) ^ boolf(e, f, g) ^ k ^ w
23 + T2 = Sigma0(a) ^ boolf(a, b, c)
24 + return (T1 ^ T2, a, b, c, d ^ T1, e, f, g)
25 +
26 +ZERO = lambda x, y, z: 0
27 +XOR3 = lambda x, y, z: x ^ y ^ z
28 +
29 +def corrections_absorb(d):
30 + return [d, Sigma1(d) ^ Sigma0(d), Sigma1(Sigma0(d)), 0, d, Sigma0(d) ^ Sigma1(d), 0, 0, d]
31 +
32 +def corrections_xor3(d):
33 + S0, S1 = Sigma0(d), Sigma1(d)
34 + return [d, S1 ^ S0, Sigma1(S0 ^ d) ^ S0 ^ d, S1 ^ S0, S1 ^ S0, S0 ^ S1, d, 0, d]
35 +
36 +def run9(s, K, W, stepf):
37 + for t in range(9):
38 + s = stepf(s, K[t], W[t])
39 + return s
40 +
41 +def main():
42 + rng = random.Random(34)
43 + for name, boolf, corr in [("absorb", ZERO, corrections_absorb), ("xor3", XOR3, corrections_xor3)]:
44 + for _ in range(5000):
45 + s = tuple(rng.getrandbits(32) for _ in range(8))
46 + K = [rng.getrandbits(32) for _ in range(9)]
47 + W = [rng.getrandbits(32) for _ in range(9)]
48 + d = rng.getrandbits(32)
49 + W2 = [w ^ c for w, c in zip(W, corr(d))]
50 + f = lambda st, k, w: lin_step(st, k, w, boolf)
51 + assert run9(s, K, W, f) == run9(s, K, W2, f), (name, s, K, W, d)
52 + print(f"model {name}: 9-step local collision holds on 5000 random (state, K, W, delta) -- "
53 + "the universal statement is proven by bv_decide in Lean")
54 +
55 + # Counter-example for the real step function (same XOR corrections, "absorb" vector).
56 + s0 = (0,) * 8; K0 = [0] * 9; W0 = [0] * 9; d = 1
57 + W1 = [w ^ c for w, c in zip(W0, corrections_absorb(d))]
58 + out0, out1 = run9(s0, K0, W0, step), run9(s0, K0, W1, step)
59 + print("real step, s = 0, K = 0, W = 0, delta = 1 :")
60 + print(" corrected W' =", [f"{w:#010x}" for w in W1])
61 + print(" out(W) =", [f"{w:#010x}" for w in out0])
62 + print(" out(W') =", [f"{w:#010x}" for w in out1])
63 + print(" collision on the real step?", out0 == out1)
64 + # how often does the characteristic hold on the real step for random inputs with 1-bit delta?
65 + hits = 0; N = 20000
66 + for _ in range(N):
67 + s = tuple(rng.getrandbits(32) for _ in range(8))
68 + K = [rng.getrandbits(32) for _ in range(9)]
69 + W = [rng.getrandbits(32) for _ in range(9)]
70 + d = 1 << rng.randrange(32)
71 + W2 = [w ^ c for w, c in zip(W, corrections_absorb(d))]
72 + hits += run9(s, K, W, step) == run9(s, K, W2, step)
73 + print(f" real step, random inputs, 1-bit delta, XOR corrections: {hits}/{N} collisions "
74 + f"(the XOR characteristic is a low-probability event on the modular function)")
75 +
76 +if __name__ == "__main__":
77 + main()
added hypotheses/H01-standardised-secp256k1-parameters-and-glv-consta.md +29 −0
@@ -0,0 +1,29 @@
1 +---
2 +id: H01
3 +title: Standardised secp256k1 parameters and GLV constants are exactly consistent
4 +axis: A
5 +kind: algebraic
6 +statement: p and n are prime, G ∈ E, [n]G = O, h = 1, β³ ≡ 1 (mod p), λ³ ≡ 1 (mod n), φ(G) = (βG_x, G_y) = [λ]G, the libsecp256k1
7 + lattice basis satisfies a_i + b_i λ ≡ 0 (mod n) and has determinant n.
8 +motivation: Every later result depends on these constants; an error here would invalidate the study.
9 +assumptions: SEC 2 v2.0 parameter values; libsecp256k1 scalar_impl.h constants.
10 +expected_contribution: Independent two-implementation verification (pure Python and PARI/GP).
11 +related_work:
12 +- certicom2010sec2
13 +- gallant2001faster
14 +- wuille2013libsecp256k1
15 +possible_counterexamples: A transcription error in any constant; a wrong pairing of β with λ (β with λ² fails).
16 +verification_strategy: 'experiments/baselines/secp256k1_facts.py: 14 named facts computed in Python and PARI/GP must agree.'
17 +computational_budget: seconds
18 +status: evaluated
19 +classification: known_reproduced
20 +evidence:
21 +- results/baselines/secp256k1_facts.json
22 +verdict: All 14 facts agree between Python and PARI/GP; (β, λ²) mismatch confirmed as expected.
23 +commands:
24 +- .venv/bin/python experiments/baselines/secp256k1_facts.py
25 +---
26 +
27 +# H01 — Standardised secp256k1 parameters and GLV constants are exactly consistent
28 +
29 +p and n are prime, G ∈ E, [n]G = O, h = 1, β³ ≡ 1 (mod p), λ³ ≡ 1 (mod n), φ(G) = (βG_x, G_y) = [λ]G, the libsecp256k1 lattice basis satisfies a_i + b_i λ ≡ 0 (mod n) and has determinant n.
added hypotheses/H02-end-e----z-\317\211--and-the-glv-basis-is-the-eisenstei.md +35 −0
@@ -0,0 +1,35 @@
1 +---
2 +id: H02
3 +title: 'End(E) = Z[ω] and the GLV basis is the Eisenstein ideal (π̄ − 1): closed form of the libsecp256k1 constants'
4 +axis: A
5 +kind: algebraic
6 +statement: 'For secp256k1, t² − 4p = −3f² with f integral; End(E) = Z[ω]; the GLV lattice {(k1,k2): k1 + k2 λ ≡ 0 (mod n)}
7 + is the principal ideal (π̄−1)Z[ω] and a1 = (t−f)/2 − 1, b1 = −f, a2 = a1 − b1, b2 = a1, with a1² − a1 b1 + b1² = n.'
8 +motivation: 'Research question A1: are there efficiently computable endomorphisms beyond the automorphisms? CM theory says
9 + no; the closed form explains the ''magic'' constants.'
10 +assumptions: E ordinary, p ≡ 1 (mod 3), j = 0.
11 +expected_contribution: Explanation of constants usually presented as output of the extended Euclidean algorithm; proof that
12 + no endomorphism outside Z[φ] exists.
13 +related_work:
14 +- gallant2001faster
15 +- sica2002analysis
16 +- galbraith2011endomorphisms
17 +- silverman2009arithmetic
18 +possible_counterexamples: If a1 ≠ (t−f)/2 − 1 exactly, the ideal description would be wrong (it was checked exactly).
19 +verification_strategy: Exact integer check of the closed form; identity search Q1/Q3; extended-Euclid re-derivation.
20 +computational_budget: seconds
21 +status: evaluated
22 +classification: known_reproduced
23 +evidence:
24 +- results/automated_discovery/identity_search.json
25 +- results/baselines/secp256k1_facts.json
26 +verdict: Closed form holds exactly; all 22 identities found by the search are explained by CM theory. Known in principle (ideal
27 + structure of the GLV lattice), the explicit identification with the published constants is a useful clarification, not new
28 + mathematics.
29 +commands:
30 +- .venv/bin/python experiments/automated_discovery/identity_search.py
31 +---
32 +
33 +# H02 — End(E) = Z[ω] and the GLV basis is the Eisenstein ideal (π̄ − 1): closed form of the libsecp256k1 constants
34 +
35 +For secp256k1, t² − 4p = −3f² with f integral; End(E) = Z[ω]; the GLV lattice {(k1,k2): k1 + k2 λ ≡ 0 (mod n)} is the principal ideal (π̄−1)Z[ω] and a1 = (t−f)/2 − 1, b1 = −f, a2 = a1 − b1, b2 = a1, with a1² − a1 b1 + b1² = n.
added hypotheses/H03-glv-decomposition-halves-doublings.md +30 −0
@@ -0,0 +1,30 @@
1 +---
2 +id: H03
3 +title: GLV decomposition halves doublings; sub-scalars never exceed 128 bits
4 +axis: A
5 +kind: algebraic
6 +statement: With the published basis, |k1|, |k2| < 2^128 for every k, and [k]P via interleaved wNAF uses ≈ 128 doublings instead
7 + of 256, giving ≈ 1.65× fewer group operations than wNAF alone.
8 +motivation: Quantify the known constant-factor gain exactly in operation counts (A3).
9 +assumptions: Affine operation model; wNAF window 4–5.
10 +expected_contribution: Exact operation counts for the paper's baseline table.
11 +related_work:
12 +- gallant2001faster
13 +- sica2002analysis
14 +- hankerson2004guide
15 +possible_counterexamples: A scalar with a 129-bit component (none found in 2 200 samples; the Sica–Ciet–Quisquater bound guarantees
16 + ≤ 128 + small).
17 +verification_strategy: experiments/baselines/scalar_mult_opcount.py on 200 random scalars, all methods return the same point.
18 +computational_budget: seconds
19 +status: evaluated
20 +classification: known_reproduced
21 +evidence:
22 +- results/baselines/scalar_mult_opcount.json
23 +verdict: Max sub-scalar size 128 bits; 184 vs 304 group ops (GLV+wNAF vs wNAF), 2.07× vs double-and-add.
24 +commands:
25 +- .venv/bin/python experiments/baselines/scalar_mult_opcount.py
26 +---
27 +
28 +# H03 — GLV decomposition halves doublings; sub-scalars never exceed 128 bits
29 +
30 +With the published basis, |k1|, |k2| < 2^128 for every k, and [k]P via interleaved wNAF uses ≈ 128 doublings instead of 256, giving ≈ 1.65× fewer group operations than wNAF alone.
added hypotheses/H04-automorphism-orbits-give-exactly--6-on-pollard-r.md +33 −0
@@ -0,0 +1,33 @@
1 +---
2 +id: H04
3 +title: Automorphism orbits give exactly √6 on Pollard rho, with measurable fruitless-cycle overhead
4 +axis: A
5 +kind: algebraic
6 +statement: On synthetic j = 0 prime-order curves, rho on classes of size 6 needs √6 fewer iterations than plain rho (and √2
7 + for the negation map alone), up to fruitless-cycle handling costs.
8 +motivation: Research question A2 (equivalence classes reduce redundant computation).
9 +assumptions: Random-mapping heuristic; Teske r-adding walk r = 20.
10 +expected_contribution: Precise measurement of the achieved versus theoretical factor and of the cycle overhead.
11 +related_work:
12 +- wiener1999faster
13 +- duursma1999speeding
14 +- gallant2000improving
15 +- bos2010negation
16 +- bernstein2011correct
17 +possible_counterexamples: Achieved factor materially below √6 because of fruitless cycles or canonicalisation cost.
18 +verification_strategy: 'Rust engine: rho_dp1 vs rho_dp1_neg vs rho_dp1_aut3 vs rho_dp1_neg_aut3 on 20 j = 0 curves per size.'
19 +computational_budget: ≈ 1 node-hour
20 +status: evaluated
21 +classification: known_reproduced
22 +evidence:
23 +- results/synthetic_ecdlp/scaling_j0_M2U64.json
24 +- results/synthetic_ecdlp/analysis.json
25 +verdict: '180 paired instances at 44–52 bits: iteration speed-up 2.21 [2.00, 2.45] (r = 128) and 2.42 [2.19, 2.70] (r = 20)
26 + vs √6 = 2.449; constant 0.499·√n vs √(π/12) = 0.512; negation alone 1.29 vs √2, φ-orbits alone 1.52 vs √3; fruitless cycles
27 + ≤ 0.11 per 1000 iterations. Wall-clock speed-up only 0.95 (order-6) and 0.73 (negation) because canonicalisation costs 56–83
28 + ns vs 38 ns per plain iteration in this engine.'
29 +---
30 +
31 +# H04 — Automorphism orbits give exactly √6 on Pollard rho, with measurable fruitless-cycle overhead
32 +
33 +On synthetic j = 0 prime-order curves, rho on classes of size 6 needs √6 fewer iterations than plain rho (and √2 for the negation map alone), up to fruitless-cycle handling costs.
added hypotheses/H05-the-automorphism-orbit-set-cannot-be-made-a-grou.md +29 −0
@@ -0,0 +1,29 @@
1 +---
2 +id: H05
3 +title: The automorphism orbit set cannot be made a group of order n/6 (rejected conjecture)
4 +axis: A
5 +kind: algebraic
6 +statement: 'Conjecture: the quotient E(F_p)/Aut(E) inherits a group law making the DLP a problem of size n/6. Reality: the
7 + orbit relation is not a congruence (P ~ φP does not imply P + Q ~ φP + Q).'
8 +motivation: A natural 'beyond √6' idea; must be ruled out explicitly (A5).
9 +assumptions: Aut(E) ≅ Z/6 acting by automorphisms fixing O.
10 +expected_contribution: Negative result with proof and computed counterexamples.
11 +related_work:
12 +- wiener1999faster
13 +- silverman2009arithmetic
14 +possible_counterexamples: 'None: the statement is refuted by a single pair.'
15 +verification_strategy: 'theory/elliptic_curves/orbit_quotient_counterexample.py: 500 random pairs on 16/20/24-bit curves.'
16 +computational_budget: seconds
17 +status: evaluated
18 +classification: rejected
19 +evidence:
20 +- results/theory/orbit_quotient_counterexample.json
21 +verdict: 'Compatibility fails for 1500/1500 random pairs; proof: a congruence would make the orbit of O a subgroup H with
22 + cosets the other orbits, but orbits have size 6 ≠ |H| = 1.'
23 +commands:
24 +- .venv/bin/python theory/elliptic_curves/orbit_quotient_counterexample.py
25 +---
26 +
27 +# H05 — The automorphism orbit set cannot be made a group of order n/6 (rejected conjecture)
28 +
29 +Conjecture: the quotient E(F_p)/Aut(E) inherits a group law making the DLP a problem of size n/6. Reality: the orbit relation is not a congruence (P ~ φP does not imply P + Q ~ φP + Q).
added hypotheses/H06-no-known-algebraic-attack-applies-to-secp256k1.md +38 −0
@@ -0,0 +1,38 @@
1 +---
2 +id: H06
3 +title: No known algebraic attack applies to secp256k1; prime-field index calculus with m = 2 costs ≫ √n
4 +axis: A
5 +kind: algebraic
6 +statement: n ≠ p; the embedding degree exceeds 10^6 (in fact it is the full order of p mod n); the twist has a 220-bit prime
7 + cofactor; x³ + 7 has no roots (no 2-torsion) and 7 is a non-residue (no x = 0 points); Semaev decomposition with |F| ≈ √p
8 + succeeds for a random point with probability 1 − exp(−|F|²/2n) but the cost of a relation is |F|/rate ≫ √n per relation
9 + and |F| relations are needed.
10 +motivation: 'Section 3.2 of the mandate: applicability limits of algebraic attacks.'
11 +assumptions: Standard reductions (MOV/Frey–Rück, Semaev/Smart/Satoh–Araki, Semaev 2004).
12 +expected_contribution: Exact verification table plus an empirical decomposition experiment on 16–28-bit curves.
13 +related_work:
14 +- menezes1993reducing
15 +- frey1994remark
16 +- smart1999discrete
17 +- semaev2004summation
18 +- gaudry2009index
19 +- diem2011discrete
20 +- petit2012elliptic
21 +- bernstein2014safecurves
22 +possible_counterexamples: A small embedding degree or anomalous order (both excluded exactly).
23 +verification_strategy: secp256k1_facts.py (exact) and experiments/synthetic_ecdlp/index_calculus_probe.py.
24 +computational_budget: seconds
25 +status: evaluated
26 +classification: known_reproduced
27 +evidence:
28 +- results/baselines/secp256k1_facts.json
29 +- results/synthetic_ecdlp/index_calculus_probe.json
30 +verdict: All exclusion criteria verified; measured decomposition rates match the Poisson prediction within sampling error;
31 + even with a free decomposition oracle the relation stage costs ≥ 2.4√n and the linear algebra |F|² ≫ √n.
32 +commands:
33 +- .venv/bin/python experiments/synthetic_ecdlp/index_calculus_probe.py
34 +---
35 +
36 +# H06 — No known algebraic attack applies to secp256k1; prime-field index calculus with m = 2 costs ≫ √n
37 +
38 +n ≠ p; the embedding degree exceeds 10^6 (in fact it is the full order of p mod n); the twist has a 220-bit prime cofactor; x³ + 7 has no roots (no 2-torsion) and 7 is a non-residue (no x = 0 points); Semaev decomposition with |F| ≈ √p succeeds for a random point with probability 1 − exp(−|F|²/2n) but the cost of a relation is |F|/rate ≫ √n per relation and |F| relations are needed.
added hypotheses/H07-an-exhaustive-small-coefficient-identity-search.md +31 −0
@@ -0,0 +1,31 @@
1 +---
2 +id: H07
3 +title: An exhaustive small-coefficient identity search finds only CM-explained relations and no cross-modulus or generator-specific
4 + relation
5 +axis: A
6 +kind: algebraic
7 +statement: Among 58 064 candidate relations (binary quadratic forms, modular quadratics, GLV linear relations, cross-modulus
8 + and generator relations), every identity found is implied by β, λ being cube roots of unity and by the Eisenstein-ideal
9 + structure; the negative-control spaces Q4/Q5 return 0 hits.
10 +motivation: 'Stage 2–3 automation: can a dumb generator rediscover structure, and does it hallucinate structure where there
11 + is none?'
12 +assumptions: Coefficient bound 6; constant set {p, n, t, f, a1, b1, a2, b2, β, λ, Gx, Gy}.
13 +expected_contribution: Calibration of the discovery engine (true positives explained, false positives zero).
14 +related_work:
15 +- gallant2001faster
16 +- sica2002analysis
17 +possible_counterexamples: A relation involving Gx or Gy would indicate a non-random generator (none).
18 +verification_strategy: experiments/automated_discovery/identity_search.py
19 +computational_budget: seconds
20 +status: evaluated
21 +classification: empirical_observation
22 +evidence:
23 +- results/automated_discovery/identity_search.json
24 +verdict: 22 identities found, 22 explained, 0 unexplained, 0 false positives (expected chance hits 10^-73).
25 +commands:
26 +- .venv/bin/python experiments/automated_discovery/identity_search.py
27 +---
28 +
29 +# H07 — An exhaustive small-coefficient identity search finds only CM-explained relations and no cross-modulus or generator-specific relation
30 +
31 +Among 58 064 candidate relations (binary quadratic forms, modular quadratics, GLV linear relations, cross-modulus and generator relations), every identity found is implied by β, λ being cube roots of unity and by the Eisenstein-ideal structure; the negative-control spaces Q4/Q5 return 0 hits.
added hypotheses/H08-measured-ecdlp-cost-scales-as-n-0-5-with-constan.md +36 −0
@@ -0,0 +1,36 @@
1 +---
2 +id: H08
3 +title: Measured ECDLP cost scales as n^0.5 with constants matching random-mapping theory
4 +axis: B
5 +kind: algorithmic
6 +statement: Fitting T(n) = a n^α (log n)^β to measured group-operation counts for BSGS, rho (Floyd/Brent/DP) and kangaroo on
7 + 20–48-bit prime-order curves gives α = 0.50 ± 0.02 and constants ≈ 2 (BSGS), ≈ √(π/2) ≈ 1.25 (rho), ≈ 2 (kangaroo on the
8 + full interval).
9 +motivation: Reproducible baseline before any novel algorithm (mandate §9).
10 +assumptions: Teske r = 20 walk; distinguished-point fraction tuned per size.
11 +expected_contribution: Baseline table and log–log scaling figure with bootstrap confidence intervals.
12 +related_work:
13 +- pollard1978monte
14 +- teske2001random
15 +- vanoorschot1999parallel
16 +- shanks1971class
17 +- pollard2000kangaroos
18 +- flajolet1990random
19 +possible_counterexamples: α significantly different from 0.5, or a rho constant below √(π/2) (which would contradict the random-mapping
20 + model).
21 +verification_strategy: experiments/synthetic_ecdlp/run_scaling.py on the cluster; analysis script fits and bootstraps.
22 +computational_budget: ≈ 4 node-hours
23 +status: evaluated
24 +classification: known_reproduced
25 +evidence:
26 +- results/synthetic_ecdlp/scaling_generic_M3U96a.json
27 +- results/synthetic_ecdlp/scaling_j0_M2U64.json
28 +- results/synthetic_ecdlp/analysis.json
29 +verdict: '3 840 verified instances, 20–56 bits: fitted α ∈ [0.478, 0.500] (≥ 32 bits) for all seven configurations with CIs
30 + touching 0.5; pooled rho constant 1.254·√n [1.21, 1.30] on 480 runs vs √(π/2) = 1.2533; BSGS 1.50·√n (x-keyed ±j table);
31 + kangaroo 1.94·√n; Floyd 2.67·√n, Brent 1.65·√n.'
32 +---
33 +
34 +# H08 — Measured ECDLP cost scales as n^0.5 with constants matching random-mapping theory
35 +
36 +Fitting T(n) = a n^α (log n)^β to measured group-operation counts for BSGS, rho (Floyd/Brent/DP) and kangaroo on 20–48-bit prime-order curves gives α = 0.50 ± 0.02 and constants ≈ 2 (BSGS), ≈ √(π/2) ≈ 1.25 (rho), ≈ 2 (kangaroo on the full interval).
added hypotheses/H09-automated-search-over-iteration-function-designs.md +32 −0
@@ -0,0 +1,32 @@
1 +---
2 +id: H09
3 +title: Automated search over iteration-function designs cannot beat the random-mapping constant for a given class size
4 +axis: B
5 +kind: algorithmic
6 +statement: Across r ∈ {3, 8, 16, 20, 32, 64}, mixed walks, and DP densities, no configuration achieves expected iterations
7 + below √(π n/(2·|class|)) by more than statistical noise; small r is measurably worse (Teske).
8 +motivation: 'Research question A4/B4: can automated algorithm search discover a better generic iteration?'
9 +assumptions: Walks are deterministic functions of the current point (generic-group restriction).
10 +expected_contribution: 'Negative result consistent with Shoup''s bound: the design space contains no asymptotic shortcut;
11 + constant-factor ranking of designs.'
12 +related_work:
13 +- teske2001random
14 +- shoup1997lower
15 +- cheon2012speeding
16 +- bernstein2012two
17 +possible_counterexamples: A configuration with constant < 1.25 at all sizes (would require explanation).
18 +verification_strategy: Rust engine sweep on 32–40-bit curves, 40 instances per configuration.
19 +computational_budget: ≈ 2 node-hours
20 +status: evaluated
21 +classification: rejected
22 +evidence:
23 +- results/synthetic_ecdlp/sweep_r_M3U96a.json
24 +- results/synthetic_ecdlp/analysis.json
25 +verdict: '25 configurations × 80 instances: r = 2 and r = 3 walks cost 3.77·√n and 2.26·√n (negative control), r ≥ 8 cluster
26 + at 1.1–1.5·√n with CIs overlapping √(π/2); mixed walks indistinguishable; best cell r32 at 1.10 [0.92, 1.28] not significantly
27 + below the random-mapping constant. No generic iteration design beats the random-mapping floor.'
28 +---
29 +
30 +# H09 — Automated search over iteration-function designs cannot beat the random-mapping constant for a given class size
31 +
32 +Across r ∈ {3, 8, 16, 20, 32, 64}, mixed walks, and DP densities, no configuration achieves expected iterations below √(π n/(2·|class|)) by more than statistical noise; small r is measurably worse (Teske).
added hypotheses/H10-parallel-distinguished-point-collision-search-ha.md +27 −0
@@ -0,0 +1,27 @@
1 +---
2 +id: H10
3 +title: Parallel distinguished-point collision search has near-linear speed-up up to 28 threads
4 +axis: B
5 +kind: algorithmic
6 +statement: Wall-clock speed-up S(t) ≥ 0.8 t for t ≤ 28 threads on a Mac Studio M3 Ultra at 44–48 bits, with total group operations
7 + independent of t up to the DP overhead.
8 +motivation: Measure parallel efficiency (mandate §4).
9 +assumptions: Shared DP table; one walk per thread.
10 +expected_contribution: Parallel-efficiency figure.
11 +related_work:
12 +- vanoorschot1999parallel
13 +possible_counterexamples: Lock contention or memory bandwidth limiting S(t).
14 +verification_strategy: Rust engine --threads sweep on M3U96a/M3U96b.
15 +computational_budget: ≈ 1 node-hour
16 +status: evaluated
17 +classification: known_reproduced
18 +evidence:
19 +- results/synthetic_ecdlp/parallel_M3U96a.json
20 +- results/synthetic_ecdlp/analysis.json
21 +verdict: '56-bit instances on an Apple M3 Ultra: 14.8× on 16 threads (efficiency 0.93), 22.1× on 28 threads (0.79, P+E cores);
22 + total group operations within a factor 1.0 of single-thread.'
23 +---
24 +
25 +# H10 — Parallel distinguished-point collision search has near-linear speed-up up to 28 threads
26 +
27 +Wall-clock speed-up S(t) ≥ 0.8 t for t ≤ 28 threads on a Mac Studio M3 Ultra at 44–48 bits, with total group operations independent of t up to the DP overhead.
added hypotheses/H11-time-memory-trade-off.md +29 −0
@@ -0,0 +1,29 @@
1 +---
2 +id: H11
3 +title: 'Time–memory trade-off: BSGS with memory m costs m + n/m operations; rho with DP density θ stores ≈ θ·√(πn/2) points'
4 +axis: B
5 +kind: algorithmic
6 +statement: Measured (time, memory) points for BSGS at m ∈ {n^0.25 … n^0.5} and for DP-rho at several θ lie on the predicted
7 + curves; BSGS is deterministic and rho has coefficient of variation ≈ 0.52 (Rayleigh).
8 +motivation: 'Mandate §9: report the time–memory trade-off for each algorithm.'
9 +assumptions: Hash-table memory counted as entries × bytes.
10 +expected_contribution: Trade-off figure.
11 +related_work:
12 +- shanks1971class
13 +- vanoorschot1999parallel
14 +- bernstein2012two
15 +possible_counterexamples: Deviation from m + n/m (would indicate an implementation error).
16 +verification_strategy: Rust engine bsgs --memory sweep; rho --dp sweep.
17 +computational_budget: ≈ 1 node-hour
18 +status: evaluated
19 +classification: known_reproduced
20 +evidence:
21 +- results/synthetic_ecdlp/tmto_M2U64.json
22 +- results/synthetic_ecdlp/analysis.json
23 +verdict: BSGS cost matches m + n/(2m) within 7.8 % for m from 2^10 to 2^20 at 36 bits; DP-rho at 40 bits spans 1.17–4.95 M
24 + operations as the DP density goes from 2^-12 (optimum, ≈ √(πn/2)) to 2^-2 (restart-dominated) and 2^-14 (overshoot-dominated).
25 +---
26 +
27 +# H11 — Time–memory trade-off: BSGS with memory m costs m + n/m operations; rho with DP density θ stores ≈ θ·√(πn/2) points
28 +
29 +Measured (time, memory) points for BSGS at m ∈ {n^0.25 … n^0.5} and for DP-rho at several θ lie on the predicted curves; BSGS is deterministic and rho has coefficient of variation ≈ 0.52 (Rayleigh).
added hypotheses/H12-discrete-logarithms-with-preprocessing.md +30 −0
@@ -0,0 +1,30 @@
1 +---
2 +id: H12
3 +title: 'Discrete logarithms with preprocessing: online cost ≈ 1.5·√(n/T) after storing T distinguished points'
4 +axis: B
5 +kind: algorithmic
6 +statement: With T stored DPs produced by walks of length W = √(n/T), the online phase needs c·√(n/T) group operations with
7 + c ≈ 1.5, i.e. a per-instance speed-up of ≈ √T/1.5 over plain rho, consistent with ST² = Ω̃(n).
8 +motivation: Non-uniform attacks are the one known way to go below √n per instance; measure the constant.
9 +assumptions: Walk independent of the target (M_j = u_j G); DP mask ≈ log₂ W bits.
10 +expected_contribution: Quantified reproduction of Bernstein–Lange on 22–26-bit groups (Python) and larger (Rust).
11 +related_work:
12 +- bernstein2013nonuniform
13 +- lee2011generic
14 +- corrigangibbs2018discrete
15 +possible_counterexamples: Online cost not decreasing as 1/√T.
16 +verification_strategy: experiments/synthetic_ecdlp/preprocessing_python.py (60 instances per cell).
17 +computational_budget: ≈ 10 min laptop
18 +status: evaluated
19 +classification: known_reproduced
20 +evidence:
21 +- results/synthetic_ecdlp/preprocessing_python.json
22 +verdict: Online/√(n/T) ratio 1.48–1.67 across T = 2^4 … 2^12 at 22 and 26 bits; per-instance speed-up 2.6× → 42× as predicted
23 + by √T scaling.
24 +commands:
25 +- .venv/bin/python experiments/synthetic_ecdlp/preprocessing_python.py
26 +---
27 +
28 +# H12 — Discrete logarithms with preprocessing: online cost ≈ 1.5·√(n/T) after storing T distinguished points
29 +
30 +With T stored DPs produced by walks of length W = √(n/T), the online phase needs c·√(n/T) group operations with c ≈ 1.5, i.e. a per-instance speed-up of ≈ √T/1.5 over plain rho, consistent with ST² = Ω̃(n).
added hypotheses/H13-bip340-batch-verification-reduces-per-signature.md +33 −0
@@ -0,0 +1,33 @@
1 +---
2 +id: H13
3 +title: BIP340 batch verification reduces per-signature cost by a factor that grows with batch size and saturates
4 +axis: B
5 +kind: algorithmic
6 +statement: Verifying b signatures with random 128-bit weights and a multi-scalar multiplication costs per signature a decreasing
7 + function of b, with speed-up ≈ 2× at b = 16 and ≈ 3× or more at b ≥ 256, without changing consensus semantics.
8 +motivation: Research question B2.
9 +assumptions: Random weights; lift_x cost included.
10 +expected_contribution: Measured curve with CI on two hardware platforms; operation counts of the MSM.
11 +related_work:
12 +- bernstein2012high
13 +- wuille2020bip340
14 +- naccache1994can
15 +- bellare1998fast
16 +- pippenger1980evaluation
17 +possible_counterexamples: No speed-up (would indicate an MSM implementation issue).
18 +verification_strategy: experiments/signature_benchmarks/sigbench (k256 arithmetic), negative control with one invalid signature.
19 +computational_budget: ≈ 30 min
20 +status: evaluated
21 +classification: known_reproduced
22 +evidence:
23 +- results/signature_benchmarks/summary.json
24 +- results/signature_benchmarks/bench_MacBookPro-de-simon-pierre-2.json
25 +- results/signature_benchmarks/bench_MacStudnpierre2.json
26 +verdict: Per-signature cost falls from 24.5 µs (single, k256) to 7.9 µs at b = 2048 with Pippenger (S = 3.10 [3.05, 3.16];
27 + 1.67× vs libsecp256k1 single verify); Straus plateaus at ≈ 2×; ≈ 3 µs/sig of non-MSM work (lift_x, tagged hash) caps the
28 + gain; negative control rejected; same shape on M3 Ultra.
29 +---
30 +
31 +# H13 — BIP340 batch verification reduces per-signature cost by a factor that grows with batch size and saturates
32 +
33 +Verifying b signatures with random 128-bit weights and a multi-scalar multiplication costs per signature a decreasing function of b, with speed-up ≈ 2× at b = 16 and ≈ 3× or more at b ≥ 256, without changing consensus semantics.
added hypotheses/H14-implementation-choice-dominates-signature-perfor.md +28 −0
@@ -0,0 +1,28 @@
1 +---
2 +id: H14
3 +title: 'Implementation choice dominates signature performance: three orders of magnitude between pure Python and libsecp256k1'
4 +axis: B
5 +kind: algorithmic
6 +statement: ECDSA/Schnorr verification spans ≈ 10³× between the pure-Python reference and libsecp256k1, with k256 within a
7 + small factor of libsecp256k1; sign and verify costs differ by the cost of the second scalar multiplication.
8 +motivation: Research question B5.
9 +assumptions: Single-threaded, prehashed messages.
10 +expected_contribution: Implementation table.
11 +related_work:
12 +- wuille2013libsecp256k1
13 +- bernstein2012high
14 +possible_counterexamples: None (descriptive).
15 +verification_strategy: sigbench + run_bench.py on laptop and M3U96a.
16 +computational_budget: ≈ 30 min
17 +status: evaluated
18 +classification: empirical_observation
19 +evidence:
20 +- results/signature_benchmarks/summary.json
21 +verdict: 'Schnorr verify: 7.4 ms (pure Python) vs 24.5 µs (k256) vs 13.2 µs (libsecp256k1): 560× spread; libsecp256k1 verifies
22 + 1.9× faster than k256 while k256 signs 1.8× faster (table-based generator multiplication 8.3 µs vs blinded ecmult_gen 19.8
23 + µs); ECDSA and Schnorr verification within 5 % in libsecp256k1.'
24 +---
25 +
26 +# H14 — Implementation choice dominates signature performance: three orders of magnitude between pure Python and libsecp256k1
27 +
28 +ECDSA/Schnorr verification spans ≈ 10³× between the pure-Python reference and libsecp256k1, with k256 within a small factor of libsecp256k1; sign and verify costs differ by the cost of the second scalar multiplication.
added hypotheses/H15-a-single-bit-message-difference-reaches-full-ava.md +25 −0
@@ -0,0 +1,25 @@
1 +---
2 +id: H15
3 +title: A single-bit message difference reaches full avalanche within a small number of steps
4 +axis: C
5 +kind: sha256
6 +statement: The mean fraction of flipped state bits after s steps reaches 0.49 within s* steps of the compression function
7 + for a single-bit difference in W_0 (s* to be measured) and the random-oracle control is at 0.5 from step 1.
8 +motivation: Research question C1 (regularities of reduced rounds).
9 +assumptions: 2000 random messages per configuration.
10 +expected_contribution: Diffusion heatmap and s*.
11 +related_work:
12 +- nist2015fips180
13 +- gilbert2003security
14 +possible_counterexamples: Flip ratio plateauing below 0.5 (would indicate a structural weakness).
15 +verification_strategy: experiments/reduced_round_sha256/diffusion.py
16 +computational_budget: minutes
17 +status: running
18 +classification: pending
19 +evidence: []
20 +verdict: pending
21 +---
22 +
23 +# H15 — A single-bit message difference reaches full avalanche within a small number of steps
24 +
25 +The mean fraction of flipped state bits after s steps reaches 0.49 within s* steps of the compression function for a single-bit difference in W_0 (s* to be measured) and the random-oracle control is at 0.5 from step 1.
added hypotheses/H16-statistically-detectable-input-output-correlatio.md +25 −0
@@ -0,0 +1,25 @@
1 +---
2 +id: H16
3 +title: Statistically detectable input–output correlations vanish after a small number of steps
4 +axis: C
5 +kind: sha256
6 +statement: With N = 20 000 samples, the number of (input bit, output bit) pairs with |corr| > 4/√N is large for s ≤ 4 and
7 + zero for s ≥ s** (to be measured), identical to the random-oracle control; output-byte chi-square tests are uniform beyond
8 + s**.
9 +motivation: C1/C3.
10 +assumptions: Single-block random messages.
11 +expected_contribution: Bias figure.
12 +related_work:
13 +- nist2015fips180
14 +possible_counterexamples: Residual correlations at 64 steps.
15 +verification_strategy: experiments/reduced_round_sha256/bias_test.py
16 +computational_budget: minutes
17 +status: running
18 +classification: pending
19 +evidence: []
20 +verdict: pending
21 +---
22 +
23 +# H16 — Statistically detectable input–output correlations vanish after a small number of steps
24 +
25 +With N = 20 000 samples, the number of (input bit, output bit) pairs with |corr| > 4/√N is large for s ≤ 4 and zero for s ≥ s** (to be measured), identical to the random-oracle control; output-byte chi-square tests are uniform beyond s**.
added hypotheses/H17-sat-preimage-search-on-step-reduced-sha-256-scal.md +28 −0
@@ -0,0 +1,28 @@
1 +---
2 +id: H17
3 +title: SAT preimage search on step-reduced SHA-256 scales exponentially in the number of steps and in the number of free bits
4 +axis: C
5 +kind: sha256
6 +statement: For fixed free message bits k, CaDiCaL/Kissat time grows roughly exponentially with the step count s, becoming
7 + infeasible (300 s) at s ≈ s_k; SMT (Z3 bit-vectors) is slower than SAT on the same cells.
8 +motivation: C2 (SAT/SMT understanding of reduced-round constraints).
9 +assumptions: Tseitin encoding, ripple-carry adders.
10 +expected_contribution: Scaling figure and table across 11 step counts × 7 free-bit counts × 2 solvers.
11 +related_work:
12 +- mironov2006applications
13 +- nejati2017adaptive
14 +- alamgir2024sha256
15 +- biere2024cadical
16 +- demoura2008z3
17 +possible_counterexamples: Sub-exponential growth (would be surprising and would need explanation).
18 +verification_strategy: experiments/reduced_round_sha256/preimage_sat.py on M3U96b
19 +computational_budget: ≈ 3 node-hours
20 +status: running
21 +classification: pending
22 +evidence: []
23 +verdict: pending
24 +---
25 +
26 +# H17 — SAT preimage search on step-reduced SHA-256 scales exponentially in the number of steps and in the number of free bits
27 +
28 +For fixed free message bits k, CaDiCaL/Kissat time grows roughly exponentially with the step count s, becoming infeasible (300 s) at s ≈ s_k; SMT (Z3 bit-vectors) is slower than SAT on the same cells.
added hypotheses/H18-generic-sat-encodings-find-step-reduced-collisio.md +33 −0
@@ -0,0 +1,33 @@
1 +---
2 +id: H18
3 +title: Generic SAT encodings find step-reduced collisions only far below the published dedicated-tool records
4 +axis: C
5 +kind: sha256
6 +statement: With a 600 s budget, CaDiCaL finds collisions (standard IV) for s ≤ s_c steps and semi-free-start collisions for
7 + s ≤ s_sfs, while the literature records are 31 steps (practical collision, Li et al. ASIACRYPT 2024; 37-step theoretical,
8 + 2026) and 39 steps (semi-free-start); automated generic search reproduces the easy regime only.
9 +motivation: C4 (can automated searches reproduce known reduced-round results?).
10 +assumptions: Generic encoding, no differential characteristic guidance.
11 +expected_contribution: Honest calibration of generic SAT against the state of the art.
12 +related_work:
13 +- nikolic2008collisions
14 +- sanadhya2008new
15 +- mendel2011finding
16 +- mendel2013improving
17 +- dobraunig2015analysis
18 +- li2024first
19 +- li2024new
20 +- zhang2026collision
21 +- alamgir2024sha256
22 +possible_counterexamples: SAT reaching ≥ 24 steps unaided (would be notable).
23 +verification_strategy: experiments/reduced_round_sha256/collision_sat.py
24 +computational_budget: ≈ 3 node-hours
25 +status: running
26 +classification: pending
27 +evidence: []
28 +verdict: pending
29 +---
30 +
31 +# H18 — Generic SAT encodings find step-reduced collisions only far below the published dedicated-tool records
32 +
33 +With a 600 s budget, CaDiCaL finds collisions (standard IV) for s ≤ s_c steps and semi-free-start collisions for s ≤ s_sfs, while the literature records are 31 steps (practical collision, Li et al. ASIACRYPT 2024; 37-step theoretical, 2026) and 39 steps (semi-free-start); automated generic search reproduces the easy regime only.
added hypotheses/H19-the-9-step-local-collision-holds-with-probabilit.md +28 −0
@@ -0,0 +1,28 @@
1 +---
2 +id: H19
3 +title: The 9-step local collision holds with probability 1 in the XOR-linearised model and with low, position-dependent probability
4 + in the real step function
5 +axis: C
6 +kind: sha256
7 +statement: The Chabaud–Joux/Gilbert–Handschuh correction pattern cancels any single-bit difference after 9 linearised steps
8 + (machine-checked); with modular additions the same pattern succeeds with probability ≈ 2^-c (c measured per bit position,
9 + highest at the MSB), and SAT finds conforming message words in seconds.
10 +motivation: C3/C4.
11 +assumptions: Linearisation of Ch and Maj as in Gilbert–Handschuh 2003.
12 +expected_contribution: Reproduction of a classical result with a formal proof of the linear part.
13 +related_work:
14 +- chabaud1998differential
15 +- gilbert2003security
16 +- nikolic2008collisions
17 +possible_counterexamples: Linearised cancellation failing for some δ (ruled out by bv_decide).
18 +verification_strategy: experiments/reduced_round_sha256/local_collision.py + formal/ Lean theorem.
19 +computational_budget: minutes
20 +status: running
21 +classification: pending
22 +evidence: []
23 +verdict: pending
24 +---
25 +
26 +# H19 — The 9-step local collision holds with probability 1 in the XOR-linearised model and with low, position-dependent probability in the real step function
27 +
28 +The Chabaud–Joux/Gilbert–Handschuh correction pattern cancels any single-bit difference after 9 linearised steps (machine-checked); with modular additions the same pattern succeeds with probability ≈ 2^-c (c measured per bit position, highest at the MSB), and SAT finds conforming message words in seconds.
added hypotheses/H20-the-sha-256-step-is-a-bijection-on-the-state-and.md +35 −0
@@ -0,0 +1,35 @@
1 +---
2 +id: H20
3 +title: The SHA-256 step is a bijection on the state and the message schedule is invertible, so free-start and pseudo-preimage
4 + problems on the compression function are trivial
5 +axis: C
6 +kind: sha256
7 +statement: For fixed (K_t, W_t) the step function is a bijection with an explicit inverse; W_{t−16} is determined by W_{t−15..t};
8 + hence for any target state and any chosen message one can invert the full 64-step compression (without feed-forward) and
9 + choose the IV — the Merkle–Damgård feed-forward is what makes preimages hard.
10 +motivation: Distinguish the compression function from the hash (mandate §6).
11 +assumptions: None.
12 +expected_contribution: Machine-checked statements (Lean 4 bv_decide) and an executable demonstration.
13 +related_work:
14 +- nist2015fips180
15 +- merkle1989one
16 +- damgard1989design
17 +- aoki2009preimages
18 +possible_counterexamples: None (theorem).
19 +verification_strategy: formal/ theorems + tests/test_core.py step inverse test.
20 +computational_budget: minutes
21 +status: evaluated
22 +classification: known_reproduced
23 +evidence:
24 +- formal/README.md
25 +- formal/build.log
26 +- tests/test_formal_lean.py
27 +- tests/test_core.py
28 +verdict: 'Machine-checked: stepInv ∘ step = id and step ∘ stepInv = id (per-component SAT lemmas, AIG 2048/2420), steps_bijective
29 + for any number of rounds, scheduleStep_inv and schedule_backward; executable demonstration in tests/test_core.py. Consequence
30 + (pseudo-preimages of the feed-forward-free compression function are free) stated informally.'
31 +---
32 +
33 +# H20 — The SHA-256 step is a bijection on the state and the message schedule is invertible, so free-start and pseudo-preimage problems on the compression function are trivial
34 +
35 +For fixed (K_t, W_t) the step function is a bijection with an explicit inverse; W_{t−16} is determined by W_{t−15..t}; hence for any target state and any chosen message one can invert the full 64-step compression (without feed-forward) and choose the IV — the Merkle–Damgård feed-forward is what makes preimages hard.
added hypotheses/H21-lean-4-bv-decide-machine-checks-the-sha-256-word.md +30 −0
@@ -0,0 +1,30 @@
1 +---
2 +id: H21
3 +title: Lean 4 bv_decide machine-checks the SHA-256 word-level identities used by optimised implementations
4 +axis: D
5 +kind: formal
6 +statement: Ch(x,y,z) = ((y ⊕ z) ∧ x) ⊕ z, two Maj forms, GF(2)-linearity of Σ0, Σ1, σ0, σ1, bijectivity of Σ0, Σ1 and of the
7 + step function are theorems checked with LRAT certificates.
8 +motivation: 'Stage 5: separate machine-checked statements from informal proofs.'
9 +assumptions: Lean 4.35 core BitVec semantics.
10 +expected_contribution: A reusable formal library of SHA-256 identities.
11 +related_work:
12 +- demoura2021lean
13 +- nist2015fips180
14 +possible_counterexamples: A false identity would produce a counterexample from the SAT solver.
15 +verification_strategy: lake build in formal/
16 +computational_budget: minutes
17 +status: evaluated
18 +classification: known_reproduced
19 +evidence:
20 +- formal/README.md
21 +- formal/build.log
22 +- tests/test_formal_lean.py
23 +verdict: 'Lean 4.35 bv_decide (LRAT certificates re-checked in Lean): Ch_opt, Maj_opt_or, Maj_opt_xor, GF(2)-linearity of
24 + Σ0, Σ1, σ0, σ1, injectivity and explicit inverses (Σ0⁻¹ = XOR of 17 rotations) hence bijectivity; clean build 32 s, no sorry,
25 + no user axiom, no Mathlib.'
26 +---
27 +
28 +# H21 — Lean 4 bv_decide machine-checks the SHA-256 word-level identities used by optimised implementations
29 +
30 +Ch(x,y,z) = ((y ⊕ z) ∧ x) ⊕ z, two Maj forms, GF(2)-linearity of Σ0, Σ1, σ0, σ1, bijectivity of Σ0, Σ1 and of the step function are theorems checked with LRAT certificates.
added hypotheses/H22-\317\2030-and-\317\2031-are-not-injective-on-32-bit-words--ker.md +30 −0
@@ -0,0 +1,30 @@
1 +---
2 +id: H22
3 +title: σ0 and σ1 are not injective on 32-bit words (kernel of positive dimension), unlike Σ0 and Σ1
4 +axis: D
5 +kind: formal
6 +statement: The logical shift in σ0, σ1 makes the GF(2)-linear maps non-bijective; bv_decide or a rank computation exhibits
7 + the kernel; Σ0, Σ1 (rotations only) are bijective.
8 +motivation: A small, decidable algebraic question whose answer matters for message-schedule analyses.
9 +assumptions: None.
10 +expected_contribution: Exact kernel dimensions and counterexamples.
11 +related_work:
12 +- nist2015fips180
13 +possible_counterexamples: σ0 injective (would contradict the rank computation).
14 +verification_strategy: formal/ + GF(2) rank script
15 +computational_budget: minutes
16 +status: evaluated
17 +classification: rejected
18 +evidence:
19 +- formal/README.md
20 +- formal/build.log
21 +- tests/test_formal_lean.py
22 +- formal/scripts/gf2_rank.py
23 +verdict: 'Conjecture refuted: σ0 and σ1 are bijective (bv_decide UNSAT certificates for injectivity; GF(2) rank 32 by two
24 + independent eliminations; explicit 32×32 inverse matrices verified in Lean). The shift makes each summand singular (x≫3
25 + has rank 29; rotr7⊕rotr18 kills 0xFFFFFFFF) but the sum is a unit. A wrong belief caught by the formal layer.'
26 +---
27 +
28 +# H22 — σ0 and σ1 are not injective on 32-bit words (kernel of positive dimension), unlike Σ0 and Σ1
29 +
30 +The logical shift in σ0, σ1 makes the GF(2)-linear maps non-bijective; bv_decide or a rank computation exhibits the kernel; Σ0, Σ1 (rotations only) are bijective.
added hypotheses/H23-cross-implementation-agreement-is-a-necessary-ga.md +33 −0
@@ -0,0 +1,33 @@
1 +---
2 +id: H23
3 +title: 'Cross-implementation agreement is a necessary gate: every arithmetic component is checked against at least one independent
4 + implementation'
5 +axis: D
6 +kind: methodology
7 +statement: Pure Python (wp34) agrees with PARI/GP (curve facts), python-ecdsa and libsecp256k1 (ECDSA), the 19 official BIP340
8 + vectors, hashlib (SHA-256), and the Rust engine (ECDLP answers) on every tested instance; discrepancies found during development
9 + are logged.
10 +motivation: 'Mandate §10: validate custom mathematical code against independent references.'
11 +assumptions: None.
12 +expected_contribution: Transparent record of the validation layer.
13 +related_work:
14 +- wuille2020bip340
15 +- pornin2013rfc6979
16 +possible_counterexamples: Any disagreement (none remained after development; the kangaroo reference initially looped because
17 + its hash ignored the retry seed — fixed and logged).
18 +verification_strategy: tests/ (pytest) — all must pass.
19 +computational_budget: minutes
20 +status: evaluated
21 +classification: empirical_observation
22 +evidence:
23 +- tests/test_core.py
24 +- tests/test_dlp_reference.py
25 +verdict: 11 reference tests pass; agreement with 5 independent implementations; two development bugs caught by the gate (kangaroo
26 + retry loop, PARI output parsing).
27 +commands:
28 +- .venv/bin/python -m pytest -q tests/test_core.py tests/test_dlp_reference.py
29 +---
30 +
31 +# H23 — Cross-implementation agreement is a necessary gate: every arithmetic component is checked against at least one independent implementation
32 +
33 +Pure Python (wp34) agrees with PARI/GP (curve facts), python-ecdsa and libsecp256k1 (ECDSA), the 19 official BIP340 vectors, hashlib (SHA-256), and the Rust engine (ECDLP answers) on every tested instance; discrepancies found during development are logged.
added hypotheses/H24-the-six-stage-pipeline-rediscovers-known-structu.md +27 −0
@@ -0,0 +1,27 @@
1 +---
2 +id: H24
3 +title: The six-stage pipeline rediscovers known structure at a high rate and reports zero validated novel shortcuts
4 +axis: D
5 +kind: methodology
6 +statement: Of the 25 hypotheses, the pipeline classifies the majority as known results reproduced or empirical observations,
7 + rejects the explicitly 'beyond √n' conjectures with proofs or counterexamples, and produces no claim of a cryptographic
8 + weakness; the baseline (literature lookup without computation) would have left the quantitative constants unmeasured.
9 +motivation: Outcome C of the mandate (methodological contribution) must be evaluated honestly.
10 +assumptions: Classification rubric of §7 Stage 6.
11 +expected_contribution: Discovery-log statistics and a critical assessment of what automation added.
12 +related_work:
13 +- fawzi2022discovering
14 +- romeraparedes2024mathematical
15 +- lu2024ai
16 +possible_counterexamples: A validated novel result (none).
17 +verification_strategy: experiments/automated_discovery/registry.py
18 +computational_budget: none
19 +status: running
20 +classification: pending
21 +evidence: []
22 +verdict: pending
23 +---
24 +
25 +# H24 — The six-stage pipeline rediscovers known structure at a high rate and reports zero validated novel shortcuts
26 +
27 +Of the 25 hypotheses, the pipeline classifies the majority as known results reproduced or empirical observations, rejects the explicitly 'beyond √n' conjectures with proofs or counterexamples, and produces no claim of a cryptographic weakness; the baseline (literature lookup without computation) would have left the quantitative constants unmeasured.
added hypotheses/H25-ecdsa-malleability-and-schnorr-correctness-follo.md +31 −0
@@ -0,0 +1,31 @@
1 +---
2 +id: H25
3 +title: ECDSA malleability and Schnorr correctness follow from the abstract group axioms alone (formalised)
4 +axis: D
5 +kind: formal
6 +statement: 'In an axiomatised cyclic group with x(−P) = x(P): if (r, s) verifies then (r, −s) verifies; and s = k + e·d implies
7 + [s]G = R + [e]P.'
8 +motivation: 'Research question B1/B4: which properties depend only on the group structure (and hence are shared by every curve)?'
9 +assumptions: Abstract group structure fields; modular inverses as hypotheses.
10 +expected_contribution: Machine-checked statements of two textbook facts.
11 +related_work:
12 +- schnorr1991efficient
13 +- nist2023fips186
14 +- wuille2020bip340
15 +possible_counterexamples: None (theorems).
16 +verification_strategy: formal/ Lean project.
17 +computational_budget: minutes
18 +status: evaluated
19 +classification: known_reproduced
20 +evidence:
21 +- formal/README.md
22 +- formal/build.log
23 +- tests/test_formal_lean.py
24 +verdict: 'Machine-checked in an axiomatised CyclicGroup structure (fields listed in formal/README.md): schnorr_correct (s
25 + = k + e·d mod n ⟹ s•G = R + e•P), ecdsa_correct, ecdsa_malleable ((r,s) valid ⟹ (r, n−s) valid, modular inverse as witness).
26 + Only propext/Quot.sound used.'
27 +---
28 +
29 +# H25 — ECDSA malleability and Schnorr correctness follow from the abstract group axioms alone (formalised)
30 +
31 +In an axiomatised cyclic group with x(−P) = x(P): if (r, s) verifies then (r, −s) verifies; and s = k + e·d implies [s]G = R + [e]P.
added literature/db.yaml +662 −0
@@ -0,0 +1,662 @@
1 +# WP34 — Stage 1 structured literature database
2 +# Every `source` is a key in paper/references.bib (all verified 2026-11-01 via Crossref / IACR ePrint /
3 +# arXiv / DOI / official standard URL). Formulas are plain text. `notes` records caveats and how the
4 +# item will be used by the discovery pipeline.
5 +
6 +theorems:
7 + - id: T01
8 + statement: >
9 + Hasse bound. For an elliptic curve E over F_q, |#E(F_q) - (q + 1)| <= 2*sqrt(q); equivalently
10 + the Frobenius trace t satisfies |t| <= 2 sqrt(q).
11 + source: hasse1936theorie
12 + notes: >
13 + Modern statement and proof in silverman2009arithmetic (Thm. V.1.1) and washington2008elliptic (Thm. 4.2). For secp256k1 n is prime and the cofactor is 1 (certicom2010sec2).
14 + - id: T02
15 + statement: >
16 + Group structure. E(F_q) is isomorphic to Z/n1 x Z/n2 with n2 | n1 and n2 | (q - 1); in particular
17 + E(F_q) is cyclic or a product of two cyclic groups.
18 + source: washington2008elliptic
19 + notes: >
20 + Also silverman2009arithmetic III.6 / V.1. Pohlig-Hellman (A08) applies to the subgroup structure.
21 + - id: T03
22 + statement: >
23 + Deuring / CM theory. End(E) over the algebraic closure is Z, an order in an imaginary quadratic
24 + field (ordinary case) or an order in a quaternion algebra (supersingular). For secp256k1
25 + (j = 0, p = 1 mod 3) End(E) contains Z[omega], omega a primitive cube root of unity, giving the
26 + order-6 automorphism group and the endomorphism phi(x, y) = (beta x, y) with beta^3 = 1 in F_p.
27 + source: deuring1941typen
28 + notes: >
29 + Operational consequences in gallant2001faster and wiener1999faster. CM discriminant -3.
30 + - id: T04
31 + statement: >
32 + Schoof's algorithm counts #E(F_q) deterministically in O(log^8 q) bit operations by computing the
33 + Frobenius trace modulo small primes l and recombining by CRT; SEA improvements (Elkies, Atkin)
34 + lower this to O~(log^4 q) heuristically.
35 + source: schoof1985elliptic
36 + notes: >
37 + SEA described in schoof1995counting. Used to validate synthetic-curve orders in the experiments.
38 + - id: T05
39 + statement: >
40 + Generic lower bound (Shoup 1997, Nechaev 1994). Any generic algorithm that computes discrete
41 + logarithms in a cyclic group of prime order p with probability >= 1/2 performs Omega(sqrt(p))
42 + group operations; the same bound holds for the Diffie-Hellman problems.
43 + source: shoup1997lower
44 + notes: >
45 + Nechaev proved the deterministic case (nechaev1994complexity). The bound is for the generic (black-box) model only; it says nothing about algorithms that use the representation of the group (e.g. index calculus).
46 + - id: T06
47 + statement: >
48 + Random-mapping statistics (Flajolet-Odlyzko 1990, Harris 1960). For a random mapping on an
49 + n-element set, the expected rho length (tail + cycle) of a random starting point is
50 + sqrt(pi n / 2) (1 + o(1)); expected tail length and cycle length are each sqrt(pi n / 8).
51 + source: flajolet1990random
52 + notes: >
53 + harris1960probability gives the earlier distributional results. Baseline for all rho experiments; a true random mapping is the negative control (CONVENTIONS 5).
54 + - id: T07
55 + statement: >
56 + Preprocessing lower bound (Corrigan-Gibbs-Kogan 2018). Any generic discrete-log algorithm with an
57 + S-bit advice string, online time T and success probability eps in a group of prime order N
58 + satisfies S T^2 = Omega~(eps N); the bound is tight up to logarithmic factors.
59 + source: corrigangibbs2018discrete
60 + notes: >
61 + Matching upper bound (S T^2 = O~(N)) given by bernstein2013nonuniform (rho with precomputed distinguished points) and by lee2011generic. Relevant to the fixed-group setting of secp256k1.
62 + - id: T08
63 + statement: >
64 + Pohlig-Hellman. If the group order factors as prod p_i^{e_i}, a discrete logarithm reduces to
65 + sum_i e_i instances in groups of prime order p_i plus CRT; the cost is dominated by the largest
66 + prime factor: O(sum_i e_i (log n + sqrt(p_i))) group operations with rho or BSGS.
67 + source: pohlig1978improved
68 + notes: >
69 + secp256k1 has prime order n, so Pohlig-Hellman gives nothing; it matters for synthetic composite-order instances.
70 + - id: T09
71 + statement: >
72 + MOV / Frey-Rueck reduction. If n | q^k - 1 for small embedding degree k, the Weil (MOV) or Tate
73 + (Frey-Rueck) pairing maps the ECDLP in the order-n subgroup into the DLP in F_{q^k}^*, which has
74 + subexponential algorithms.
75 + source: menezes1993reducing
76 + notes: >
77 + Tate-pairing version in frey1994remark. For secp256k1 the embedding degree is astronomically large (checked in SafeCurves, bernstein2014safecurves); the reduction does not apply.
78 + - id: T10
79 + statement: >
80 + Anomalous-curve attack. If #E(F_p) = p (trace 1), the ECDLP is solvable in polynomial time by
81 + lifting to the p-adics / Fermat quotients (Semaev, Smart, Satoh-Araki, independently).
82 + source: smart1999discrete
83 + notes: >
84 + semaev1998evaluation and satoh1998fermat are the other two original sources. secp256k1 has n != p, so the attack does not apply.
85 + - id: T11
86 + statement: >
87 + Summation polynomials (Semaev 2004). For an elliptic curve E, there exist polynomials f_m in m
88 + variables of degree 2^{m-2} in each variable such that f_m(x_1, ..., x_m) = 0 iff there exist
89 + points P_i with x(P_i) = x_i and P_1 + ... + P_m = O.
90 + source: semaev2004summation
91 + notes: >
92 + Basis of index-calculus on elliptic curves; see T12 / N05 for why this fails over prime fields.
93 + - id: T12
94 + statement: >
95 + Index calculus on E(F_{q^n}) (Gaudry 2009, Diem 2011). For fixed n >= 2 and q -> infinity, Weil
96 + descent on summation polynomials solves the ECDLP in heuristic time O~(q^{2 - 2/n}); Diem proves
97 + subexponential complexity for families with n growing like sqrt(log q).
98 + source: gaudry2009index
99 + notes: >
100 + diem2011discrete gives the rigorous version. Requires an extension field: no analogue is known for prime fields (N05).
101 + - id: T13
102 + statement: >
103 + Forking lemma. From a forger that succeeds with probability eps against a Schnorr-type signature
104 + in the random-oracle model after q_h hash queries, one extracts the discrete logarithm with
105 + probability about eps^2 / q_h; hence Schnorr is EUF-CMA secure under the DLP in the ROM, with a
106 + loss factor q_h.
107 + source: pointcheval2000security
108 + notes: >
109 + General forking lemma (any algorithm) in bellare2006multi. Tightness discussed in T14.
110 + - id: T14
111 + statement: >
112 + Seurin 2012. In the ROM, any algebraic reduction from the DLP to Schnorr unforgeability must lose
113 + a factor close to q_h; the forking-lemma bound is essentially optimal, and the random-oracle
114 + assumption cannot be removed for the standard DL-based proof (building on Paillier-Vergnaud 2005).
115 + source: seurin2012exact
116 + notes: >
117 + paillier2005discrete shows DL-based signatures (including Schnorr and ECDSA-like schemes) cannot be proven equivalent to DL via algebraic reductions in the standard model under the one-more DL assumption.
118 + - id: T15
119 + statement: >
120 + ECDSA in the generic group model (Brown 2005). If the conversion function x(R) mod n is modelled
121 + via a generic group and the hash is collision-resistant (plus a 'almost-invertibility' condition
122 + on the conversion), ECDSA is existentially unforgeable.
123 + source: brown2005generic
124 + notes: >
125 + Also vaudenay2003security (DSA/ECDSA in the ROM-like 'generic' hash model). The generic group model is strong; it ignores the actual structure of secp256k1.
126 + - id: T16
127 + statement: >
128 + Fersch-Kiltz-Poettering 2016/2017. Modelling the conversion function f(R) = x(R) mod n as a
129 + bijective random oracle (and the hash as a random oracle), ECDSA is EUF-CMA secure under the
130 + DLP; without the bijective-RO idealisation one still gets one-per-message unforgeability for
131 + ECDSA and its variants under appropriate assumptions.
132 + source: fersch2016provable
133 + notes: >
134 + fersch2017one is the one-per-message result. Neither result covers the malleability (s, n - s) of ECDSA, which is inherent.
135 + - id: T17
136 + statement: >
137 + Groth-Shoup 2022. ECDSA remains secure in the generic group model when additive key derivation
138 + (BIP32-style tweaks pk + tG) and presignatures (R computed before the message) are used, with an
139 + explicit degradation bound; the proof is given in the GGM with a concrete hash assumption.
140 + source: groth2022security
141 + notes: >
142 + Relevant to Bitcoin derivation paths and threshold ECDSA; the result is in the GGM only.
143 + - id: T18
144 + statement: >
145 + Nonce leakage breaks ECDSA. Knowledge of a few bits of the nonce k for many signatures reduces key
146 + recovery to a hidden-number / closest-vector problem solvable by lattice reduction (Howgrave-Graham
147 + and Smart 2001); real-world biased nonces were exploited in cryptocurrencies (Breitner-Heninger
148 + 2019); less than one bit of leakage suffices with enough signatures (LadderLeak 2020).
149 + source: howgravegraham2001lattice
150 + notes: >
151 + breitner2019biased, aldaya2020ladderleak (authors Aranha, Novaes, Takahashi, Tibouchi, Yarom), jancar2020minerva. Motivates deterministic nonces (pornin2013rfc6979, wuille2020bip340).
152 + - id: T19
153 + statement: >
154 + Merkle-Damgaard. If the compression function f is collision resistant and the padding encodes the
155 + message length (MD strengthening), the iterated hash H is collision resistant.
156 + source: merkle1989one
157 + notes: >
158 + damgard1989design is the independent proof. SHA-256 follows this construction (nist2015fips180) with 64-step compression on 512-bit blocks and 256-bit chaining value.
159 + - id: T20
160 + statement: >
161 + Tseitin transformation. Any Boolean circuit with m gates has an equisatisfiable CNF with O(m)
162 + clauses and O(m) auxiliary variables; this is the encoding used to turn reduced-step SHA-256 into
163 + SAT instances.
164 + source: tseitin1983complexity
165 + notes: >
166 + Used by mironov2006applications, nejati2017adaptive, alamgir2024sha256 and by Lean's bv_decide (boving2025interactive).
167 + - id: T21
168 + statement: >
169 + Shor 1997. On a quantum computer the discrete logarithm problem in any cyclic group with
170 + efficiently computable group law is solvable in polynomial time; resource estimates for
171 + 256-bit prime-field ECDLP are about 2330 logical qubits and 1.26e11 Toffoli gates
172 + (Roetteler et al. 2017).
173 + source: shor1997polynomial
174 + notes: >
175 + roetteler2017quantum. Out of scope for the classical experiments; cited in the implications section.
176 +
177 +algorithms:
178 + - id: A01
179 + statement: >
180 + Baby-step giant-step (Shanks 1971). Deterministic, O(sqrt(n)) group operations and O(sqrt(n))
181 + memory; with m = ceil(sqrt(n)) baby steps and giant steps of size m.
182 + source: shanks1971class
183 + notes: >
184 + Interval and low-weight variants in bernstein2012two and stinson2002some. Memory makes it impractical beyond ~2^40 in practice but it is the exact-count baseline.
185 + - id: A02
186 + statement: >
187 + Pollard rho (1978). Iterate a pseudo-random walk x_{i+1} = f(x_i) on the group with a cycle-finding
188 + method; a collision x_i = x_j with distinct (a, b) representations yields the logarithm. Expected
189 + cost about sqrt(pi n / 2) group operations with O(1) memory when f behaves like a random mapping.
190 + source: pollard1978monte
191 + notes: >
192 + Cycle finding by Floyd (original) or Brent (brent1980improved, fewer function evaluations). The original 3-partition walk is not random enough (teske1998speeding).
193 + - id: A03
194 + statement: >
195 + Pollard kangaroo / lambda (1978, 2000). Two walks (tame, wild) with jumps of mean size about
196 + sqrt(w)/2 solve a logarithm known to lie in an interval of width w in about 2 sqrt(w) operations;
197 + the 2000 paper gives the refined 'monopoly' analysis and the parallel version.
198 + source: pollard2000kangaroos
199 + notes: >
200 + Interval DLP improvements with equivalence classes in galbraith2010interval (about 1.36 sqrt(w)) and the 'grumpy giants' BSGS variant in bernstein2012two.
201 + - id: A04
202 + statement: >
203 + r-adding walks (Teske 1998/2001). With r >= 20 random multipliers, the adding walk x -> x + M_{h(x)}
204 + behaves close to a random mapping and the rho cost approaches sqrt(pi n / 2); r = 3 is about
205 + 30% slower; mixed adding/doubling walks also work.
206 + source: teske2001random
207 + notes: >
208 + teske1998speeding is the ANTS version. Partition count r is a free parameter for sensitivity analysis.
209 + - id: A05
210 + statement: >
211 + Parallel collision search with distinguished points (van Oorschot-Wiener 1999). m processors
212 + each run independent walks and report points with a distinguishing property (probability theta)
213 + to a central store; expected time sqrt(pi n / 2)/m + 1/theta per processor, i.e. linear
214 + speedup with memory about theta sqrt(pi n / 2) points.
215 + source: vanoorschot1999parallel
216 + notes: >
217 + Used by every ECDLP record since 2000 (bos2012solving, wenger2014solving, bernstein2016faster). The additive overhead 1/theta is the price of distinguished points.
218 + - id: A06
219 + statement: >
220 + Negation map. Iterating on classes {P, -P} halves the effective group size, giving an expected
221 + speedup of sqrt(2) for rho; it introduces fruitless 2-cycles (and longer cycles) that must be
222 + detected and escaped, costing a constant factor if handled carelessly.
223 + source: wiener1999faster
224 + notes: >
225 + bos2010negation quantified the fruitless-cycle overhead and reported practical speedups below sqrt(2); bernstein2011correct shows how to approach the full sqrt(2) with cheap cycle handling. Also gallant2000improving.
226 + - id: A07
227 + statement: >
228 + Automorphism speedup. If E has an efficiently computable automorphism group of order m acting on
229 + the order-n subgroup, rho on orbits gives an expected speedup of sqrt(m): sqrt(2 l) for Koblitz
230 + curves over F_{2^l} (Frobenius and negation), sqrt(6) for j = 0 curves such as secp256k1 (order-6
231 + group generated by (x, y) -> (beta x, -y)), sqrt(4) for j = 1728.
232 + source: duursma1999speeding
233 + notes: >
234 + Independently gallant2000improving and wiener1999faster. The sqrt(6) for secp256k1 is the only known structural advantage of its CM; it is a constant factor, not a change of exponent.
235 + - id: A08
236 + statement: >
237 + GLV scalar decomposition. For a curve with endomorphism phi acting as [lambda], write
238 + k = k1 + k2 lambda (mod n) with |k1|, |k2| about sqrt(n) via a short lattice basis; the scalar
239 + multiplication kP = k1 P + k2 phi(P) with simultaneous (Straus-Shamir) double-and-add roughly
240 + halves the number of doublings.
241 + source: gallant2001faster
242 + notes: >
243 + Bounds on |k1|, |k2| proven in sica2002analysis; generalised to a large class of curves (GLS) in galbraith2011endomorphisms (and galbraith2009endomorphisms). libsecp256k1 implements the split (wuille2013libsecp256k1, src/scalar_impl.h).
244 + - id: A09
245 + statement: >
246 + Multi-scalar multiplication. Straus (1964) / Shamir trick computes a1 P1 + ... + ak Pk with one
247 + shared doubling chain; Pippenger (1980) achieves about (l / log l)-type savings for many terms via
248 + bucket methods; interleaving with sliding windows (Moeller 2001) is the practical choice for
249 + k = 2 (ECDSA/Schnorr verification).
250 + source: straus1964addition
251 + notes: >
252 + pippenger1980evaluation, moller2001algorithms. Batch verification reduces to one large MSM (A11).
253 + - id: A10
254 + statement: >
255 + Coordinates and ladders. Mixed Jacobian/affine coordinates minimise field operations for
256 + double-and-add (Cohen-Miyaji-Ono 1998); Montgomery's x-only ladder (1987) and its Weierstrass
257 + adaptation (Brier-Joye 2002) give uniform, side-channel-resistant scalar multiplication.
258 + source: cohen1998efficient
259 + notes: >
260 + montgomery1987speeding, brier2002weierstrass. Generalised-Mersenne reduction for p = 2^256 - 2^32 - 977 follows solinas1999generalized. Timing attacks: kocher1996timing, brumley2011remote.
261 + - id: A11
262 + statement: >
263 + Batch verification. For Schnorr-type signatures, verifying N signatures reduces to one random
264 + linear combination sum a_i (s_i G - R_i - e_i P_i) = O with random 128-bit a_i, soundness error
265 + 2^-128 (Naccache et al. 1994; Bellare-Garay-Rabin 1998); with Pippenger's MSM the cost per
266 + signature drops to a fraction of a single verification (Ed25519: roughly half, Bernstein et al. 2012).
267 + source: bellare1998fast
268 + notes: >
269 + naccache1994can, bernstein2012high. BIP340 (wuille2020bip340) is designed for batch verification; ECDSA is not batch-verifiable without recovering R (B04).
270 + - id: A12
271 + statement: >
272 + Schoof / SEA point counting (see T04) and Hasse-interval search are used to generate synthetic
273 + prime-order curves at 20-80 bits for the experiments.
274 + source: schoof1995counting
275 + notes: >
276 + Implementation gate: cross-check orders with two independent implementations (CONVENTIONS).
277 + - id: A13
278 + statement: >
279 + Tag tracing (Cheon-Hong-Kim 2012). In prime-field (multiplicative) groups, the rho walk can be
280 + advanced without full group multiplications by tracing a tag of the product, giving large
281 + constant-factor speedups; the technique is specific to F_p^* representation.
282 + source: cheon2012speeding
283 + notes: >
284 + No analogue for elliptic-curve groups is known (relevant to A4 of the mandate: representation-specific speedups).
285 + - id: A14
286 + statement: >
287 + Automated differential-characteristic search for SHA-2 (Mendel-Nad-Schlaeffer 2011/2013):
288 + guess-and-determine over generalised conditions with contradiction detection; the 2013 extension
289 + to longer local collisions produced 28-step practical, 31-step theoretical and 38-step
290 + semi-free-start collisions.
291 + source: mendel2011finding
292 + notes: >
293 + mendel2013improving; branching heuristics in eichlseder2014branching. SAT/MILP-based successors in li2024new, li2024first, zhang2026collision.
294 + - id: A15
295 + statement: >
296 + SAT-based cryptanalysis of hash functions (Mironov-Zhang 2006). Encode the step function with
297 + Tseitin and solve with CDCL; MapleCrypt adds adaptive restarts and CEGAR for inversion
298 + (Nejati et al. 2017); programmatic SAT with a CAS reproduces the 38-step SFS collision and finds
299 + pure-SAT collisions only up to 28 steps (Alamgir-Nejati-Bright 2024).
300 + source: mironov2006applications
301 + notes: >
302 + nejati2017adaptive, alamgir2024sha256. Solvers: biere2020cadical (Kissat), biere2024cadical (CaDiCaL 2.0), demoura2008z3 (SMT). Lean 4 bv_decide (boving2025interactive) checks LRAT proofs from CaDiCaL inside the kernel.
303 + - id: A16
304 + statement: >
305 + Biclique meet-in-the-middle preimages (Khovratovich-Rechberger-Savelieva 2012) extend the
306 + MITM preimage attacks of Aoki et al. 2009 from 43 to 45 steps of SHA-256 (hash) and 52 steps
307 + (compression function), all with complexity close to 2^256.
308 + source: khovratovich2012bicliques
309 + notes: >
310 + aoki2009preimages. These are not practical; they are the step-count frontier for preimages.
311 + - id: A17
312 + statement: >
313 + Deterministic nonces: RFC 6979 derives k = HMAC_DRBG(d, H(m)); BIP340 derives the nonce from a
314 + tagged hash of (auxiliary randomness xor d), P, m and uses x-only keys with even-y normalisation
315 + and challenge e = H_tag(r || P || m).
316 + source: pornin2013rfc6979
317 + notes: >
318 + wuille2020bip340; Taproot usage in wuille2020bip341. Both remove the nonce-reuse failure mode (T18) but not side channels.
319 +
320 +security_assumptions:
321 + - id: S01
322 + statement: >
323 + ECDLP on secp256k1: given G and Q = dG in the prime-order-n subgroup (n about 2^256), computing d
324 + requires about 2^128 group operations with the best known (generic) algorithms; the sqrt(6)
325 + automorphism gain lowers this to about 2^126.7 but no non-generic attack is known.
326 + source: galbraith2016recent
327 + notes: >
328 + Parameters from certicom2010sec2; curve analysis in bernstein2014safecurves. Baseline assumption for Axis A.
329 + - id: S02
330 + statement: >
331 + Random-oracle assumption on SHA-256 for Schnorr/BIP340 proofs; the challenge hash must be modelled
332 + as a random oracle, and Seurin shows this cannot be avoided for the DL-based reduction.
333 + source: seurin2012exact
334 + notes: >
335 + Links Axis B to Axis C: any structural regularity in SHA-256 would weaken the ROM idealisation before it weakens the DLP.
336 + - id: S03
337 + statement: >
338 + ECDSA security relies additionally on idealising the conversion function x(R) mod n
339 + (bijective random oracle or generic group); ECDSA is inherently malleable (s <-> n - s), which is
340 + why Bitcoin enforces low-s and why BIP340 Schnorr is preferred for new outputs.
341 + source: fersch2016provable
342 + notes: >
343 + brown2005generic, groth2022security. Malleability is a property, not a break.
344 + - id: S04
345 + statement: >
346 + Collision resistance of SHA-256 at the 2^128 level and preimage resistance at 2^256 (ideal-hash
347 + benchmarks); the Merkle-Damgaard structure makes these reduce to the compression function.
348 + source: nist2015fips180
349 + notes: >
350 + merkle1989one. Reduced-step results (E-series) do not threaten the 64-step function (gilbert2003security).
351 + - id: S05
352 + statement: >
353 + Generic-group model: lower bounds (T05) and ECDSA/presignature proofs (T15, T17) hold only for
354 + algorithms that ignore the encoding of group elements; secp256k1's special p and j = 0 are
355 + exactly the kind of structure the GGM cannot see.
356 + source: shoup1997lower
357 + notes: >
358 + This is the formal gap that Axis A probes: is there any representation-aware algorithm? See open problems.
359 + - id: S06
360 + statement: >
361 + Non-uniform / preprocessing security: with a 2^S precomputation table specific to secp256k1, the
362 + online ECDLP cost is about sqrt(n / S) (T07); a 2^80 table would give 2^88 online operations.
363 + The precomputation itself costs about 2^128 (bernstein2013nonuniform).
364 + source: bernstein2013nonuniform
365 + notes: >
366 + corrigangibbs2018discrete. Shows that 'free precomputation' claims need to account for the one-time cost; relevant to any claimed shortcut that amortises over many keys.
367 +
368 +complexity_bounds:
369 + - id: C01
370 + statement: >
371 + Expected rho iterations for a random mapping on n points: sqrt(pi n / 2) about 1.2533 sqrt(n);
372 + with the negation map sqrt(pi n / 4) about 0.886 sqrt(n); with the order-6 automorphism
373 + sqrt(pi n / 12) about 0.512 sqrt(n).
374 + source: flajolet1990random
375 + notes: >
376 + Second and third figures from wiener1999faster / duursma1999speeding assuming perfect handling of fruitless cycles (bernstein2011correct).
377 + - id: C02
378 + statement: >
379 + Parallel rho with m processors and distinguishing probability theta: expected time
380 + (sqrt(pi n / 2)/m + 1/theta) iterations per processor; memory about theta sqrt(pi n / 2) points.
381 + source: vanoorschot1999parallel
382 + notes: >
383 + The experiments must measure the 1/theta overhead and the linear scaling in m.
384 + - id: C03
385 + statement: >
386 + BSGS: sqrt(n) group operations and sqrt(n) stored points, deterministic; interval of width w:
387 + about 2 sqrt(w) for kangaroo (Pollard 2000), about 1.36 sqrt(w) with equivalence classes
388 + (Galbraith-Ruprai 2010), and BSGS variants in bernstein2012two.
389 + source: shanks1971class
390 + notes: >
391 + pollard2000kangaroos, galbraith2010interval.
392 + - id: C04
393 + statement: >
394 + Generic lower bound Omega(sqrt(p)) for prime p (T05); with S bits of preprocessing
395 + S T^2 = Omega~(N) (T07). Any claimed generic algorithm beating these is wrong.
396 + source: shoup1997lower
397 + notes: >
398 + corrigangibbs2018discrete. Serves as the falsification test for Axis A hypotheses that are generic.
399 + - id: C05
400 + statement: >
401 + Index calculus on E(F_{q^n}): O~(q^{2 - 2/n}) for fixed n (Gaudry), subexponential
402 + exp(O(log^{2/3} q)) type for suitable families (Diem); summation-polynomial systems over
403 + prime fields have no known subexponential solution (Petit-Quisquater analyse the Weil-descent
404 + systems and give heuristics for binary fields only).
405 + source: gaudry2009index
406 + notes: >
407 + diem2011discrete, petit2012elliptic, surveyed in galbraith2016recent.
408 + - id: C06
409 + statement: >
410 + GLV: with |k1|, |k2| < C sqrt(n) (explicit C from Sica-Ciet-Quisquater), the double-scalar
411 + computation needs about log2(n)/2 doublings instead of log2(n); the gain is a constant factor
412 + near 1.5-2 in practice.
413 + source: sica2002analysis
414 + notes: >
415 + gallant2001faster.
416 + - id: C07
417 + statement: >
418 + Batch verification of N Schnorr signatures: about one MSM of size 2N + 1 with 128-bit
419 + coefficients; with Pippenger the amortised cost per signature is roughly 1/2 to 1/3 of a single
420 + verification for N in the hundreds (Ed25519 measurements: about 134k cycles per signature in a
421 + batch of 64 versus 2.3x more individually, Bernstein et al. 2012).
422 + source: bernstein2012high
423 + notes: >
424 + bellare1998fast, pippenger1980evaluation. Axis B benchmark target.
425 + - id: C08
426 + statement: >
427 + SHA-256 collision frontier: practical 31 steps (1.2 h on 64 threads), theoretical 37 steps,
428 + semi-free-start practical 39 steps; preimage frontier 43 steps (2^254.9), 45 steps (biclique, hash),
429 + 52 steps (biclique, compression function).
430 + source: li2024first
431 + notes: >
432 + zhang2026collision, li2024new, aoki2009preimages, khovratovich2012bicliques; full table in literature/records.md.
433 + - id: C09
434 + statement: >
435 + SAT-based SHA-256: pure CDCL finds collisions only up to 28 steps; with CAS guidance a 38-step
436 + semi-free-start collision is reproduced (Alamgir et al. 2024). Earlier SAT inversions reach only
437 + about 20-24 steps (Mironov-Zhang 2006; Nejati et al. 2017).
438 + source: alamgir2024sha256
439 + notes: >
440 + These numbers are the baseline for Axis C research question C4 (reproduce known results with automated search).
441 + - id: C10
442 + statement: >
443 + Quantum: polynomial time for ECDLP (Shor); 256-bit prime-field ECDLP needs about 2330 logical
444 + qubits and 1.26e11 Toffoli gates (Roetteler et al. 2017).
445 + source: roetteler2017quantum
446 + notes: >
447 + shor1997polynomial.
448 +
449 +experimental_records:
450 + - id: E01
451 + statement: >
452 + Certicom challenges: ECCp-109 (prime, 109 bits) solved Oct. 15, 2002; ECC2K-108 (Koblitz) Apr. 4,
453 + 2000; ECC2-109 Apr. 8, 2004; the 131-bit challenges and ECC2K-130 remain open in the 2009 document.
454 + source: certicom2009challenge
455 + notes: >
456 + Attack plan for ECC2K-130 in bailey2009breaking (completion not verified).
457 + - id: E02
458 + statement: >
459 + Largest completed prime-field ECDLP: secp112r1 (112-bit), Pollard rho on about 215 PlayStation 3
460 + consoles at EPFL, 2009 (published 2012).
461 + source: bos2012solving
462 + notes: >
463 + bernstein2011correct later roughly doubled the per-console throughput with a correct negation map.
464 + - id: E03
465 + statement: >
466 + Binary-field records: 113-bit Koblitz curve on an 18-core Virtex-6 FPGA cluster (extrapolated
467 + 24 days, 2014); sect113r1 on 10 Kintex-7 FPGAs at 900 M iterations/s over several months (2015);
468 + sect113r2 on FPGAs in 2016, 'a prime order very slightly larger than the previous record holder'.
469 + source: wenger2016harder
470 + notes: >
471 + wenger2014solving, bernstein2016faster. No '117.35-bit' record could be verified (unverified.md).
472 + - id: E04
473 + statement: >
474 + SHA-256 collisions: 21 steps (2008), 23/24 steps practical (2008), 27 steps practical (2011),
475 + 28 steps practical and 31 steps at 2^65.5 (2013), 31 steps practical (2024), 37 steps theoretical
476 + (2026).
477 + source: li2024first
478 + notes: >
479 + nikolic2008collisions, indesteege2008collisions, sanadhya2008new, mendel2011finding, mendel2013improving, zhang2026collision.
480 + - id: E05
481 + statement: >
482 + SHA-256 semi-free-start collisions: 23 steps (2008), 32 steps (2011), 38 steps (2013),
483 + 39 steps practical (2024); free-start collision on 40-step SHA-224 practical (2024);
484 + SHA-512: 27-step practical collision (2015), 28-step practical and 31-step theoretical (2024),
485 + 38-step SFS at 2^40.5 (2014).
486 + source: li2024new
487 + notes: >
488 + eichlseder2014branching, dobraunig2015analysis.
489 + - id: E06
490 + statement: >
491 + SHA-256 preimages: 43 steps at 2^254.9 (2009); 45 steps (hash) and 52 steps (compression
492 + function) with bicliques (2012); SHA-512: 46 steps at 2^511.5 (2009), 50/57 steps (2012).
493 + source: aoki2009preimages
494 + notes: >
495 + khovratovich2012bicliques.
496 + - id: E07
497 + statement: >
498 + SHA-1 (contrast): identical-prefix collision at about 2^63.1 (SHAttered, 2017) and chosen-prefix
499 + collision at about 2^63.4 (2020), both practical; SHA-0 differential collisions (1998) and the
500 + first theoretical full-SHA-1 collision (2005) preceded them.
501 + source: stevens2017first
502 + notes: >
503 + leurent2020sha1, chabaud1998differential, wang2005finding. The SHA-2 step function (two-input Sigma functions, Maj/Ch, 64 steps) has resisted the same techniques beyond 37 steps.
504 + - id: E08
505 + statement: >
506 + Side channels: remote timing recovered OpenSSL ECDSA keys (Brumley-Tuveri 2011); Minerva recovered
507 + keys from timing leaks in cards and libraries (2020); LadderLeak needs less than one bit of nonce
508 + leakage (2020).
509 + source: brumley2011remote
510 + notes: >
511 + jancar2020minerva, aldaya2020ladderleak. Out of scope for attacks, in scope for the constant-time benchmark requirement.
512 + - id: E09
513 + statement: >
514 + Automated discovery baselines: AlphaTensor found a 47-multiplication 4x4 matrix product over F_2
515 + (2022); FunSearch found new cap-set constructions (2024); AI-guided intuition produced theorems
516 + in knot theory and representation theory (2021); AlphaGeometry solved 25/30 IMO geometry problems
517 + (2024); AlphaEvolve improved several algorithmic constructions (2025).
518 + source: romeraparedes2024mathematical
519 + notes: >
520 + fawzi2022discovering, davies2021advancing, trinh2024solving, novikov2025alphaevolve, lu2024ai. None addresses cryptanalysis of standardised primitives; Axis D baseline comparison.
521 + - id: E10
522 + statement: >
523 + Verified bit-blasting: Lean 4 bv_decide proves QF_BV goals by CaDiCaL + LRAT checking inside the
524 + kernel and beats the previous verified baseline (CoqQFBV) on SMT-LIB.
525 + source: boving2025interactive
526 + notes: >
527 + demoura2021lean. Candidate tool for machine-checking reduced-round SHA-256 identities (Stage 5).
528 +
529 +open_problems:
530 + - id: O01
531 + statement: >
532 + Is the special structure of secp256k1 (p = 2^256 - 2^32 - 977, j = 0, CM discriminant -3,
533 + efficiently computable order-6 automorphism) exploitable beyond the sqrt(6) rho speedup and the
534 + GLV constant factor? No attack is known.
535 + source: galbraith2016recent
536 + notes: >
537 + bernstein2014safecurves lists the curve as failing only 'rigidity' and 'ladder'-type criteria, not any attack criterion. Axis A questions A1-A5.
538 + - id: O02
539 + statement: >
540 + Is there any sub-exponential algorithm for the ECDLP over prime fields? Summation-polynomial
541 + index calculus requires an extension field; no decomposition base with a usable size is known over
542 + F_p.
543 + source: galbraith2016recent
544 + notes: >
545 + semaev2004summation, gaudry2009index, diem2011discrete, petit2012elliptic.
546 + - id: O03
547 + statement: >
548 + Can generic-group lower bounds be matched by algorithms exploiting the representation in a way
549 + that is provably impossible for generic algorithms, for prime fields? (Tag tracing does this for
550 + F_p^* but has no elliptic analogue.)
551 + source: cheon2012speeding
552 + notes: >
553 + Axis A question A3/A4.
554 + - id: O04
555 + statement: >
556 + Practical SHA-256 collision beyond 31 steps and any collision beyond 37 steps; semi-free-start
557 + beyond 39 steps; preimage beyond 45 (hash) / 52 (compression) steps.
558 + source: zhang2026collision
559 + notes: >
560 + li2024first, li2024new, khovratovich2012bicliques. Axis C questions C1-C5.
561 + - id: O05
562 + statement: >
563 + Tight security of ECDSA without idealising the conversion function, and of Schnorr without the
564 + random-oracle model.
565 + source: fersch2016provable
566 + notes: >
567 + seurin2012exact, paillier2005discrete. Axis B question B1/B4.
568 + - id: O06
569 + statement: >
570 + Can SAT/SMT solvers, possibly with CAS or learned heuristics, match the hand-tuned
571 + characteristic-search tools (31-39 steps) rather than stalling at 28 steps?
572 + source: alamgir2024sha256
573 + notes: >
574 + Axis C question C2/C4; Axis D methodology.
575 + - id: O07
576 + statement: >
577 + Can automated hypothesis generation (LLM- or search-based) rediscover known constant-factor
578 + results (negation map, sqrt(6), GLV) and correctly reject non-results on synthetic groups?
579 + source: lu2024ai
580 + notes: >
581 + novikov2025alphaevolve, romeraparedes2024mathematical. Axis D success criterion.
582 + - id: O08
583 + statement: >
584 + Verification acceleration without changing consensus: how much of BIP340 batch verification and
585 + ECDSA verification cost can be removed by MSM/endomorphism tricks while preserving bit-exact
586 + acceptance semantics?
587 + source: wuille2020bip340
588 + notes: >
589 + bernstein2012high, bellare1998fast, wuille2013libsecp256k1. Axis B question B2/B5.
590 +
591 +negative_results:
592 + - id: N01
593 + statement: >
594 + No generic algorithm beats Omega(sqrt(p)) (Shoup/Nechaev); with preprocessing S T^2 =
595 + Omega~(N) (Corrigan-Gibbs-Kogan). Any proposed generic shortcut is refuted a priori.
596 + source: shoup1997lower
597 + notes: >
598 + corrigangibbs2018discrete.
599 + - id: N02
600 + statement: >
601 + Automorphisms give at most sqrt(|Aut|) on rho: sqrt(2) from negation, sqrt(6) on j = 0 curves;
602 + careless fruitless-cycle handling can erase the gain (Bos-Kleinjung-Lenstra 2010).
603 + source: duursma1999speeding
604 + notes: >
605 + bos2010negation, bernstein2011correct, wiener1999faster, gallant2000improving. Settles mandate claim 'endomorphism gives at most sqrt(6) on rho' as a known result.
606 + - id: N03
607 + statement: >
608 + MOV/Frey-Rueck does not apply to secp256k1 (huge embedding degree) and the anomalous attack does
609 + not apply (n != p); SafeCurves verifies both conditions.
610 + source: bernstein2014safecurves
611 + notes: >
612 + menezes1993reducing, frey1994remark, smart1999discrete.
613 + - id: N04
614 + statement: >
615 + Pohlig-Hellman gives nothing on secp256k1 because n is prime and the cofactor is 1.
616 + source: pohlig1978improved
617 + notes: >
618 + certicom2010sec2.
619 + - id: N05
620 + statement: >
621 + Index calculus via summation polynomials fails over prime fields: there is no subfield to define
622 + a small factor base, the summation-polynomial systems have degree 2^{m-2} per variable, and
623 + Groebner-basis solving costs more than rho for all parameter choices studied (Gaudry, Diem,
624 + Petit-Quisquater; survey Galbraith-Gaudry 2016).
625 + source: galbraith2016recent
626 + notes: >
627 + semaev2004summation, gaudry2009index, diem2011discrete, petit2012elliptic.
628 + - id: N06
629 + statement: >
630 + Schnorr cannot be proven equivalent to DL via algebraic reductions without the ROM, and the
631 + forking-lemma loss q_h is unavoidable for such reductions (Paillier-Vergnaud 2005, Seurin 2012).
632 + source: paillier2005discrete
633 + notes: >
634 + seurin2012exact.
635 + - id: N07
636 + statement: >
637 + Pure SAT (no CAS guidance) finds SHA-256 collisions for at most 28 steps; the 2006 and 2017 SAT
638 + inversions reach about 20-24 steps; SAT alone is far from the 31/37-step frontier.
639 + source: alamgir2024sha256
640 + notes: >
641 + mironov2006applications, nejati2017adaptive.
642 + - id: N08
643 + statement: >
644 + Chabaud-Joux / Dobbertin-style attacks on SHA-0/SHA-1 do not transfer to SHA-256; only the
645 + XOR-linearised, symmetric-constant variant is weak (Gilbert-Handschuh 2003).
646 + source: gilbert2003security
647 + notes: >
648 + chabaud1998differential. Reduced-step results since 2008 use different, local-collision-based techniques.
649 + - id: N09
650 + statement: >
651 + Practical ECDLP records stop at 112-113 bits (prime and binary fields); secp256k1 is about 2^71
652 + times harder than the largest completed prime-field computation.
653 + source: bos2012solving
654 + notes: >
655 + bernstein2016faster, certicom2009challenge. Any experimental claim must be scaled honestly (mandate section 4).
656 + - id: N10
657 + statement: >
658 + ECDSA malleability (s, n - s) and the absence of batch verification are structural features, not
659 + bugs; BIP340 removes both by design but still needs the ROM.
660 + source: wuille2020bip340
661 + notes: >
662 + fersch2016provable, seurin2012exact.
added literature/records.md +68 −0
@@ -0,0 +1,68 @@
1 +# Computational records (verified rows only)
2 +
3 +Date: 2026-11-01. Every row cites a key in `paper/references.bib`; figures are those stated in the
4 +paper abstracts or in the Certicom challenge document, not extrapolations.
5 +
6 +## A. Elliptic-curve discrete logarithm computations
7 +
8 +| Instance | Group-order size | Field type | Hardware | Completed | Source | Notes |
9 +|---|---|---|---|---|---|---|
10 +| ECCp-79 / -89 / -97 (Certicom exercises) | 79 / 89 / 97 bits | prime | distributed PCs | Dec. 1997 / Jan. 1998 / Mar. 1998 | `certicom2009challenge` | Certicom table 6.2.2 |
11 +| ECC2K-108 (Certicom Level I) | 108 bits | binary, Koblitz | distributed (≈1.3·10^6 machine-days) | Apr. 4, 2000 | `certicom2009challenge` | Frobenius automorphism exploited (cf. `gallant2000improving`, `wiener1999faster`) |
12 +| ECCp-109 (Certicom Level I) | 109 bits | prime | distributed (≈9.0·10^6 machine-days) | Oct. 15, 2002 | `certicom2009challenge` | 2.1·10^16 expected curve operations |
13 +| ECC2-109 (Certicom Level I) | 109 bits | binary, random | distributed (≈2.1·10^7 machine-days) | Apr. 8, 2004 | `certicom2009challenge` | |
14 +| secp112r1 (112-bit prime field) | 112 bits | prime | cluster of ~215 PlayStation 3 (Cell) consoles, EPFL | July 2009 (published 2012) | `bos2012solving` | Pollard rho with "sloppy reduction"; still the largest completed prime-field ECDLP found in this survey |
15 +| 113-bit Koblitz curve | 113 bits | binary, Koblitz | 18-core Virtex-6 FPGA cluster | 2014 ("extrapolated 24 days") | `wenger2014solving` | first FPGA-cluster ECDLP record |
16 +| sect113r1 (SECG binary curve) | 113 bits | binary, random | 10 Kintex-7 FPGAs, 900·10^6 iterations/s, "several months" | 2015 (JCEN 2016) | `wenger2016harder` | negation map + fruitless-cycle handling |
17 +| sect113r2 (SECG binary curve) | 113 bits ("prime order very slightly larger than the previous record holder") | binary, random | Spartan-6 / larger FPGAs, 3 cores per Spartan-6 | 2016 | `bernstein2016faster` | "new size record for completed ECDL computations" per abstract; the paper also *benchmarks* (does not solve) a curve over a 127-bit field |
18 +| ECCp-131, ECC2-131, ECC2K-130 | 131 / 131 / 130 bits | prime / binary / Koblitz | — | **open** (no end date in Nov. 2009 Certicom document; no later completion found) | `certicom2009challenge` | ECC2K-130 attack plan described in ePrint 2009/541 (not cited as a record, see `unverified.md`) |
19 +
20 +Gap to secp256k1: the subgroup order n ≈ 2^256, so generic rho needs ≈ √(πn/4) ≈ 2^127.5 group
21 +operations with the negation map — about 2^71 times the 112-bit computation (`bos2012solving`,
22 +`bernstein2011correct`).
23 +
24 +## B. SHA-256 (and SHA-2 family) reduced-step cryptanalysis
25 +
26 +Type legend: C = collision (standard IV, hash function); SFS = semi-free-start collision (attacker chooses
27 +the chaining value, same for both messages); FS = free-start collision (two different chaining values);
28 +P = preimage on the hash function; PP = (pseudo-)preimage on the compression function. SHA-256 has 64
29 +steps, SHA-512 has 80.
30 +
31 +| Function | Type | Steps | Complexity | Practical (example given)? | Year / venue | Source |
32 +|---|---|---|---|---|---|---|
33 +| SHA-256 | C | 21 | high-probability differential (≈2^19 per later analysis) | yes | FSE 2008 | `nikolic2008collisions` |
34 +| SHA-256 | SFS | 23 | — | yes | FSE 2008 | `nikolic2008collisions` |
35 +| SHA-256 | SFS near-collision (15/256 bits differ) | 25 | — | yes | FSE 2008 | `nikolic2008collisions` |
36 +| SHA-256 | C | 23 / 24 | 2^18 / 2^28.5 | yes (pairs given) | SAC 2008 | `indesteege2008collisions` |
37 +| SHA-512 | C | 23 / 24 | 2^44.9 / 2^53.0 | theoretical | SAC 2008 | `indesteege2008collisions` |
38 +| SHA-256 | FS near-collision | 31 | — | — | SAC 2008 | `indesteege2008collisions` |
39 +| SHA-256 | C | 23 / 24 | 2^11.5 / 2^28.5 (2^15.5 with a 2^32 table) | yes (pairs given) | INDOCRYPT 2008 | `sanadhya2008new` |
40 +| SHA-512 | C | 23 / 24 | 2^16.5 / 2^32.5 (2^22.5 with a 2^64 table) | yes (first 24-step SHA-512 pair) | INDOCRYPT 2008 | `sanadhya2008new` |
41 +| SHA-256 | C | 27 | practical | yes | ASIACRYPT 2011 | `mendel2011finding` |
42 +| SHA-256 | SFS | 32 | practical | yes | ASIACRYPT 2011 | `mendel2011finding` |
43 +| SHA-256 | C | 28 | practical | yes | EUROCRYPT 2013 | `mendel2013improving` |
44 +| SHA-256 | C | 31 | ≤ 2^65.5 (two-block) | theoretical | EUROCRYPT 2013 | `mendel2013improving` |
45 +| SHA-256 | SFS | 38 | practical | yes | EUROCRYPT 2013 | `mendel2013improving` |
46 +| SHA-512 | SFS | 38 | 2^40.5 | yes | FSE 2014 | `eichlseder2014branching` |
47 +| SHA-512, SHA-512/224, SHA-512/256 | C | 27 | practical | yes | ASIACRYPT 2015 | `dobraunig2015analysis` |
48 +| SHA-512/224 ; SHA-512/256 | FS | 44 ; 43 | practical | yes | ASIACRYPT 2015 | `dobraunig2015analysis` |
49 +| SHA-256 | SFS | 39 | practical | yes (first 39-step SFS pair) | EUROCRYPT 2024 | `li2024new` |
50 +| SHA-224 | FS | 40 | practical (previous theoretical bound 2^110) | yes | EUROCRYPT 2024 | `li2024new` |
51 +| SHA-512 | C | 28 | practical | yes (first) | EUROCRYPT 2024 | `li2024new` |
52 +| SHA-512 | C | 31 | theoretical | no | EUROCRYPT 2024 | `li2024new` |
53 +| SHA-256 | C | 31 | ≈1.2 h on 64 threads, negligible memory | **yes (first practical 31-step pair)** | ASIACRYPT 2024 (best paper) | `li2024first` |
54 +| SHA-256 | C | 37 | theoretical (first attack beyond 31 steps in 12 years) | no | ePrint 2026/232 | `zhang2026collision` |
55 +| SHA-256 | SFS (SAT + CAS) | 38 | solver-based reproduction ("modified initialization vector") | yes | SC-Square 2024 | `alamgir2024sha256` |
56 +| SHA-256 | C (pure SAT, no CAS) | ≤ 28 | — | yes | SC-Square 2024 | `alamgir2024sha256` |
57 +| SHA-256 | P | 43 | 2^254.9 (pseudo-preimage 2^251.9), memory ≈ 2^6 words | theoretical | ASIACRYPT 2009 | `aoki2009preimages` |
58 +| SHA-512 | P | 46 | 2^511.5 (pseudo-preimage 2^509) | theoretical | ASIACRYPT 2009 | `aoki2009preimages` |
59 +| SHA-256 | P (hash function) | 45 | close to 2^256 (biclique) | theoretical | FSE 2012 | `khovratovich2012bicliques` |
60 +| SHA-256 | PP (compression function) | 52 | close to 2^256 (biclique) | theoretical | FSE 2012 | `khovratovich2012bicliques` |
61 +| SHA-512 | P / PP | 50 / 57 | close to 2^512 (biclique) | theoretical | FSE 2012 | `khovratovich2012bicliques` |
62 +| SHA-1 (for comparison) | C (full 80 steps) | 80 | ≈2^63.1 | yes (SHAttered) | CRYPTO 2017 | `stevens2017first` |
63 +| SHA-1 | chosen-prefix C (full) | 80 | ≈2^63.4 | yes | USENIX Security 2020 | `leurent2020sha1` |
64 +
65 +Summary of the state of the art for SHA-256 (as of 2026-11-01): practical collisions up to 31 steps,
66 +theoretical collisions up to 37 steps, practical semi-free-start collisions up to 39 steps, theoretical
67 +preimages up to 45 steps (52 for the compression function). No attack on the full 64-step function is
68 +known; the generic bounds 2^128 (collision) and 2^256 (preimage) stand.
added literature/research_gaps.md +139 −0
@@ -0,0 +1,139 @@
1 +# WP34 — Preliminary research-gap report (Stage 1, literature extraction)
2 +
3 +Date: 2026-11-01. Companion files: `db.yaml` (structured database, 21 theorems + 17 algorithms + 6
4 +assumptions + 10 bounds + 10 records + 8 open problems + 10 negative results = 82 items),
5 +`records.md` (computational records), `unverified.md` (items left out), `paper/references.bib`
6 +(129 verified entries). All citation keys below exist in `references.bib`.
7 +
8 +## 1. What is known (settled by the literature)
9 +
10 +**ECDLP, generic setting.** The square-root barrier is a theorem, not a heuristic: any generic
11 +algorithm needs Ω(√p) group operations in a prime-order group (`shoup1997lower`, `nechaev1994complexity`),
12 +and with S bits of curve-specific precomputation the online cost obeys S·T² = Ω̃(N)
13 +(`corrigangibbs2018discrete`), which rho with distinguished points matches (`bernstein2013nonuniform`).
14 +The expected constant is fixed by random-mapping statistics, √(πn/2) (`flajolet1990random`,
15 +`harris1960probability`); r-adding walks reach it for r ≥ 20 (`teske2001random`); parallel collision
16 +search gives a linear speedup in processors at the price of a 1/θ distinguished-point overhead
17 +(`vanoorschot1999parallel`).
18 +
19 +**Endomorphisms give constant factors only.** For secp256k1 (j = 0, CM discriminant −3) the order-6
20 +automorphism group yields an expected rho speedup of exactly √6 ≈ 2.45 (`duursma1999speeding`,
21 +`wiener1999faster`, `gallant2000improving`), and even the √2 of the negation map is lost unless
22 +fruitless cycles are handled correctly (`bos2010negation`, `bernstein2011correct`). On the
23 +constructive side, GLV decomposition halves the doubling chain with provable bounds on the
24 +sub-scalars (`gallant2001faster`, `sica2002analysis`, `galbraith2011endomorphisms`). The mandate's
25 +claim "the endomorphism gives at most √6 on rho" is therefore a known result, to be *reproduced*,
26 +not discovered.
27 +
28 +**Non-generic attacks do not apply to secp256k1.** MOV/Frey–Rück need a small embedding degree
29 +(`menezes1993reducing`, `frey1994remark`); the anomalous attack needs n = p (`semaev1998evaluation`,
30 +`smart1999discrete`, `satoh1998fermat`); Pohlig–Hellman needs a composite order (`pohlig1978improved`).
31 +SafeCurves checks all three for secp256k1 (`bernstein2014safecurves`). Index calculus through summation
32 +polynomials (`semaev2004summation`) works only over extension fields (`gaudry2009index`,
33 +`diem2011discrete`, `petit2012elliptic`); **no sub-exponential algorithm is known for prime-field ECDLP**
34 +(`galbraith2016recent`). Computational practice agrees: the largest completed prime-field ECDLP is
35 +112 bits (`bos2012solving`), binary-field records are 113 bits (`wenger2014solving`, `wenger2016harder`,
36 +`bernstein2016faster`), and the 131-bit Certicom challenges are open (`certicom2009challenge`).
37 +
38 +**Signatures.** Schnorr is EUF-CMA under DL in the ROM through the forking lemma
39 +(`pointcheval2000security`, `bellare2006multi`); the loss factor q_h is unavoidable for algebraic
40 +reductions and the ROM cannot be dropped (`paillier2005discrete`, `seurin2012exact`). ECDSA needs an
41 +extra idealisation of the conversion function x(R) mod n (`brown2005generic`, `vaudenay2003security`,
42 +`fersch2016provable`, `fersch2017one`), extended to additive key derivation and presignatures in the
43 +GGM (`groth2022security`). Nonce bias is fatal (`howgravegraham2001lattice`, `breitner2019biased`,
44 +`aldaya2020ladderleak`, `jancar2020minerva`), which is why RFC 6979 and BIP340 derive nonces
45 +deterministically (`pornin2013rfc6979`, `wuille2020bip340`). Batch verification of Schnorr-type
46 +signatures is a solved problem (`naccache1994can`, `bellare1998fast`, `bernstein2012high`) built on
47 +multi-scalar multiplication (`straus1964addition`, `pippenger1980evaluation`, `moller2001algorithms`).
48 +
49 +**SHA-256.** The Merkle–Damgård reduction (`merkle1989one`, `damgard1989design`) and the 2003
50 +analysis (`gilbert2003security`) remain the only structural statements about the full function.
51 +Reduced-step cryptanalysis has a clear frontier (see `records.md`): practical collisions for 31 of 64
52 +steps (`li2024first`), theoretical collisions for 37 steps (`zhang2026collision`), practical
53 +semi-free-start collisions for 39 steps (`li2024new`), preimages for 43/45 steps and 52 steps of the
54 +compression function at complexities within a factor 2-4 of brute force (`aoki2009preimages`,
55 +`khovratovich2012bicliques`). Pure SAT stalls at 28 steps; SAT+CAS reproduces the 38-step SFS result
56 +(`mironov2006applications`, `nejati2017adaptive`, `alamgir2024sha256`).
57 +
58 +## 2. What is unexplored or under-explored
59 +
60 +1. **Representation-aware generic algorithms over prime fields.** Tag tracing (`cheon2012speeding`)
61 + shows that the generic lower bound can be matched by algorithms that exploit the *representation*
62 + of F_p^* elements to make each rho step much cheaper. No elliptic-curve analogue is published. The
63 + question of whether the x-only / Jacobian representation of secp256k1 admits a cheaper iteration
64 + function (fewer field multiplications per step, not fewer steps) is open and is testable at scale.
65 +2. **Joint use of the order-6 orbit and distinguished points.** The √6 gain is analysed asymptotically;
66 + the interaction between orbit canonicalisation cost, fruitless 6-cycles and the distinguished-point
67 + overhead 1/θ has not, to our knowledge, been measured systematically across group sizes with
68 + confidence intervals. This is a reproducibility target (expected outcome: *known result reproduced*).
69 +3. **Preprocessing trade-offs on a fixed curve.** The S·T² curve of `corrigangibbs2018discrete` has
70 + been matched in theory (`bernstein2013nonuniform`, `lee2011generic`) but concrete, seed-controlled
71 + measurements on synthetic prime-order curves at 30-60 bits with varying S are rare; they would
72 + calibrate what "free precomputation" means for a single standardised curve.
73 +4. **Sub-exponential ECDLP over F_p.** Nothing in the literature even suggests a factor base. Any
74 + hypothesis here must be framed as a negative-control experiment (summation-polynomial decomposition
75 + over small prime fields with explicit Gröbner cost), with the expected classification *rejected*.
76 +5. **Verification-side algebra (Axis B).** Fast BIP340 batch verification is implemented in
77 + `wuille2013libsecp256k1`; what is unexplored is a rigorous, bit-exact catalogue of which algebraic
78 + rearrangements of sG = R + eP (GLV on both terms, shared-doubling interleaving, random-linear-combination
79 + batching, x-only recovery of R) are *new identities* versus reformulations, with operation counts.
80 + `groth2022security` is also the first place to look before claiming anything about derived keys.
81 +6. **SHA-256 reduced-step regularities with automated search (Axis C).** The frontier tools are
82 + hand-tuned characteristic searchers (`mendel2011finding`, `mendel2013improving`, `eichlseder2014branching`)
83 + and recent SAT/MILP models (`li2024new`, `zhang2026collision`). Open: can solver-based search
84 + (CaDiCaL 2.0, Kissat, Z3 — `biere2024cadical`, `biere2020cadical`, `demoura2008z3`) reproduce
85 + the 24- and 27-step practical collisions from scratch, and how do solver conflicts scale in the
86 + number of steps? The published negative result (28 steps for pure SAT, `alamgir2024sha256`) is the
87 + baseline to beat or confirm.
88 +7. **Formal verification of reduced-round identities.** `boving2025interactive` makes QF_BV facts
89 + about SHA-256 step functions machine-checkable in Lean 4 (`demoura2021lean`); no published work
90 + uses it for cryptanalytic bookkeeping (e.g. proving that a differential characteristic is
91 + contradiction-free for k steps).
92 +8. **Automated discovery baselines.** AlphaTensor, FunSearch, AlphaGeometry, AlphaEvolve and the
93 + AI Scientist (`fawzi2022discovering`, `romeraparedes2024mathematical`, `trinh2024solving`,
94 + `novikov2025alphaevolve`, `lu2024ai`, `davies2021advancing`) show program search can produce
95 + verified constructions; none targets standardised cryptographic primitives, so Axis D has no
96 + published baseline and must define one (e.g. rediscovery rate of the √2 / √6 / GLV results on
97 + synthetic groups).
98 +
99 +## 3. Mandate claims already settled
100 +
101 +| Mandate claim / question | Status | Sources |
102 +|---|---|---|
103 +| Generic baseline T(n) = O(√n) | theorem (upper and lower bound) | `shanks1971class`, `pollard1978monte`, `shoup1997lower` |
104 +| Endomorphism gives "meaningful improvements beyond constant factors" (A1) | **No**: at most √6 on rho, ≈2× on scalar multiplication | `duursma1999speeding`, `gallant2001faster` |
105 +| Equivalence classes / orbits reduce redundant computation (A2) | Yes, by exactly √|Aut| with fruitless-cycle cost | `wiener1999faster`, `bernstein2011correct` |
106 +| Candidate algorithms provably cannot beat lower bounds (A5) | True for generic algorithms, with and without preprocessing | `shoup1997lower`, `corrigangibbs2018discrete` |
107 +| secp256k1's special form makes it insecure | No known attack; all standard exclusion criteria pass | `bernstein2014safecurves`, `galbraith2016recent` |
108 +| Sub-exponential prime-field ECDLP | none known | `galbraith2016recent` |
109 +| ECDSA vs Schnorr proof structure (B1) | Schnorr: DL + ROM + forking; ECDSA: additionally idealised conversion / GGM | `pointcheval2000security`, `fersch2016provable`, `brown2005generic` |
110 +| Batch verification without consensus change (B2) | known for Schnorr/BIP340, not for ECDSA | `bellare1998fast`, `bernstein2012high`, `wuille2020bip340` |
111 +| Reduced-round SHA-256 regularities (C1) | exploited up to 31 (practical) / 37 (theoretical) steps | `li2024first`, `zhang2026collision` |
112 +| SAT/SMT for reduced-round SHA-256 (C2, C4) | pure SAT ≤ 28 steps; SAT+CAS 38-step SFS | `alamgir2024sha256` |
113 +| Generic hash benchmarks 2^256 / 2^128 | standard; no full-round attack | `nist2015fips180`, `gilbert2003security` |
114 +
115 +## 4. Candidate directions worth testing computationally (for Stage 2)
116 +
117 +- **D1 (Axis A, reproduction):** measure rho / negation / order-6 constants on synthetic j = 0 curves at
118 + 24-56 bits, ≥ 10 seeds each, fit T = a·n^α(log n)^β and confirm α = 1/2 and the √2, √6 ratios with
119 + bootstrap CIs. Expected classification: *known result reproduced*.
120 +- **D2 (Axis A, exploratory):** cost per rho iteration under alternative representations (affine with
121 + Montgomery's simultaneous inversion, x-only with orbit canonicalisation, Jacobian) — a representation
122 + result in the spirit of `cheon2012speeding`; success criterion is field multiplications per step.
123 +- **D3 (Axis A, negative control):** summation-polynomial decomposition over small F_p with explicit
124 + Gröbner-basis cost versus rho; expected *rejected*.
125 +- **D4 (Axis A, preprocessing):** S·T² measurements on a fixed synthetic curve with S from 2^8 to 2^20.
126 +- **D5 (Axis B):** operation-count benchmark of BIP340 single vs batch verification (N = 1…1024) and
127 + ECDSA verification with/without GLV, against `wuille2013libsecp256k1`; identity catalogue checked by
128 + exact arithmetic and, where possible, by Lean (`boving2025interactive`).
129 +- **D6 (Axis C):** SAT encodings of k-step SHA-256 collision and preimage instances for k = 16…28 with
130 + CaDiCaL/Kissat/Z3, conflict counts and wall-clock, with a random-function control; compare with the
131 + published 24/27/28-step practical results.
132 +- **D7 (Axis C):** differential-probability measurements of published local collisions as the step count
133 + grows (mandate C3), using the characteristics of `mendel2013improving` as the reference.
134 +- **D8 (Axis D):** an automated hypothesis loop whose scoring is the classification vocabulary of
135 + `CLAUDE.md`, evaluated by its ability to rediscover D1 results and reject D3; baseline = random
136 + hypothesis sampling.
137 +
138 +None of these directions assumes that a shortcut exists; D1, D3 and D6 are explicitly expected to
139 +return known or negative results, which the mandate counts as valid outcomes (Outcome D).
added literature/unverified.md +25 −0
@@ -0,0 +1,25 @@
1 +# Items not included in `paper/references.bib` (could not be verified, or verified as incorrect)
2 +
3 +Date: 2026-11-01. Every entry in `references.bib` was checked against Crossref, IACR ePrint, arXiv,
4 +DOI resolution, a publisher page or the official standard URL. The items below were requested in the
5 +Stage-1 brief but were either not confirmable or turned out to be mis-stated; they are deliberately
6 +absent from the bibliography (or included only with an explicit caveat).
7 +
8 +| Requested item | Status | What was found |
9 +|---|---|---|
10 +| "ECDLP 117.35-bit" record (2016) | **Not found — excluded** | No source (ePrint 2016/382 abstract, the cr.yp.to copy of the paper, web search) contains a 117.35-bit figure. Bernstein–Engels–Lange–Niederhagen–Paar–Schwabe–Zimmermann 2016 attacked `sect113r2` ("a prime order very slightly larger than the previous record holder"); the same paper benchmarks, but does not solve, a curve over a 127-bit field. Only the sect113r2 fact is recorded in `records.md`. |
11 +| "114-bit" or any prime-field ECDLP record after 2009 | **Not found — excluded** | Searches returned no completed prime-field ECDLP above the 112-bit computation of Bos et al. (July 2009, published 2012). The Certicom ECCp-131 challenge is listed as open in the 2009 challenge document. |
12 +| Lee–Cheon–Hong 2011 "discrete log with preprocessing" | **Included with corrected title** | The real document is IACR ePrint 2011/187, *Accelerating ID-based Encryption based on Trapdoor DL using Pre-computation* (Hyung Tae Lee, Jung Hee Cheon, Jin Hong). It is the paper cited by Corrigan-Gibbs–Kogan as a preprocessing DL algorithm; the brief's title did not match any record. |
13 +| "Aldaya et al. 2020 (LadderLeak)" | **Author list corrected** | LadderLeak (CCS 2020, DOI 10.1145/3372297.3417268) is by Aranha, Novaes, Takahashi, Tibouchi and Yarom. Key `aldaya2020ladderleak`. |
14 +| Satoh–Araki 1998 | **Included with caveat** | *Comment. Math. Univ. St. Pauli* 47(1):81–92 has no DOI and no Crossref record. Volume/pages were confirmed through three independent citing sources (reference lists, the 1997 Waterloo ECC workshop programme, and a USPTO document). |
15 +| ECC2K-130 "broken" (ePrint 2009/541, Bailey et al.) | **Excluded** | The ePrint abstract says "describes breaking the ECC2K-130 challenge", but the Certicom challenge document (Nov. 2009) lists ECC2K-130 without an end date and no completion announcement was found. Not recorded as a solved instance. |
16 +| Gilbert–Handschuh 2003 numeric claims (e.g. "9-round collision", "2^9 differential") | **Numbers not confirmed** | The paper is verified (SAC 2003, LNCS 3006, pp. 175–193) and included; only its qualitative conclusions (Chabaud–Joux / Dobbertin-style attacks do not apply; XOR-linearised variant is weak) are used in `db.yaml`. |
17 +| Nejati et al. 2018 "programmatic SAT for SHA-1/SHA-256" | **Different paper exists — not included** | The 2018 paper found is *Algebraic Fault Attack on SHA Hash Functions Using Programmatic SAT Solvers* (Nejati, Horáček, Gebotys, Ganesh; CP 2018, DOI 10.1007/978-3-319-98334-9_47). It concerns fault attacks, not collision/preimage search, so only the 2017 VSTTE paper (`nejati2017adaptive`) and `alamgir2024sha256` are cited for SAT-based SHA-256 work. The arXiv note *CDCL(Crypto) SAT Solvers for Cryptanalysis* (Nejati–Ganesh, arXiv:2005.13415) exists but was not needed. |
18 +| Harrison (HOL Light) | **Optional — omitted** | Not required by the mandate; not searched. |
19 +| Bernstein–Lange 2017 *Post-quantum cryptography* (Nature 549:188–194, DOI 10.1038/nature23461) | **Verified but omitted** | Verified via Crossref; left out to keep the bibliography focused (the implications section cites Shor 1997 and Roetteler et al. 2017). Can be added on request. |
20 +| Nikolić–Biryukov 2008 ePrint | **n/a** | No ePrint version; FSE 2008 DOI used. |
21 +| Kissat system descriptions 2021–2024 | **Not individually verified** | Only the 2020 SAT Competition description (JKU record: Univ. Helsinki Report Series B, B-2020-1, pp. 50–53) and CaDiCaL 2.0 (CAV 2024, DOI) were verified. |
22 +| LNCS volume numbers | **From series knowledge, not Crossref** | Crossref does not return LNCS volume numbers for Springer chapters; DOI, pages and year were verified for every chapter, volume numbers were added from the standard LNCS numbering and should be treated as secondary metadata. The ASIACRYPT 2024 volume for `li2024first` was deliberately left blank. |
23 +| Blackburn–Murphy 1998 "The number of partitions in Pollard rho" | **Not found — excluded** | No Crossref/DOI record; it is an unpublished manuscript cited by Teske. `teske1998speeding` / `teske2001random` are cited for the r-adding-walk partition count instead. |
24 +| Cornacchia 1908 | **Optional — omitted** | Not searched (not needed by the manuscript). |
25 +| Cheon–Hong–Kim 2012 "Speeding up the Pollard rho method on prime fields" | **Title corrected** | The J. Cryptology 25(2):195–242 paper is titled *Accelerating Pollard's Rho Algorithm on Finite Fields*; the ASIACRYPT 2008 version carries the "Speeding up ..." title. Key `cheon2012speeding` points to the journal version with a note. |
added paper/.latexmkrc +5 −0
@@ -0,0 +1,5 @@
1 +$pdf_mode = 1;
2 +$pdflatex = 'pdflatex -interaction=nonstopmode -halt-on-error -synctex=1 %O %S';
3 +$bibtex_use = 2;
4 +$clean_ext = 'synctex.gz run.xml bbl bcf fdb_latexmk fls log aux out toc lof lot blg';
5 +@default_files = ('main.tex');
added paper/Makefile +11 −0
@@ -0,0 +1,11 @@
1 +.PHONY: all clean distclean
2 +
3 +all:
4 + latexmk
5 +
6 +clean:
7 + latexmk -c
8 +
9 +distclean:
10 + latexmk -C
11 + rm -f *.synctex.gz *.run.xml *.bbl *.bcf
added paper/appendix/app_results.tex +1 −0
@@ -0,0 +1 @@
1 +% additional result tables (generated/edited later)
added paper/appendix/appendix.tex +80 −0
@@ -0,0 +1,80 @@
1 +% ============================================================================
2 +% Technical Appendices
3 +% ============================================================================
4 +\newpage
5 +\begin{appendices}
6 +\renewcommand{\thetable}{A\arabic{table}}
7 +\renewcommand{\thefigure}{A\arabic{figure}}
8 +\setcounter{table}{0}
9 +\setcounter{figure}{0}
10 +
11 +\section{Reproducibility statement}
12 +\label{app:repro}
13 +
14 +The repository \code{wp34\_uqo} (UQO working-paper series, \code{mathematiques/beyond-square-root-barrier}) contains
15 +every script, result file and figure source. All experiments are re-runnable from the repository root:
16 +\begin{lstlisting}
17 +uv venv --python 3.12 .venv && uv pip install -r requirements.txt # Python environment
18 +.venv/bin/python -m pytest -q tests/ # cross-implementation gates
19 +.venv/bin/python experiments/baselines/secp256k1_facts.py # Table 2 (needs PARI/GP `gp`)
20 +.venv/bin/python experiments/baselines/scalar_mult_opcount.py # GLV operation counts
21 +.venv/bin/python theory/elliptic_curves/symbolic_checks.py # SymPy checks S1-S6
22 +.venv/bin/python theory/elliptic_curves/orbit_quotient_counterexample.py # H05
23 +.venv/bin/python experiments/synthetic_ecdlp/index_calculus_probe.py # H06
24 +.venv/bin/python experiments/synthetic_ecdlp/preprocessing_python.py # H12
25 +.venv/bin/python experiments/automated_discovery/identity_search.py # H02, H07
26 +cargo build --release --manifest-path experiments/synthetic_ecdlp/rho_engine/Cargo.toml
27 +.venv/bin/python experiments/synthetic_ecdlp/run_scaling.py --sizes 20,24,28,32,36,40 # H08-H11 (see README for the cluster split)
28 +cargo run --release --manifest-path experiments/signature_benchmarks/sigbench/Cargo.toml -- ... # H13-H14 (run_bench.py)
29 +.venv/bin/python experiments/reduced_round_sha256/diffusion.py / bias_test.py / preimage_sat.py / collision_sat.py / local_collision.py
30 +cd formal && lake build # H21, H22, H25 (Lean 4.35)
31 +.venv/bin/python experiments/automated_discovery/registry.py # discovery log + Table of hypotheses
32 +.venv/bin/python scripts/make_tables.py && cd paper && make # tables/numbers.tex, main.pdf
33 +\end{lstlisting}
34 +Every result JSON carries a provenance block (host name, CPU, cores, RAM, OS, Python and library versions, seed,
35 +command line, UTC timestamp, elapsed time). Random seeds are fixed in the scripts (default 34) and recorded.
36 +Raw solver outputs and the Rust batch outputs are kept next to the aggregated files.
37 +
38 +\section{Computational environment}
39 +\label{app:env}
40 +
41 +\begin{table}[htbp]
42 + \centering\footnotesize
43 + \begin{threeparttable}
44 + \caption{Hardware and software used (measured, from the provenance blocks).}
45 + \label{tab:env}
46 + \begin{tabular}{@{}llllll@{}}
47 + \toprule
48 + Role & Machine & CPU & Cores & RAM & Used for \\
49 + \midrule
50 + development, reference checks, figures & laptop & Apple M5 Max & 18 & 48 GB & tests, SymPy/PARI, Python experiments, signature benchmarks \\
51 + benchmarking & M3U96a (Mac Studio) & Apple M3 Ultra & 28 & 96 GB & signature benchmarks (second hardware point), ECDLP scaling, parallel efficiency \\
52 + benchmarking & M3U96b (Mac Studio) & Apple M3 Ultra & 28 & 96 GB & SAT/SMT grids, ECDLP scaling \\
53 + \bottomrule
54 + \end{tabular}
55 + \begin{tablenotes}\footnotesize
56 + \item Software: macOS 27.0; Python 3.12 (uv), SymPy 1.14, NumPy 2.5, python-sat 1.9 (CaDiCaL 1.9.5 bindings), Z3 4.15 (\code{z3-solver}), CaDiCaL 2.x and Kissat 4.x binaries, PARI/GP 2.19; Rust 1.95--1.96 (\code{k256} 0.14, \code{secp256k1} 0.33 / \libsecp{}); Lean 4.35.0 (\code{bv\_decide} with bundled CaDiCaL); TeX Live 2026. Exact versions are in each result file. No GPU was used.
57 + \end{tablenotes}
58 + \end{threeparttable}
59 +\end{table}
60 +
61 +\section{Full secp256k1 constants}
62 +\label{app:constants}
63 +\begin{lstlisting}
64 +p = FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F
65 +n = FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141
66 +G.x = 79BE667E F9DCBBAC 55A06295 CE870B07 029BFCDB 2DCE28D9 59F2815B 16F81798
67 +G.y = 483ADA77 26A3C465 5DA4FBFC 0E1108A8 FD17B448 A6855419 9C47D08F FB10D4B8
68 +beta = 7AE96A2B 657C0710 6E64479E AC3434E9 9CF04975 12F58995 C1396C28 719501EE
69 +lambda = 5363AD4C C05C30E0 A5261C02 8812645A 122E22EA 20816678 DF02967C 1B23BD72
70 +a1 = 3086D221 A7D46BCD E86C90E4 9284EB15 (= (t - f)/2 - 1)
71 +b1 = -E4437ED6 010E8828 6F547FA9 0ABFE4C3 (= -f)
72 +a2 = 114CA50F 7A8E2F3F 657C1108 D9D44CFD8 (= a1 - b1 = (t + f)/2 - 1)
73 +b2 = a1
74 +t = 432420386565659656852420866390673177327 (trace of Frobenius)
75 +f = 303414439467246543595250775667605759171 (4p = t^2 + 3 f^2)
76 +\end{lstlisting}
77 +
78 +\input{appendix/app_results}
79 +
80 +\end{appendices}
added paper/figures/ec_geometry.pdf +0 −0

Binary file not shown.

added paper/figures/ecdlp_constants.pdf +0 −0

Binary file not shown.

added paper/figures/ecdlp_endomorphism.pdf +0 −0

Binary file not shown.

added paper/figures/ecdlp_parallel.pdf +0 −0

Binary file not shown.

added paper/figures/ecdlp_scaling.pdf +0 −0

Binary file not shown.

added paper/figures/ecdlp_sweep_r.pdf +0 −0

Binary file not shown.

added paper/figures/ecdlp_tmto.pdf +0 −0

Binary file not shown.

added paper/figures/fig_dependency_map.tex +31 −0
@@ -0,0 +1,31 @@
1 +% Illustrative diagram (not generated from data): cryptographic dependency map of Bitcoin.
2 +\begin{tikzpicture}[
3 + font=\small, >=Latex,
4 + prim/.style={draw, rounded corners=2pt, fill=blue!8, text width=3.6cm, minimum height=0.9cm, align=center},
5 + math/.style={draw, rounded corners=2pt, fill=orange!12, text width=3.6cm, minimum height=0.9cm, align=center},
6 + app/.style={draw, rounded corners=2pt, fill=green!10, text width=3.6cm, minimum height=0.9cm, align=center},
7 + assm/.style={draw, dashed, rounded corners=2pt, fill=red!6, text width=3.6cm, minimum height=0.9cm, align=center},
8 + every edge/.style={draw, ->, thick}]
9 + \node[assm] (ecdlp) at (0,0) {ECDLP on \secp{}\\ $\Theta(\sqrt{n})$ generic};
10 + \node[assm] (pre) at (5.4,0) {SHA-256 preimage\\ $\approx 2^{256}$};
11 + \node[assm] (col) at (10.8,0) {SHA-256 collision\\ $\approx 2^{128}$};
12 + \node[math] (field) at (0,1.7) {$\F_p$, $p=2^{256}-2^{32}-977$};
13 + \node[math] (curve) at (0,3.1) {$E:\ y^2=x^3+7$, $\#E=n$ prime};
14 + \node[math] (md) at (8.1,1.7) {Merkle--Damg\aa rd + 64-step compression $f$};
15 + \node[prim, text width=3.2cm] (ecdsa) at (-2.5,5.0) {ECDSA\\ (RFC 6979 nonces)};
16 + \node[prim, text width=3.2cm] (schnorr) at (2.5,5.0) {BIP340 Schnorr\\ (x-only keys, tagged hashes)};
17 + \node[prim] (hash) at (8.1,3.3) {SHA-256, SHA-256d, RIPEMD-160(SHA-256)};
18 + \node[prim] (tagged) at (8.1,5.4) {Tagged hashes, Merkle trees, HMAC/PBKDF2 (BIP32/39)};
19 + \node[app] (addr) at (0,7.3) {Addresses (P2PKH, P2WPKH, P2TR)};
20 + \node[app] (tx) at (5.4,7.3) {Transaction authorization (scriptSig / witness)};
21 + \node[app] (pow) at (10.8,7.3) {Proof of work (block header SHA-256d)};
22 + \node[app, text width=7cm] (cons) at (5.4,8.8) {Consensus: block chain, UTXO set, difficulty};
23 + \path (ecdlp) edge (field) (field) edge (curve) (curve) edge (ecdsa) (curve) edge (schnorr);
24 + \path (pre) edge (md) (col) edge (md) (md) edge (hash) (hash) edge (tagged);
25 + \path (hash.west) edge (schnorr.east) (hash.west) edge[bend right=12] (ecdsa.south east);
26 + \path (ecdsa) edge (addr) (schnorr) edge (addr) (ecdsa) edge (tx) (schnorr) edge (tx) (tagged) edge (tx) (hash.east) edge[bend right=20] (pow.south) (tagged) edge (addr);
27 + \path (addr) edge (cons) (tx) edge (cons) (pow) edge (cons);
28 + \begin{scope}[on background layer]
29 + \node[fit=(ecdlp)(col), draw=red!50, dashed, rounded corners, inner sep=5pt, label={[red!60!black,font=\footnotesize]below:hardness assumptions studied in this paper (Axes A--C)}] {};
30 + \end{scope}
31 +\end{tikzpicture}
added paper/figures/fig_pipeline.tex +21 −0
@@ -0,0 +1,21 @@
1 +% Illustrative diagram: the six-stage autonomous discovery pipeline of WP34 (implemented in experiments/automated_discovery).
2 +\begin{tikzpicture}[font=\small, >=Latex,
3 + st/.style={draw, rounded corners=3pt, fill=blue!7, minimum height=1.55cm, text width=3.3cm, align=center},
4 + art/.style={draw, dashed, rounded corners=3pt, fill=gray!6, text width=3.3cm, align=center, font=\footnotesize}]
5 + \node[st] (s1) at (0,0) {1. Literature extraction\\ \footnotesize structured DB, verified BibTeX};
6 + \node[st] (s2) at (4.3,0) {2. Hypothesis generation\\ \footnotesize 25 structured records, falsifiable};
7 + \node[st] (s3) at (8.6,0) {3. Symbolic investigation\\ \footnotesize SymPy / PARI exact checks, identity search};
8 + \node[st] (s4) at (8.6,-3.4) {4. Experimental evaluation\\ \footnotesize Rust engine, SAT/SMT, benchmarks, seeds};
9 + \node[st] (s5) at (4.3,-3.4) {5. Formal verification\\ \footnotesize Lean~4 \code{bv\_decide}, no \code{sorry}};
10 + \node[st] (s6) at (0,-3.4) {6. Scientific evaluation\\ \footnotesize 6-way classification, discovery log};
11 + \node[art] (a1) at (0,1.7) {\code{literature/db.yaml}, \code{references.bib}};
12 + \node[art] (a2) at (4.3,1.7) {\code{hypotheses/H*.md}};
13 + \node[art] (a3) at (8.6,1.7) {\code{results/theory/*}, \code{identity\_search.json}};
14 + \node[art] (a4) at (8.6,-5.1) {\code{results/**/*.json} with provenance block};
15 + \node[art] (a5) at (4.3,-5.1) {\code{formal/} theorems, \code{build.log}};
16 + \node[art] (a6) at (0,-5.1) {\code{discovery\_log.jsonl}, Table~\ref{tab:hypotheses}};
17 + \draw[->] (s1) -- (s2); \draw[->] (s2) -- (s3); \draw[->] (s3) -- (s4); \draw[->] (s4) -- (s5); \draw[->] (s5) -- (s6);
18 + \draw[->, dashed] (s6.north) to[out=70,in=-110] node[pos=0.5, left, font=\footnotesize]{refine / reject} (s2.south);
19 + \draw[->, dashed] (s4.west) to[out=160,in=-20] node[pos=0.5, above, font=\footnotesize, xshift=-2mm]{counterexample} (s3.south west);
20 + \foreach \a/\s in {a1/s1,a2/s2,a3/s3,a4/s4,a5/s5,a6/s6} { \draw[gray, dotted] (\a) -- (\s); }
21 +\end{tikzpicture}
added paper/figures/fig_secp256k1_structure.tex +13 −0
@@ -0,0 +1,13 @@
1 +% Illustrative diagram (verified constants; layout is schematic): algebraic structure of secp256k1.
2 +\begin{tikzpicture}[font=\small, >=Latex, node distance=0.9cm,
3 + box/.style={draw, rounded corners=2pt, fill=gray!6, align=center, inner sep=4pt},
4 + key/.style={draw, rounded corners=2pt, fill=blue!8, align=center, inner sep=4pt}]
5 + \node[key] (E) {$E/\F_p:\ y^2 = x^3 + 7$,\quad $j(E)=0$,\quad $\#E(\F_p)=n$ prime,\quad $h=1$};
6 + \node[box, below=of E, xshift=-4.2cm] (cm) {CM by $\Z[\omega]$, $\omega^2+\omega+1=0$\\ $\End(E)=\Z[\omega]$, $\Aut(E)=\langle -\phi\rangle\cong\Z/6$\\ $4p = t^2 + 3f^2$, $n = p+1-t$};
7 + \node[box, below=of E, xshift=4.2cm] (phi) {$\phi(x,y)=(\beta x,\,y)$, $\beta^3\equiv 1\ (p)$\\ $\phi(P)=[\lambda]P$, $\lambda^3\equiv 1\ (n)$\\ $\lambda^2+\lambda+1\equiv 0\ (n)$};
8 + \node[box, below=of cm] (orbit) {orbits $\{\pm P,\pm\phi P,\pm\phi^2 P\}$ of size 6\\ $x$-coordinates: $\{x,\beta x,\beta^2 x\}$\\ $\Rightarrow$ rho speed-up $\le\sqrt{6}$};
9 + \node[box, below=of phi] (glv) {GLV lattice $L=\{(k_1,k_2):k_1+k_2\lambda\equiv 0\ (n)\}$\\ $=(\bar\pi-1)\Z[\omega]$, basis $(a_1,b_1)=\big(\tfrac{t-f}{2}-1,\,-f\big)$,\\ $(a_2,b_2)=(a_1-b_1,\,a_1)$, $\ a_1^2-a_1b_1+b_1^2=n$};
10 + \node[box, below=of orbit, xshift=4.2cm] (sec) {$n\ne p$ (not anomalous)\quad embedding degree $> 10^{6}$ (no MOV)\\ twist order $=3^2\cdot 13^2\cdot 3319\cdot 22639\cdot q_{220}$\quad no $x=0$ points\\ special prime $p=2^{256}-2^{32}-977$: fast reduction only};
11 + \draw[->] (E) -- (cm); \draw[->] (E) -- (phi); \draw[->] (cm) -- (orbit); \draw[->] (phi) -- (glv); \draw[->] (phi) -- (orbit);
12 + \draw[->] (orbit) -- (sec); \draw[->] (glv) -- (sec);
13 +\end{tikzpicture}
added paper/figures/fig_sha256_architecture.tex +27 −0
@@ -0,0 +1,27 @@
1 +% Illustrative diagram (exact data flow of FIPS 180-4): SHA-256 compression function.
2 +\begin{tikzpicture}[font=\footnotesize, >=Latex,
3 + reg/.style={draw, minimum width=0.9cm, minimum height=0.6cm, fill=blue!8},
4 + op/.style={draw, circle, inner sep=1pt, fill=orange!15},
5 + fn/.style={draw, rounded corners=2pt, fill=green!10, inner sep=3pt}]
6 + % registers at step t
7 + \foreach \r/\i in {a/0,b/1,c/2,d/3,e/4,f/5,g/6,h/7} { \node[reg] (\r) at (\i*1.25, 3) {$\r_t$}; }
8 + \foreach \r/\i in {a/0,b/1,c/2,d/3,e/4,f/5,g/6,h/7} { \node[reg] (\r 2) at (\i*1.25, -1.2) {$\r_{t+1}$}; }
9 + % functions
10 + \node[fn] (S0) at (0.4,1.9) {$\SigZ(a)$};
11 + \node[fn] (Maj) at (1.9,1.9) {$\Maj(a,b,c)$};
12 + \node[fn] (S1) at (5.4,1.9) {$\SigO(e)$};
13 + \node[fn] (Ch) at (7.0,1.9) {$\Ch(e,f,g)$};
14 + \node[fn] (KW) at (8.9,0.9) {$K_t \boxplus W_t$};
15 + \node[op] (T2) at (1.1,0.9) {$\boxplus$};
16 + \node[op] (T1) at (6.6,0.9) {$\boxplus$};
17 + \node[op] (A) at (0,0.0) {$\boxplus$};
18 + \node[op] (Ea) at (3.75,0.0) {$\boxplus$};
19 + \draw[->] (a) -- (S0); \draw[->] (a) -- (Maj); \draw[->] (b) -- (Maj); \draw[->] (c) -- (Maj);
20 + \draw[->] (S0) -- (T2); \draw[->] (Maj) -- (T2);
21 + \draw[->] (e) -- (S1); \draw[->] (e) -- (Ch); \draw[->] (f) -- (Ch); \draw[->] (g) -- (Ch); \draw[->] (h) -- ++(0,-0.5) -| (T1);
22 + \draw[->] (S1) -- (T1); \draw[->] (Ch) -- (T1); \draw[->] (KW) -- (T1);
23 + \node at (6.6,0.35) {$T_1$}; \node at (1.1,0.35) {$T_2$};
24 + \draw[->] (T1) -- (A); \draw[->] (T2) -- (A); \draw[->] (A) -- (a2);
25 + \draw[->] (d) -- ++(0,-1.9) -- (Ea); \draw[->] (T1) -- (Ea); \draw[->] (Ea) -- (e2);
26 + \foreach \s/\t in {a/b2,b/c2,c/d2,e/f2,f/g2,g/h2} { \draw[->, gray] (\s.south) to[out=-90,in=90] (\t.north); }
27 +\end{tikzpicture}
added paper/figures/sig_batch_verification.pdf +0 −0

Binary file not shown.

added paper/figures/sig_implementations.pdf +0 −0

Binary file not shown.

added paper/main.pdf +0 −0

Binary file not shown.

added paper/main.tex +140 −0
@@ -0,0 +1,140 @@
1 +% ============================================================================
2 +% UQO Working Paper 34 — Beyond the Square-Root Barrier
3 +% ============================================================================
4 +\documentclass[12pt,letterpaper]{article}
5 +\usepackage[utf8]{inputenc}
6 +\usepackage[T1]{fontenc}
7 +\usepackage[english]{babel}
8 +\usepackage[letterpaper,top=1in,bottom=1in,left=1in,right=1in]{geometry}
9 +\usepackage{mathptmx}
10 +\usepackage{microtype}
11 +\usepackage{setspace}
12 +\onehalfspacing
13 +\usepackage{amsmath,amssymb,amsthm}
14 +\usepackage{bm}
15 +\usepackage{booktabs}
16 +\usepackage{tabularx}
17 +\usepackage{multirow}
18 +\usepackage{threeparttable}
19 +\usepackage{longtable,array,calc}
20 +\usepackage{siunitx}
21 +\sisetup{input-symbols = {()}, parse-numbers = false}
22 +\usepackage{graphicx}
23 +\usepackage{subcaption}
24 +\usepackage{float}
25 +\usepackage{tikz}
26 +\usetikzlibrary{arrows.meta,positioning,calc,fit,backgrounds,shapes.misc,shapes.geometric,matrix,chains}
27 +\usepackage[dvipsnames]{xcolor}
28 +\usepackage[colorlinks=true,linkcolor=NavyBlue,citecolor=NavyBlue,urlcolor=NavyBlue,breaklinks=true]{hyperref}
29 +\usepackage[authoryear,round,semicolon]{natbib}
30 +\bibliographystyle{aer}
31 +\usepackage[font=small,labelfont=bf,labelsep=period,justification=justified,singlelinecheck=false]{caption}
32 +\usepackage{fancyhdr}
33 +\pagestyle{fancy}
34 +\fancyhf{}
35 +\renewcommand{\headrulewidth}{0pt}
36 +\fancyfoot[C]{\thepage}
37 +\usepackage{titlesec}
38 +\titleformat{\section}{\large\bfseries}{\thesection.}{0.5em}{}
39 +\titleformat{\subsection}{\normalsize\bfseries}{\thesubsection.}{0.5em}{}
40 +\titleformat{\subsubsection}{\normalsize\itshape}{\thesubsubsection.}{0.5em}{}
41 +\usepackage[toc,page]{appendix}
42 +\usepackage{enumitem}
43 +\setlist{nosep,leftmargin=*}
44 +\usepackage{algorithm}
45 +\usepackage{algpseudocode}
46 +\usepackage{listings}
47 +\lstset{basicstyle=\ttfamily\footnotesize,breaklines=true,columns=fullflexible,frame=single,framerule=0.2pt}
48 +
49 +\theoremstyle{plain}
50 +\newtheorem{theorem}{Theorem}[section]
51 +\newtheorem{proposition}[theorem]{Proposition}
52 +\newtheorem{lemma}[theorem]{Lemma}
53 +\newtheorem{corollary}[theorem]{Corollary}
54 +\newtheorem{fact}[theorem]{Fact}
55 +\theoremstyle{definition}
56 +\newtheorem{definition}[theorem]{Definition}
57 +\newtheorem{assumption}[theorem]{Assumption}
58 +\newtheorem{hypothesis}[theorem]{Hypothesis}
59 +\newtheorem{conjecture}[theorem]{Conjecture}
60 +\newtheorem{observation}[theorem]{Observation}
61 +\theoremstyle{remark}
62 +\newtheorem{remark}[theorem]{Remark}
63 +\newtheorem{example}[theorem]{Example}
64 +
65 +\newcommand{\F}{\mathbb{F}}
66 +\newcommand{\Z}{\mathbb{Z}}
67 +\newcommand{\Q}{\mathbb{Q}}
68 +\newcommand{\N}{\mathbb{N}}
69 +\newcommand{\R}{\mathbb{R}}
70 +\newcommand{\Fp}{\mathbb{F}_p}
71 +\newcommand{\E}{\mathcal{E}}
72 +\newcommand{\Oinf}{\mathcal{O}}
73 +\newcommand{\End}{\operatorname{End}}
74 +\newcommand{\Aut}{\operatorname{Aut}}
75 +\newcommand{\ord}{\operatorname{ord}}
76 +\newcommand{\Norm}{\operatorname{N}}
77 +\newcommand{\Tr}{\operatorname{Tr}}
78 +\newcommand{\Ch}{\mathrm{Ch}}
79 +\newcommand{\Maj}{\mathrm{Maj}}
80 +\newcommand{\SigZ}{\Sigma_0}
81 +\newcommand{\SigO}{\Sigma_1}
82 +\newcommand{\sigz}{\sigma_0}
83 +\newcommand{\sigo}{\sigma_1}
84 +\newcommand{\rotr}{\mathrm{R}}
85 +\newcommand{\shr}{\mathrm{S}}
86 +\newcommand{\secp}{\textsf{secp256k1}}
87 +\newcommand{\libsecp}{\texttt{libsecp256k1}}
88 +\newcommand{\kk}{\texttt{k256}}
89 +\newcommand{\code}[1]{\texttt{#1}}
90 +\newcommand{\Hname}[1]{\textsf{#1}}
91 +\providecommand{\tightlist}{\setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}}
92 +\graphicspath{{figures/}}
93 +
94 +% Numbers generated by scripts (never typed by hand): paper/tables/numbers.tex
95 +\input{tables/numbers}
96 +\input{tables/numbers_ecdlp}
97 +
98 +% ============================================================================
99 +\newcommand{\WPnumber}{34}
100 +\newcommand{\WPtitle}{Beyond the Square-Root Barrier: Automated Discovery of Algebraic and Algorithmic Shortcuts in Bitcoin Cryptography}
101 +\newcommand{\WPsubtitle}{A hypothesis-driven, reproducible study of secp256k1, ECDSA, BIP340 Schnorr and SHA-256 with machine-checked identities and controlled negative results}
102 +\newcommand{\WPdate}{November 2026}
103 +\newcommand{\WPversion}{1.0}
104 +\newcommand{\WPabstract}{\input{sections/abstract}}
105 +\newcommand{\WPkeywords}{elliptic curve discrete logarithm; secp256k1; Pollard rho; endomorphisms; ECDSA; Schnorr signatures; BIP340; SHA-256; SAT-based cryptanalysis; automated mathematical discovery; Lean 4; reproducibility}
106 +\newcommand{\WPjel}{C63, C88, D83 (MSC 2020: 94A60, 11T71, 11G20, 68V15, 68W30)}
107 +\newcommand{\WPauthor}{Simon-Pierre Boucher}
108 +\newcommand{\WPaffiliation}{%
109 + Département des sciences administratives\\
110 + Université du Québec en Outaouais%
111 +}
112 +\newcommand{\WPemail}{simon-pierre.boucher@uqo.ca}
113 +\newcommand{\WPaddress}{%
114 + Gatineau -- Pavillon Alexandre-Taché\\
115 + 283, boulevard Alexandre-Taché\\
116 + Gatineau, Québec, Canada J9A 1L8%
117 +}
118 +
119 +\begin{document}
120 +\input{sections/titlepage}
121 +\tableofcontents
122 +\newpage
123 +\input{sections/01_introduction}
124 +\input{sections/02_foundations}
125 +\input{sections/03_literature}
126 +\input{sections/04_secp256k1}
127 +\input{sections/05_dlog_algorithms}
128 +\input{sections/06_signatures}
129 +\input{sections/07_sha256}
130 +\input{sections/08_discovery_framework}
131 +\input{sections/09_experimental_design}
132 +\input{sections/10_results}
133 +\input{sections/11_formal_negative}
134 +\input{sections/12_implications}
135 +\input{sections/13_limitations}
136 +\input{sections/14_conclusion}
137 +\newpage
138 +\bibliography{references}
139 +\input{appendix/appendix}
140 +\end{document}
added paper/references.bib +1438 −0
@@ -0,0 +1,1438 @@
1 +@article{koblitz1987elliptic,
2 + author = {Koblitz, Neal},
3 + title = {Elliptic Curve Cryptosystems},
4 + journal = {Mathematics of Computation},
5 + year = {1987},
6 + volume = {48},
7 + number = {177},
8 + pages = {203--209},
9 + doi = {10.1090/S0025-5718-1987-0866109-5}
10 +}
11 +
12 +@incollection{miller1985use,
13 + author = {Miller, Victor S.},
14 + title = {Use of Elliptic Curves in Cryptography},
15 + booktitle = {Advances in Cryptology --- CRYPTO '85},
16 + series = {Lecture Notes in Computer Science},
17 + volume = {218},
18 + publisher = {Springer},
19 + year = {1986},
20 + pages = {417--426},
21 + doi = {10.1007/3-540-39799-X_31}
22 +}
23 +
24 +@book{silverman2009arithmetic,
25 + author = {Silverman, Joseph H.},
26 + title = {The Arithmetic of Elliptic Curves},
27 + edition = {2nd},
28 + series = {Graduate Texts in Mathematics},
29 + volume = {106},
30 + publisher = {Springer},
31 + address = {New York},
32 + year = {2009},
33 + doi = {10.1007/978-0-387-09494-6}
34 +}
35 +
36 +@book{washington2008elliptic,
37 + author = {Washington, Lawrence C.},
38 + title = {Elliptic Curves: Number Theory and Cryptography},
39 + edition = {2nd},
40 + publisher = {Chapman and Hall/CRC},
41 + address = {Boca Raton},
42 + year = {2008},
43 + doi = {10.1201/9781420071474}
44 +}
45 +
46 +@book{galbraith2012mathematics,
47 + author = {Galbraith, Steven D.},
48 + title = {Mathematics of Public Key Cryptography},
49 + publisher = {Cambridge University Press},
50 + address = {Cambridge},
51 + year = {2012},
52 + doi = {10.1017/CBO9781139012843}
53 +}
54 +
55 +@book{hankerson2004guide,
56 + author = {Hankerson, Darrel and Menezes, Alfred and Vanstone, Scott},
57 + title = {Guide to Elliptic Curve Cryptography},
58 + publisher = {Springer},
59 + address = {New York},
60 + year = {2004},
61 + doi = {10.1007/b97644}
62 +}
63 +
64 +@book{menezes1996handbook,
65 + author = {Menezes, Alfred J. and van Oorschot, Paul C. and Vanstone, Scott A.},
66 + title = {Handbook of Applied Cryptography},
67 + publisher = {CRC Press},
68 + address = {Boca Raton},
69 + year = {1996},
70 + doi = {10.1201/9781439821916}
71 +}
72 +
73 +@article{schoof1985elliptic,
74 + author = {Schoof, Ren{\'e}},
75 + title = {Elliptic Curves over Finite Fields and the Computation of Square Roots mod $p$},
76 + journal = {Mathematics of Computation},
77 + year = {1985},
78 + volume = {44},
79 + number = {170},
80 + pages = {483--494},
81 + doi = {10.1090/S0025-5718-1985-0777280-6}
82 +}
83 +
84 +@article{schoof1995counting,
85 + author = {Schoof, Ren{\'e}},
86 + title = {Counting Points on Elliptic Curves over Finite Fields},
87 + journal = {Journal de Th{\'e}orie des Nombres de Bordeaux},
88 + year = {1995},
89 + volume = {7},
90 + number = {1},
91 + pages = {219--254},
92 + doi = {10.5802/jtnb.142}
93 +}
94 +
95 +@incollection{cohen1998efficient,
96 + author = {Cohen, Henri and Miyaji, Atsuko and Ono, Takatoshi},
97 + title = {Efficient Elliptic Curve Exponentiation Using Mixed Coordinates},
98 + booktitle = {Advances in Cryptology --- ASIACRYPT '98},
99 + series = {Lecture Notes in Computer Science},
100 + volume = {1514},
101 + publisher = {Springer},
102 + year = {1998},
103 + pages = {51--65},
104 + doi = {10.1007/3-540-49649-1_6}
105 +}
106 +
107 +@techreport{solinas1999generalized,
108 + author = {Solinas, Jerome A.},
109 + title = {Generalized {M}ersenne Numbers},
110 + institution = {Centre for Applied Cryptographic Research, University of Waterloo},
111 + number = {CORR 99-39},
112 + year = {1999},
113 + url = {https://cacr.uwaterloo.ca/techreports/1999/corr99-39.pdf}
114 +}
115 +
116 +@article{montgomery1987speeding,
117 + author = {Montgomery, Peter L.},
118 + title = {Speeding the {P}ollard and Elliptic Curve Methods of Factorization},
119 + journal = {Mathematics of Computation},
120 + year = {1987},
121 + volume = {48},
122 + number = {177},
123 + pages = {243--264},
124 + doi = {10.1090/S0025-5718-1987-0866113-7}
125 +}
126 +
127 +@incollection{brier2002weierstrass,
128 + author = {Brier, {\'E}ric and Joye, Marc},
129 + title = {Weierstra{\ss} Elliptic Curves and Side-Channel Attacks},
130 + booktitle = {Public Key Cryptography --- PKC 2002},
131 + series = {Lecture Notes in Computer Science},
132 + volume = {2274},
133 + publisher = {Springer},
134 + year = {2002},
135 + pages = {335--345},
136 + doi = {10.1007/3-540-45664-3_24}
137 +}
138 +
139 +@incollection{gallant2001faster,
140 + author = {Gallant, Robert P. and Lambert, Robert J. and Vanstone, Scott A.},
141 + title = {Faster Point Multiplication on Elliptic Curves with Efficient Endomorphisms},
142 + booktitle = {Advances in Cryptology --- CRYPTO 2001},
143 + series = {Lecture Notes in Computer Science},
144 + volume = {2139},
145 + publisher = {Springer},
146 + year = {2001},
147 + pages = {190--200},
148 + doi = {10.1007/3-540-44647-8_11}
149 +}
150 +
151 +@article{straus1964addition,
152 + author = {Straus, Ernst G.},
153 + title = {Addition Chains of Vectors (Problem 5125)},
154 + journal = {The American Mathematical Monthly},
155 + year = {1964},
156 + volume = {71},
157 + number = {7},
158 + pages = {806--808},
159 + doi = {10.2307/2310929},
160 + note = {Solution to problem 5125 proposed by R. Bellman}
161 +}
162 +
163 +@article{pippenger1980evaluation,
164 + author = {Pippenger, Nicholas},
165 + title = {On the Evaluation of Powers and Monomials},
166 + journal = {SIAM Journal on Computing},
167 + year = {1980},
168 + volume = {9},
169 + number = {2},
170 + pages = {230--250},
171 + doi = {10.1137/0209022}
172 +}
173 +
174 +@incollection{moller2001algorithms,
175 + author = {M{\"o}ller, Bodo},
176 + title = {Algorithms for Multi-exponentiation},
177 + booktitle = {Selected Areas in Cryptography --- SAC 2001},
178 + series = {Lecture Notes in Computer Science},
179 + volume = {2259},
180 + publisher = {Springer},
181 + year = {2001},
182 + pages = {165--180},
183 + doi = {10.1007/3-540-45537-X_13}
184 +}
185 +
186 +@article{bernstein2012high,
187 + author = {Bernstein, Daniel J. and Duif, Niels and Lange, Tanja and Schwabe, Peter and Yang, Bo-Yin},
188 + title = {High-Speed High-Security Signatures},
189 + journal = {Journal of Cryptographic Engineering},
190 + year = {2012},
191 + volume = {2},
192 + number = {2},
193 + pages = {77--89},
194 + doi = {10.1007/s13389-012-0027-1}
195 +}
196 +
197 +@incollection{naccache1994can,
198 + author = {Naccache, David and M'Ra{\"i}hi, David and Vaudenay, Serge and Raphaeli, Dan},
199 + title = {Can {D.S.A.} Be Improved? Complexity Trade-Offs with the Digital Signature Standard},
200 + booktitle = {Advances in Cryptology --- EUROCRYPT '94},
201 + series = {Lecture Notes in Computer Science},
202 + volume = {950},
203 + publisher = {Springer},
204 + year = {1995},
205 + pages = {77--85},
206 + doi = {10.1007/BFb0053426}
207 +}
208 +
209 +@incollection{bellare1998fast,
210 + author = {Bellare, Mihir and Garay, Juan A. and Rabin, Tal},
211 + title = {Fast Batch Verification for Modular Exponentiation and Digital Signatures},
212 + booktitle = {Advances in Cryptology --- EUROCRYPT '98},
213 + series = {Lecture Notes in Computer Science},
214 + volume = {1403},
215 + publisher = {Springer},
216 + year = {1998},
217 + pages = {236--250},
218 + doi = {10.1007/BFb0054130}
219 +}
220 +
221 +@incollection{shanks1971class,
222 + author = {Shanks, Daniel},
223 + title = {Class Number, a Theory of Factorization, and Genera},
224 + booktitle = {1969 Number Theory Institute},
225 + series = {Proceedings of Symposia in Pure Mathematics},
226 + volume = {20},
227 + publisher = {American Mathematical Society},
228 + year = {1971},
229 + pages = {415--440},
230 + doi = {10.1090/pspum/020/0316385}
231 +}
232 +
233 +@article{pollard1978monte,
234 + author = {Pollard, John M.},
235 + title = {Monte {C}arlo Methods for Index Computation (mod $p$)},
236 + journal = {Mathematics of Computation},
237 + year = {1978},
238 + volume = {32},
239 + number = {143},
240 + pages = {918--924},
241 + doi = {10.1090/S0025-5718-1978-0491431-9}
242 +}
243 +
244 +@article{pollard2000kangaroos,
245 + author = {Pollard, John M.},
246 + title = {Kangaroos, {M}onopoly and Discrete Logarithms},
247 + journal = {Journal of Cryptology},
248 + year = {2000},
249 + volume = {13},
250 + number = {4},
251 + pages = {437--447},
252 + doi = {10.1007/s001450010010}
253 +}
254 +
255 +@article{vanoorschot1999parallel,
256 + author = {van Oorschot, Paul C. and Wiener, Michael J.},
257 + title = {Parallel Collision Search with Cryptanalytic Applications},
258 + journal = {Journal of Cryptology},
259 + year = {1999},
260 + volume = {12},
261 + number = {1},
262 + pages = {1--28},
263 + doi = {10.1007/PL00003816}
264 +}
265 +
266 +@incollection{teske1998speeding,
267 + author = {Teske, Edlyn},
268 + title = {Speeding up {P}ollard's Rho Method for Computing Discrete Logarithms},
269 + booktitle = {Algorithmic Number Theory --- ANTS-III},
270 + series = {Lecture Notes in Computer Science},
271 + volume = {1423},
272 + publisher = {Springer},
273 + year = {1998},
274 + pages = {541--554},
275 + doi = {10.1007/BFb0054891}
276 +}
277 +
278 +@article{teske2001random,
279 + author = {Teske, Edlyn},
280 + title = {On Random Walks for {P}ollard's Rho Method},
281 + journal = {Mathematics of Computation},
282 + year = {2001},
283 + volume = {70},
284 + number = {234},
285 + pages = {809--825},
286 + doi = {10.1090/S0025-5718-00-01213-8}
287 +}
288 +
289 +@article{brent1980improved,
290 + author = {Brent, Richard P.},
291 + title = {An Improved {M}onte {C}arlo Factorization Algorithm},
292 + journal = {BIT Numerical Mathematics},
293 + year = {1980},
294 + volume = {20},
295 + number = {2},
296 + pages = {176--184},
297 + doi = {10.1007/BF01933190}
298 +}
299 +
300 +@incollection{flajolet1990random,
301 + author = {Flajolet, Philippe and Odlyzko, Andrew M.},
302 + title = {Random Mapping Statistics},
303 + booktitle = {Advances in Cryptology --- EUROCRYPT '89},
304 + series = {Lecture Notes in Computer Science},
305 + volume = {434},
306 + publisher = {Springer},
307 + year = {1990},
308 + pages = {329--354},
309 + doi = {10.1007/3-540-46885-4_34}
310 +}
311 +
312 +@incollection{shoup1997lower,
313 + author = {Shoup, Victor},
314 + title = {Lower Bounds for Discrete Logarithms and Related Problems},
315 + booktitle = {Advances in Cryptology --- EUROCRYPT '97},
316 + series = {Lecture Notes in Computer Science},
317 + volume = {1233},
318 + publisher = {Springer},
319 + year = {1997},
320 + pages = {256--266},
321 + doi = {10.1007/3-540-69053-0_18}
322 +}
323 +
324 +@article{nechaev1994complexity,
325 + author = {Nechaev, Vassiliy I.},
326 + title = {Complexity of a Determinate Algorithm for the Discrete Logarithm},
327 + journal = {Mathematical Notes},
328 + year = {1994},
329 + volume = {55},
330 + number = {2},
331 + pages = {165--172},
332 + doi = {10.1007/BF02113297}
333 +}
334 +
335 +@article{gallant2000improving,
336 + author = {Gallant, Robert and Lambert, Robert and Vanstone, Scott},
337 + title = {Improving the Parallelized {P}ollard Lambda Search on Anomalous Binary Curves},
338 + journal = {Mathematics of Computation},
339 + year = {2000},
340 + volume = {69},
341 + number = {232},
342 + pages = {1699--1705},
343 + doi = {10.1090/S0025-5718-99-01119-9}
344 +}
345 +
346 +@incollection{wiener1999faster,
347 + author = {Wiener, Michael J. and Zuccherato, Robert J.},
348 + title = {Faster Attacks on Elliptic Curve Cryptosystems},
349 + booktitle = {Selected Areas in Cryptography --- SAC '98},
350 + series = {Lecture Notes in Computer Science},
351 + volume = {1556},
352 + publisher = {Springer},
353 + year = {1999},
354 + pages = {190--200},
355 + doi = {10.1007/3-540-48892-8_15}
356 +}
357 +
358 +@incollection{duursma1999speeding,
359 + author = {Duursma, Iwan and Gaudry, Pierrick and Morain, Fran{\c{c}}ois},
360 + title = {Speeding up the Discrete Log Computation on Curves with Automorphisms},
361 + booktitle = {Advances in Cryptology --- ASIACRYPT '99},
362 + series = {Lecture Notes in Computer Science},
363 + volume = {1716},
364 + publisher = {Springer},
365 + year = {1999},
366 + pages = {103--121},
367 + doi = {10.1007/978-3-540-48000-6_10}
368 +}
369 +
370 +@incollection{bos2010negation,
371 + author = {Bos, Joppe W. and Kleinjung, Thorsten and Lenstra, Arjen K.},
372 + title = {On the Use of the Negation Map in the {P}ollard Rho Method},
373 + booktitle = {Algorithmic Number Theory --- ANTS-IX},
374 + series = {Lecture Notes in Computer Science},
375 + volume = {6197},
376 + publisher = {Springer},
377 + year = {2010},
378 + pages = {66--82},
379 + doi = {10.1007/978-3-642-14518-6_9}
380 +}
381 +
382 +@incollection{bernstein2011correct,
383 + author = {Bernstein, Daniel J. and Lange, Tanja and Schwabe, Peter},
384 + title = {On the Correct Use of the Negation Map in the {P}ollard Rho Method},
385 + booktitle = {Public Key Cryptography --- PKC 2011},
386 + series = {Lecture Notes in Computer Science},
387 + volume = {6571},
388 + publisher = {Springer},
389 + year = {2011},
390 + pages = {128--146},
391 + doi = {10.1007/978-3-642-19379-8_8}
392 +}
393 +
394 +@incollection{bernstein2013nonuniform,
395 + author = {Bernstein, Daniel J. and Lange, Tanja},
396 + title = {Non-uniform Cracks in the Concrete: The Power of Free Precomputation},
397 + booktitle = {Advances in Cryptology --- ASIACRYPT 2013},
398 + series = {Lecture Notes in Computer Science},
399 + volume = {8270},
400 + publisher = {Springer},
401 + year = {2013},
402 + pages = {321--340},
403 + doi = {10.1007/978-3-642-42045-0_17},
404 + note = {Full version: IACR Cryptology ePrint Archive, Report 2012/318}
405 +}
406 +
407 +@incollection{corrigangibbs2018discrete,
408 + author = {Corrigan-Gibbs, Henry and Kogan, Dmitry},
409 + title = {The Discrete-Logarithm Problem with Preprocessing},
410 + booktitle = {Advances in Cryptology --- EUROCRYPT 2018},
411 + series = {Lecture Notes in Computer Science},
412 + volume = {10821},
413 + publisher = {Springer},
414 + year = {2018},
415 + pages = {415--447},
416 + doi = {10.1007/978-3-319-78375-8_14},
417 + note = {Full version: IACR Cryptology ePrint Archive, Report 2017/1113}
418 +}
419 +
420 +@misc{lee2011generic,
421 + author = {Lee, Hyung Tae and Cheon, Jung Hee and Hong, Jin},
422 + title = {Accelerating {ID}-based Encryption Based on Trapdoor {DL} Using Pre-computation},
423 + howpublished = {IACR Cryptology ePrint Archive, Report 2011/187},
424 + year = {2011},
425 + url = {https://eprint.iacr.org/2011/187},
426 + note = {Discrete-logarithm computation with precomputation (preprocessing) in a fixed group}
427 +}
428 +
429 +@article{bernstein2012two,
430 + author = {Bernstein, Daniel J. and Lange, Tanja},
431 + title = {Two Grumpy Giants and a Baby},
432 + journal = {The Open Book Series},
433 + year = {2013},
434 + volume = {1},
435 + number = {1},
436 + pages = {87--111},
437 + doi = {10.2140/obs.2013.1.87},
438 + note = {Proceedings of ANTS X (2012)}
439 +}
440 +
441 +@incollection{galbraith2010interval,
442 + author = {Galbraith, Steven D. and Ruprai, Raminder S.},
443 + title = {Using Equivalence Classes to Accelerate Solving the Discrete Logarithm Problem in a Short Interval},
444 + booktitle = {Public Key Cryptography --- PKC 2010},
445 + series = {Lecture Notes in Computer Science},
446 + volume = {6056},
447 + publisher = {Springer},
448 + year = {2010},
449 + pages = {368--383},
450 + doi = {10.1007/978-3-642-13013-7_22}
451 +}
452 +
453 +@article{galbraith2016recent,
454 + author = {Galbraith, Steven D. and Gaudry, Pierrick},
455 + title = {Recent Progress on the Elliptic Curve Discrete Logarithm Problem},
456 + journal = {Designs, Codes and Cryptography},
457 + year = {2016},
458 + volume = {78},
459 + number = {1},
460 + pages = {51--72},
461 + doi = {10.1007/s10623-015-0146-7}
462 +}
463 +
464 +@article{pohlig1978improved,
465 + author = {Pohlig, Stephen C. and Hellman, Martin E.},
466 + title = {An Improved Algorithm for Computing Logarithms over {GF}($p$) and Its Cryptographic Significance},
467 + journal = {IEEE Transactions on Information Theory},
468 + year = {1978},
469 + volume = {24},
470 + number = {1},
471 + pages = {106--110},
472 + doi = {10.1109/TIT.1978.1055817}
473 +}
474 +
475 +@article{menezes1993reducing,
476 + author = {Menezes, Alfred J. and Okamoto, Tatsuaki and Vanstone, Scott A.},
477 + title = {Reducing Elliptic Curve Logarithms to Logarithms in a Finite Field},
478 + journal = {IEEE Transactions on Information Theory},
479 + year = {1993},
480 + volume = {39},
481 + number = {5},
482 + pages = {1639--1646},
483 + doi = {10.1109/18.259647}
484 +}
485 +
486 +@article{frey1994remark,
487 + author = {Frey, Gerhard and R{\"u}ck, Hans-Georg},
488 + title = {A Remark Concerning $m$-Divisibility and the Discrete Logarithm in the Divisor Class Group of Curves},
489 + journal = {Mathematics of Computation},
490 + year = {1994},
491 + volume = {62},
492 + number = {206},
493 + pages = {865--874},
494 + doi = {10.1090/S0025-5718-1994-1218343-6}
495 +}
496 +
497 +@article{semaev1998evaluation,
498 + author = {Semaev, Igor A.},
499 + title = {Evaluation of Discrete Logarithms in a Group of $p$-Torsion Points of an Elliptic Curve in Characteristic $p$},
500 + journal = {Mathematics of Computation},
501 + year = {1998},
502 + volume = {67},
503 + number = {221},
504 + pages = {353--356},
505 + doi = {10.1090/S0025-5718-98-00887-4}
506 +}
507 +
508 +@article{smart1999discrete,
509 + author = {Smart, Nigel P.},
510 + title = {The Discrete Logarithm Problem on Elliptic Curves of Trace One},
511 + journal = {Journal of Cryptology},
512 + year = {1999},
513 + volume = {12},
514 + number = {3},
515 + pages = {193--196},
516 + doi = {10.1007/s001459900052}
517 +}
518 +
519 +@article{satoh1998fermat,
520 + author = {Satoh, Takakazu and Araki, Kiyomichi},
521 + title = {Fermat Quotients and the Polynomial Time Discrete Log Algorithm for Anomalous Elliptic Curves},
522 + journal = {Commentarii Mathematici Universitatis Sancti Pauli},
523 + year = {1998},
524 + volume = {47},
525 + number = {1},
526 + pages = {81--92},
527 + note = {Errata: same journal 48 (1999), no. 2, 211--213. No DOI; bibliographic data confirmed through citing sources}
528 +}
529 +
530 +@misc{semaev2004summation,
531 + author = {Semaev, Igor},
532 + title = {Summation Polynomials and the Discrete Logarithm Problem on Elliptic Curves},
533 + howpublished = {IACR Cryptology ePrint Archive, Report 2004/031},
534 + year = {2004},
535 + url = {https://eprint.iacr.org/2004/031}
536 +}
537 +
538 +@article{gaudry2009index,
539 + author = {Gaudry, Pierrick},
540 + title = {Index Calculus for Abelian Varieties of Small Dimension and the Elliptic Curve Discrete Logarithm Problem},
541 + journal = {Journal of Symbolic Computation},
542 + year = {2009},
543 + volume = {44},
544 + number = {12},
545 + pages = {1690--1702},
546 + doi = {10.1016/j.jsc.2008.08.005}
547 +}
548 +
549 +@article{diem2011discrete,
550 + author = {Diem, Claus},
551 + title = {On the Discrete Logarithm Problem in Elliptic Curves},
552 + journal = {Compositio Mathematica},
553 + year = {2011},
554 + volume = {147},
555 + number = {1},
556 + pages = {75--104},
557 + doi = {10.1112/S0010437X10005075}
558 +}
559 +
560 +@incollection{petit2012elliptic,
561 + author = {Petit, Christophe and Quisquater, Jean-Jacques},
562 + title = {On Polynomial Systems Arising from a {W}eil Descent},
563 + booktitle = {Advances in Cryptology --- ASIACRYPT 2012},
564 + series = {Lecture Notes in Computer Science},
565 + volume = {7658},
566 + publisher = {Springer},
567 + year = {2012},
568 + pages = {451--466},
569 + doi = {10.1007/978-3-642-34961-4_28}
570 +}
571 +
572 +@article{bos2012solving,
573 + author = {Bos, Joppe W. and Kaihara, Marcelo E. and Kleinjung, Thorsten and Lenstra, Arjen K. and Montgomery, Peter L.},
574 + title = {Solving a 112-bit Prime Elliptic Curve Discrete Logarithm Problem on Game Consoles Using Sloppy Reduction},
575 + journal = {International Journal of Applied Cryptography},
576 + year = {2012},
577 + volume = {2},
578 + number = {3},
579 + pages = {212--228},
580 + doi = {10.1504/IJACT.2012.045590}
581 +}
582 +
583 +@incollection{wenger2014solving,
584 + author = {Wenger, Erich and Wolfger, Paul},
585 + title = {Solving the Discrete Logarithm of a 113-bit {K}oblitz Curve with an {FPGA} Cluster},
586 + booktitle = {Selected Areas in Cryptography --- SAC 2014},
587 + series = {Lecture Notes in Computer Science},
588 + volume = {8781},
589 + publisher = {Springer},
590 + year = {2014},
591 + pages = {363--379},
592 + doi = {10.1007/978-3-319-13051-4_22},
593 + note = {Also IACR Cryptology ePrint Archive, Report 2014/368}
594 +}
595 +
596 +@article{wenger2016harder,
597 + author = {Wenger, Erich and Wolfger, Paul},
598 + title = {Harder, Better, Faster, Stronger: Elliptic Curve Discrete Logarithm Computations on {FPGAs}},
599 + journal = {Journal of Cryptographic Engineering},
600 + year = {2016},
601 + volume = {6},
602 + number = {4},
603 + pages = {287--297},
604 + doi = {10.1007/s13389-015-0108-z},
605 + note = {Also IACR Cryptology ePrint Archive, Report 2015/143}
606 +}
607 +
608 +@misc{bernstein2016faster,
609 + author = {Bernstein, Daniel J. and Engels, Susanne and Lange, Tanja and Niederhagen, Ruben and Paar, Christof and Schwabe, Peter and Zimmermann, Ralf},
610 + title = {Faster Elliptic-Curve Discrete Logarithms on {FPGAs}},
611 + howpublished = {IACR Cryptology ePrint Archive, Report 2016/382},
612 + year = {2016},
613 + url = {https://eprint.iacr.org/2016/382}
614 +}
615 +
616 +@techreport{certicom2009challenge,
617 + author = {{Certicom Research}},
618 + title = {Certicom {ECC} Challenge},
619 + institution = {Certicom Corp.},
620 + year = {2009},
621 + note = {Original date November 6, 1997; latest update November 10, 2009},
622 + url = {https://www.certicom.com/content/dam/certicom/images/pdfs/challenge-2009.pdf}
623 +}
624 +
625 +@article{shor1997polynomial,
626 + author = {Shor, Peter W.},
627 + title = {Polynomial-Time Algorithms for Prime Factorization and Discrete Logarithms on a Quantum Computer},
628 + journal = {SIAM Journal on Computing},
629 + year = {1997},
630 + volume = {26},
631 + number = {5},
632 + pages = {1484--1509},
633 + doi = {10.1137/S0097539795293172}
634 +}
635 +
636 +@incollection{roetteler2017quantum,
637 + author = {Roetteler, Martin and Naehrig, Michael and Svore, Krysta M. and Lauter, Kristin},
638 + title = {Quantum Resource Estimates for Computing Elliptic Curve Discrete Logarithms},
639 + booktitle = {Advances in Cryptology --- ASIACRYPT 2017},
640 + series = {Lecture Notes in Computer Science},
641 + volume = {10625},
642 + publisher = {Springer},
643 + year = {2017},
644 + pages = {241--270},
645 + doi = {10.1007/978-3-319-70697-9_9}
646 +}
647 +
648 +@article{schnorr1991efficient,
649 + author = {Schnorr, Claus-Peter},
650 + title = {Efficient Signature Generation by Smart Cards},
651 + journal = {Journal of Cryptology},
652 + year = {1991},
653 + volume = {4},
654 + number = {3},
655 + pages = {161--174},
656 + doi = {10.1007/BF00196725}
657 +}
658 +
659 +@article{pointcheval2000security,
660 + author = {Pointcheval, David and Stern, Jacques},
661 + title = {Security Arguments for Digital Signatures and Blind Signatures},
662 + journal = {Journal of Cryptology},
663 + year = {2000},
664 + volume = {13},
665 + number = {3},
666 + pages = {361--396},
667 + doi = {10.1007/s001450010003}
668 +}
669 +
670 +@incollection{seurin2012exact,
671 + author = {Seurin, Yannick},
672 + title = {On the Exact Security of {S}chnorr-Type Signatures in the Random Oracle Model},
673 + booktitle = {Advances in Cryptology --- EUROCRYPT 2012},
674 + series = {Lecture Notes in Computer Science},
675 + volume = {7237},
676 + publisher = {Springer},
677 + year = {2012},
678 + pages = {554--571},
679 + doi = {10.1007/978-3-642-29011-4_33}
680 +}
681 +
682 +@incollection{paillier2005discrete,
683 + author = {Paillier, Pascal and Vergnaud, Damien},
684 + title = {Discrete-Log-Based Signatures May Not Be Equivalent to Discrete Log},
685 + booktitle = {Advances in Cryptology --- ASIACRYPT 2005},
686 + series = {Lecture Notes in Computer Science},
687 + volume = {3788},
688 + publisher = {Springer},
689 + year = {2005},
690 + pages = {1--20},
691 + doi = {10.1007/11593447_1}
692 +}
693 +
694 +@article{brown2005generic,
695 + author = {Brown, Daniel R. L.},
696 + title = {Generic Groups, Collision Resistance, and {ECDSA}},
697 + journal = {Designs, Codes and Cryptography},
698 + year = {2005},
699 + volume = {35},
700 + number = {1},
701 + pages = {119--152},
702 + doi = {10.1007/s10623-003-6154-z}
703 +}
704 +
705 +@incollection{vaudenay2003security,
706 + author = {Vaudenay, Serge},
707 + title = {The Security of {DSA} and {ECDSA}},
708 + booktitle = {Public Key Cryptography --- PKC 2003},
709 + series = {Lecture Notes in Computer Science},
710 + volume = {2567},
711 + publisher = {Springer},
712 + year = {2003},
713 + pages = {309--323},
714 + doi = {10.1007/3-540-36288-6_23}
715 +}
716 +
717 +@inproceedings{fersch2016provable,
718 + author = {Fersch, Manuel and Kiltz, Eike and Poettering, Bertram},
719 + title = {On the Provable Security of {(EC)DSA} Signatures},
720 + booktitle = {Proceedings of the 2016 ACM SIGSAC Conference on Computer and Communications Security (CCS 2016)},
721 + publisher = {ACM},
722 + year = {2016},
723 + pages = {1651--1662},
724 + doi = {10.1145/2976749.2978413}
725 +}
726 +
727 +@incollection{fersch2017one,
728 + author = {Fersch, Manuel and Kiltz, Eike and Poettering, Bertram},
729 + title = {On the One-Per-Message Unforgeability of {(EC)DSA} and Its Variants},
730 + booktitle = {Theory of Cryptography --- TCC 2017},
731 + series = {Lecture Notes in Computer Science},
732 + volume = {10678},
733 + publisher = {Springer},
734 + year = {2017},
735 + pages = {519--534},
736 + doi = {10.1007/978-3-319-70503-3_17}
737 +}
738 +
739 +@incollection{groth2022security,
740 + author = {Groth, Jens and Shoup, Victor},
741 + title = {On the Security of {ECDSA} with Additive Key Derivation and Presignatures},
742 + booktitle = {Advances in Cryptology --- EUROCRYPT 2022},
743 + series = {Lecture Notes in Computer Science},
744 + volume = {13276},
745 + publisher = {Springer},
746 + year = {2022},
747 + pages = {365--396},
748 + doi = {10.1007/978-3-031-06944-4_13}
749 +}
750 +
751 +@article{howgravegraham2001lattice,
752 + author = {Howgrave-Graham, Nick A. and Smart, Nigel P.},
753 + title = {Lattice Attacks on Digital Signature Schemes},
754 + journal = {Designs, Codes and Cryptography},
755 + year = {2001},
756 + volume = {23},
757 + number = {3},
758 + pages = {283--290},
759 + doi = {10.1023/A:1011214926272}
760 +}
761 +
762 +@incollection{breitner2019biased,
763 + author = {Breitner, Joachim and Heninger, Nadia},
764 + title = {Biased Nonce Sense: Lattice Attacks Against Weak {ECDSA} Signatures in Cryptocurrencies},
765 + booktitle = {Financial Cryptography and Data Security --- FC 2019},
766 + series = {Lecture Notes in Computer Science},
767 + volume = {11598},
768 + publisher = {Springer},
769 + year = {2019},
770 + pages = {3--20},
771 + doi = {10.1007/978-3-030-32101-7_1}
772 +}
773 +
774 +@inproceedings{bellare2006multi,
775 + author = {Bellare, Mihir and Neven, Gregory},
776 + title = {Multi-signatures in the Plain Public-Key Model and a General Forking Lemma},
777 + booktitle = {Proceedings of the 13th ACM Conference on Computer and Communications Security (CCS 2006)},
778 + publisher = {ACM},
779 + year = {2006},
780 + pages = {390--399},
781 + doi = {10.1145/1180405.1180453}
782 +}
783 +
784 +@article{maxwell2019simple,
785 + author = {Maxwell, Gregory and Poelstra, Andrew and Seurin, Yannick and Wuille, Pieter},
786 + title = {Simple {S}chnorr Multi-Signatures with Applications to {B}itcoin},
787 + journal = {Designs, Codes and Cryptography},
788 + year = {2019},
789 + volume = {87},
790 + number = {9},
791 + pages = {2139--2164},
792 + doi = {10.1007/s10623-019-00608-x}
793 +}
794 +
795 +@incollection{nick2021musig2,
796 + author = {Nick, Jonas and Ruffing, Tim and Seurin, Yannick},
797 + title = {{MuSig2}: Simple Two-Round {S}chnorr Multi-signatures},
798 + booktitle = {Advances in Cryptology --- CRYPTO 2021},
799 + series = {Lecture Notes in Computer Science},
800 + volume = {12825},
801 + publisher = {Springer},
802 + year = {2021},
803 + pages = {189--221},
804 + doi = {10.1007/978-3-030-84242-0_8}
805 +}
806 +
807 +@incollection{kocher1996timing,
808 + author = {Kocher, Paul C.},
809 + title = {Timing Attacks on Implementations of {D}iffie-{H}ellman, {RSA}, {DSS}, and Other Systems},
810 + booktitle = {Advances in Cryptology --- CRYPTO '96},
811 + series = {Lecture Notes in Computer Science},
812 + volume = {1109},
813 + publisher = {Springer},
814 + year = {1996},
815 + pages = {104--113},
816 + doi = {10.1007/3-540-68697-5_9}
817 +}
818 +
819 +@incollection{brumley2011remote,
820 + author = {Brumley, Billy Bob and Tuveri, Nicola},
821 + title = {Remote Timing Attacks Are Still Practical},
822 + booktitle = {Computer Security --- ESORICS 2011},
823 + series = {Lecture Notes in Computer Science},
824 + volume = {6879},
825 + publisher = {Springer},
826 + year = {2011},
827 + pages = {355--371},
828 + doi = {10.1007/978-3-642-23822-2_20}
829 +}
830 +
831 +@inproceedings{aldaya2020ladderleak,
832 + author = {Aranha, Diego F. and Novaes, Felipe Rodrigues and Takahashi, Akira and Tibouchi, Mehdi and Yarom, Yuval},
833 + title = {{LadderLeak}: Breaking {ECDSA} with Less than One Bit of Nonce Leakage},
834 + booktitle = {Proceedings of the 2020 ACM SIGSAC Conference on Computer and Communications Security (CCS 2020)},
835 + publisher = {ACM},
836 + year = {2020},
837 + pages = {225--242},
838 + doi = {10.1145/3372297.3417268}
839 +}
840 +
841 +@article{jancar2020minerva,
842 + author = {Jan{\v{c}}ar, J{\'a}n and Sedl{\'a}{\v{c}}ek, Vladim{\'i}r and {\v{S}}venda, Petr and S{\'y}s, Marek},
843 + title = {Minerva: The Curse of {ECDSA} Nonces},
844 + journal = {IACR Transactions on Cryptographic Hardware and Embedded Systems},
845 + year = {2020},
846 + volume = {2020},
847 + number = {4},
848 + pages = {281--308},
849 + doi = {10.46586/tches.v2020.i4.281-308}
850 +}
851 +
852 +@techreport{pornin2013rfc6979,
853 + author = {Pornin, Thomas},
854 + title = {Deterministic Usage of the Digital Signature Algorithm ({DSA}) and Elliptic Curve Digital Signature Algorithm ({ECDSA})},
855 + institution = {Internet Engineering Task Force},
856 + type = {RFC},
857 + number = {6979},
858 + year = {2013},
859 + doi = {10.17487/RFC6979},
860 + url = {https://www.rfc-editor.org/rfc/rfc6979}
861 +}
862 +
863 +@misc{wuille2020bip340,
864 + author = {Wuille, Pieter and Nick, Jonas and Ruffing, Tim},
865 + title = {{BIP} 340: {S}chnorr Signatures for secp256k1},
866 + howpublished = {Bitcoin Improvement Proposal 340},
867 + year = {2020},
868 + url = {https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki},
869 + note = {Status: Deployed; type: Specification; assigned 2020-01-19}
870 +}
871 +
872 +@misc{wuille2020bip341,
873 + author = {Wuille, Pieter and Nick, Jonas and Towns, Anthony},
874 + title = {{BIP} 341: {T}aproot: {S}eg{W}it Version 1 Spending Rules},
875 + howpublished = {Bitcoin Improvement Proposal 341},
876 + year = {2020},
877 + url = {https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki},
878 + note = {Status: Deployed; consensus layer (soft fork); requires BIP 340}
879 +}
880 +
881 +@misc{nakamoto2008bitcoin,
882 + author = {Nakamoto, Satoshi},
883 + title = {Bitcoin: A Peer-to-Peer Electronic Cash System},
884 + howpublished = {White paper},
885 + year = {2008},
886 + url = {https://bitcoin.org/bitcoin.pdf}
887 +}
888 +
889 +@misc{wuille2013libsecp256k1,
890 + author = {Wuille, Pieter and Maxwell, Gregory and Nick, Jonas and Ruffing, Tim and others},
891 + title = {libsecp256k1: Optimized {C} Library for {ECDSA} and {BIP}-340 Signatures on Curve secp256k1},
892 + howpublished = {Software, Bitcoin Core project, GitHub},
893 + year = {2024},
894 + url = {https://github.com/bitcoin-core/secp256k1},
895 + note = {The GLV endomorphism $\lambda\cdot(x,y)=(\beta x,y)$ and the scalar split are implemented in \texttt{src/scalar\_impl.h}; accessed 2026-11-01}
896 +}
897 +
898 +@techreport{certicom2009sec1,
899 + author = {{Certicom Research}},
900 + title = {{SEC} 1: Elliptic Curve Cryptography, Version 2.0},
901 + institution = {Standards for Efficient Cryptography Group},
902 + year = {2009},
903 + month = may,
904 + note = {May 21, 2009},
905 + url = {https://www.secg.org/sec1-v2.pdf}
906 +}
907 +
908 +@techreport{certicom2010sec2,
909 + author = {{Certicom Research}},
910 + title = {{SEC} 2: Recommended Elliptic Curve Domain Parameters, Version 2.0},
911 + institution = {Standards for Efficient Cryptography Group},
912 + year = {2010},
913 + month = jan,
914 + note = {January 27, 2010},
915 + url = {https://www.secg.org/sec2-v2.pdf}
916 +}
917 +
918 +@techreport{nist2023fips186,
919 + author = {{National Institute of Standards and Technology}},
920 + title = {Digital Signature Standard ({DSS})},
921 + institution = {U.S. Department of Commerce},
922 + type = {Federal Information Processing Standards Publication},
923 + number = {FIPS 186-5},
924 + year = {2023},
925 + month = feb,
926 + doi = {10.6028/NIST.FIPS.186-5}
927 +}
928 +
929 +@techreport{nist2015fips180,
930 + author = {{National Institute of Standards and Technology}},
931 + title = {Secure Hash Standard ({SHS})},
932 + institution = {U.S. Department of Commerce},
933 + type = {Federal Information Processing Standards Publication},
934 + number = {FIPS 180-4},
935 + year = {2015},
936 + month = aug,
937 + doi = {10.6028/NIST.FIPS.180-4}
938 +}
939 +
940 +@misc{bernstein2014safecurves,
941 + author = {Bernstein, Daniel J. and Lange, Tanja},
942 + title = {{SafeCurves}: Choosing Safe Curves for Elliptic-Curve Cryptography},
943 + howpublished = {Web page},
944 + year = {2014},
945 + url = {https://safecurves.cr.yp.to},
946 + note = {Accessed 2026-11-01}
947 +}
948 +
949 +@incollection{merkle1989one,
950 + author = {Merkle, Ralph C.},
951 + title = {One Way Hash Functions and {DES}},
952 + booktitle = {Advances in Cryptology --- CRYPTO '89},
953 + series = {Lecture Notes in Computer Science},
954 + volume = {435},
955 + publisher = {Springer},
956 + year = {1990},
957 + pages = {428--446},
958 + doi = {10.1007/0-387-34805-0_40}
959 +}
960 +
961 +@incollection{damgard1989design,
962 + author = {Damg{\aa}rd, Ivan Bjerre},
963 + title = {A Design Principle for Hash Functions},
964 + booktitle = {Advances in Cryptology --- CRYPTO '89},
965 + series = {Lecture Notes in Computer Science},
966 + volume = {435},
967 + publisher = {Springer},
968 + year = {1990},
969 + pages = {416--427},
970 + doi = {10.1007/0-387-34805-0_39}
971 +}
972 +
973 +@incollection{chabaud1998differential,
974 + author = {Chabaud, Florent and Joux, Antoine},
975 + title = {Differential Collisions in {SHA-0}},
976 + booktitle = {Advances in Cryptology --- CRYPTO '98},
977 + series = {Lecture Notes in Computer Science},
978 + volume = {1462},
979 + publisher = {Springer},
980 + year = {1998},
981 + pages = {56--71},
982 + doi = {10.1007/BFb0055720}
983 +}
984 +
985 +@incollection{gilbert2003security,
986 + author = {Gilbert, Henri and Handschuh, Helena},
987 + title = {Security Analysis of {SHA-256} and Sisters},
988 + booktitle = {Selected Areas in Cryptography --- SAC 2003},
989 + series = {Lecture Notes in Computer Science},
990 + volume = {3006},
991 + publisher = {Springer},
992 + year = {2004},
993 + pages = {175--193},
994 + doi = {10.1007/978-3-540-24654-1_13}
995 +}
996 +
997 +@incollection{wang2005finding,
998 + author = {Wang, Xiaoyun and Yin, Yiqun Lisa and Yu, Hongbo},
999 + title = {Finding Collisions in the Full {SHA-1}},
1000 + booktitle = {Advances in Cryptology --- CRYPTO 2005},
1001 + series = {Lecture Notes in Computer Science},
1002 + volume = {3621},
1003 + publisher = {Springer},
1004 + year = {2005},
1005 + pages = {17--36},
1006 + doi = {10.1007/11535218_2}
1007 +}
1008 +
1009 +@incollection{stevens2017first,
1010 + author = {Stevens, Marc and Bursztein, Elie and Karpman, Pierre and Albertini, Ange and Markov, Yarik},
1011 + title = {The First Collision for Full {SHA-1}},
1012 + booktitle = {Advances in Cryptology --- CRYPTO 2017},
1013 + series = {Lecture Notes in Computer Science},
1014 + volume = {10401},
1015 + publisher = {Springer},
1016 + year = {2017},
1017 + pages = {570--596},
1018 + doi = {10.1007/978-3-319-63688-7_19}
1019 +}
1020 +
1021 +@inproceedings{leurent2020sha1,
1022 + author = {Leurent, Ga{\"e}tan and Peyrin, Thomas},
1023 + title = {{SHA-1} is a Shambles: First Chosen-Prefix Collision on {SHA-1} and Application to the {PGP} Web of Trust},
1024 + booktitle = {29th USENIX Security Symposium (USENIX Security 20)},
1025 + publisher = {USENIX Association},
1026 + year = {2020},
1027 + url = {https://www.usenix.org/conference/usenixsecurity20/presentation/leurent},
1028 + note = {Also IACR Cryptology ePrint Archive, Report 2020/014}
1029 +}
1030 +
1031 +@incollection{nikolic2008collisions,
1032 + author = {Nikoli{\'c}, Ivica and Biryukov, Alex},
1033 + title = {Collisions for Step-Reduced {SHA-256}},
1034 + booktitle = {Fast Software Encryption --- FSE 2008},
1035 + series = {Lecture Notes in Computer Science},
1036 + volume = {5086},
1037 + publisher = {Springer},
1038 + year = {2008},
1039 + pages = {1--15},
1040 + doi = {10.1007/978-3-540-71039-4_1}
1041 +}
1042 +
1043 +@incollection{indesteege2008collisions,
1044 + author = {Indesteege, Sebastiaan and Mendel, Florian and Preneel, Bart and Rechberger, Christian},
1045 + title = {Collisions and Other Non-random Properties for Step-Reduced {SHA-256}},
1046 + booktitle = {Selected Areas in Cryptography --- SAC 2008},
1047 + series = {Lecture Notes in Computer Science},
1048 + volume = {5381},
1049 + publisher = {Springer},
1050 + year = {2009},
1051 + pages = {276--293},
1052 + doi = {10.1007/978-3-642-04159-4_18},
1053 + note = {Also IACR Cryptology ePrint Archive, Report 2008/131}
1054 +}
1055 +
1056 +@incollection{sanadhya2008new,
1057 + author = {Sanadhya, Somitra Kumar and Sarkar, Palash},
1058 + title = {New Collision Attacks against Up to 24-Step {SHA-2}},
1059 + booktitle = {Progress in Cryptology --- INDOCRYPT 2008},
1060 + series = {Lecture Notes in Computer Science},
1061 + volume = {5365},
1062 + publisher = {Springer},
1063 + year = {2008},
1064 + pages = {91--103},
1065 + doi = {10.1007/978-3-540-89754-5_8},
1066 + note = {Also IACR Cryptology ePrint Archive, Report 2008/270}
1067 +}
1068 +
1069 +@incollection{mendel2011finding,
1070 + author = {Mendel, Florian and Nad, Tomislav and Schl{\"a}ffer, Martin},
1071 + title = {Finding {SHA-2} Characteristics: Searching through a Minefield of Contradictions},
1072 + booktitle = {Advances in Cryptology --- ASIACRYPT 2011},
1073 + series = {Lecture Notes in Computer Science},
1074 + volume = {7073},
1075 + publisher = {Springer},
1076 + year = {2011},
1077 + pages = {288--307},
1078 + doi = {10.1007/978-3-642-25385-0_16}
1079 +}
1080 +
1081 +@incollection{mendel2013improving,
1082 + author = {Mendel, Florian and Nad, Tomislav and Schl{\"a}ffer, Martin},
1083 + title = {Improving Local Collisions: New Attacks on Reduced {SHA-256}},
1084 + booktitle = {Advances in Cryptology --- EUROCRYPT 2013},
1085 + series = {Lecture Notes in Computer Science},
1086 + volume = {7881},
1087 + publisher = {Springer},
1088 + year = {2013},
1089 + pages = {262--278},
1090 + doi = {10.1007/978-3-642-38348-9_16},
1091 + note = {Also IACR Cryptology ePrint Archive, Report 2015/350}
1092 +}
1093 +
1094 +@incollection{eichlseder2014branching,
1095 + author = {Eichlseder, Maria and Mendel, Florian and Schl{\"a}ffer, Martin},
1096 + title = {Branching Heuristics in Differential Collision Search with Applications to {SHA-512}},
1097 + booktitle = {Fast Software Encryption --- FSE 2014},
1098 + series = {Lecture Notes in Computer Science},
1099 + volume = {8540},
1100 + publisher = {Springer},
1101 + year = {2015},
1102 + pages = {473--488},
1103 + doi = {10.1007/978-3-662-46706-0_24},
1104 + note = {Also IACR Cryptology ePrint Archive, Report 2014/302}
1105 +}
1106 +
1107 +@incollection{dobraunig2015analysis,
1108 + author = {Dobraunig, Christoph and Eichlseder, Maria and Mendel, Florian},
1109 + title = {Analysis of {SHA-512/224} and {SHA-512/256}},
1110 + booktitle = {Advances in Cryptology --- ASIACRYPT 2015},
1111 + series = {Lecture Notes in Computer Science},
1112 + volume = {9453},
1113 + publisher = {Springer},
1114 + year = {2015},
1115 + pages = {612--630},
1116 + doi = {10.1007/978-3-662-48800-3_25},
1117 + note = {Also IACR Cryptology ePrint Archive, Report 2016/374}
1118 +}
1119 +
1120 +@incollection{li2024new,
1121 + author = {Li, Yingxin and Liu, Fukang and Wang, Gaoli},
1122 + title = {New Records in Collision Attacks on {SHA-2}},
1123 + booktitle = {Advances in Cryptology --- EUROCRYPT 2024},
1124 + series = {Lecture Notes in Computer Science},
1125 + volume = {14654},
1126 + publisher = {Springer},
1127 + year = {2024},
1128 + pages = {158--186},
1129 + doi = {10.1007/978-3-031-58716-0_6},
1130 + note = {Also IACR Cryptology ePrint Archive, Report 2024/349; extended journal version in Journal of Cryptology 39 (2026), article 27, doi:10.1007/s00145-026-09582-9}
1131 +}
1132 +
1133 +@incollection{li2024first,
1134 + author = {Li, Yingxin and Liu, Fukang and Wang, Gaoli and Dong, Xiaoyang and Sun, Siwei},
1135 + title = {The First Practical Collision for 31-Step {SHA-256}},
1136 + booktitle = {Advances in Cryptology --- ASIACRYPT 2024},
1137 + series = {Lecture Notes in Computer Science},
1138 + publisher = {Springer},
1139 + year = {2024},
1140 + pages = {237--266},
1141 + doi = {10.1007/978-981-96-0941-3_8}
1142 +}
1143 +
1144 +@misc{zhang2026collision,
1145 + author = {Zhang, Zhuolong and Li, Muzhou and Gao, Lei and Wang, Meiqin},
1146 + title = {Collision Attacks on {SHA-256} up to 37 Steps with Improved Trail Search},
1147 + howpublished = {IACR Cryptology ePrint Archive, Report 2026/232},
1148 + year = {2026},
1149 + url = {https://eprint.iacr.org/2026/232}
1150 +}
1151 +
1152 +@incollection{aoki2009preimages,
1153 + author = {Aoki, Kazumaro and Guo, Jian and Matusiewicz, Krystian and Sasaki, Yu and Wang, Lei},
1154 + title = {Preimages for Step-Reduced {SHA-2}},
1155 + booktitle = {Advances in Cryptology --- ASIACRYPT 2009},
1156 + series = {Lecture Notes in Computer Science},
1157 + volume = {5912},
1158 + publisher = {Springer},
1159 + year = {2009},
1160 + pages = {578--597},
1161 + doi = {10.1007/978-3-642-10366-7_34}
1162 +}
1163 +
1164 +@incollection{khovratovich2012bicliques,
1165 + author = {Khovratovich, Dmitry and Rechberger, Christian and Savelieva, Alexandra},
1166 + title = {Bicliques for Preimages: Attacks on {Skein-512} and the {SHA-2} Family},
1167 + booktitle = {Fast Software Encryption --- FSE 2012},
1168 + series = {Lecture Notes in Computer Science},
1169 + volume = {7549},
1170 + publisher = {Springer},
1171 + year = {2012},
1172 + pages = {244--263},
1173 + doi = {10.1007/978-3-642-34047-5_15},
1174 + note = {Also IACR Cryptology ePrint Archive, Report 2011/286}
1175 +}
1176 +
1177 +@incollection{mironov2006applications,
1178 + author = {Mironov, Ilya and Zhang, Lintao},
1179 + title = {Applications of {SAT} Solvers to Cryptanalysis of Hash Functions},
1180 + booktitle = {Theory and Applications of Satisfiability Testing --- SAT 2006},
1181 + series = {Lecture Notes in Computer Science},
1182 + volume = {4121},
1183 + publisher = {Springer},
1184 + year = {2006},
1185 + pages = {102--115},
1186 + doi = {10.1007/11814948_13}
1187 +}
1188 +
1189 +@incollection{nejati2017adaptive,
1190 + author = {Nejati, Saeed and Liang, Jia Hui and Gebotys, Catherine and Czarnecki, Krzysztof and Ganesh, Vijay},
1191 + title = {Adaptive Restart and {CEGAR}-Based Solver for Inverting Cryptographic Hash Functions},
1192 + booktitle = {Verified Software: Theories, Tools, and Experiments --- VSTTE 2017},
1193 + series = {Lecture Notes in Computer Science},
1194 + volume = {10712},
1195 + publisher = {Springer},
1196 + year = {2017},
1197 + pages = {120--131},
1198 + doi = {10.1007/978-3-319-72308-2_8}
1199 +}
1200 +
1201 +@inproceedings{alamgir2024sha256,
1202 + author = {Alamgir, Nahiyan and Nejati, Saeed and Bright, Curtis},
1203 + title = {{SHA-256} Collision Attack with Programmatic {SAT}},
1204 + booktitle = {Joint Proceedings of the 9th Workshop on Practical Aspects of Automated Reasoning (PAAR) and the 9th Satisfiability Checking and Symbolic Computation Workshop (SC-Square)},
1205 + series = {CEUR Workshop Proceedings},
1206 + volume = {3717},
1207 + publisher = {CEUR-WS.org},
1208 + year = {2024},
1209 + pages = {91--110},
1210 + url = {https://ceur-ws.org/Vol-3717/},
1211 + note = {Also arXiv:2406.20072}
1212 +}
1213 +
1214 +@inproceedings{biere2020cadical,
1215 + author = {Biere, Armin and Fazekas, Katalin and Fleury, Mathias and Heisinger, Maximilian},
1216 + title = {{CaDiCaL}, {Kissat}, {Paracooba}, {Plingeling} and {Treengeling} Entering the {SAT} Competition 2020},
1217 + booktitle = {Proceedings of SAT Competition 2020 --- Solver and Benchmark Descriptions},
1218 + editor = {Balyo, Tomas and Froleyks, Nils and Heule, Marijn and Iser, Markus and J{\"a}rvisalo, Matti and Suda, Martin},
1219 + series = {Department of Computer Science Report Series B},
1220 + volume = {B-2020-1},
1221 + publisher = {University of Helsinki},
1222 + year = {2020},
1223 + pages = {50--53}
1224 +}
1225 +
1226 +@incollection{biere2024cadical,
1227 + author = {Biere, Armin and Faller, Tobias and Fazekas, Katalin and Fleury, Mathias and Froleyks, Nils and Pollitt, Florian},
1228 + title = {{CaDiCaL} 2.0},
1229 + booktitle = {Computer Aided Verification --- CAV 2024},
1230 + series = {Lecture Notes in Computer Science},
1231 + volume = {14682},
1232 + publisher = {Springer},
1233 + year = {2024},
1234 + pages = {133--152},
1235 + doi = {10.1007/978-3-031-65627-9_7}
1236 +}
1237 +
1238 +@incollection{demoura2008z3,
1239 + author = {de Moura, Leonardo and Bj{\o}rner, Nikolaj},
1240 + title = {{Z3}: An Efficient {SMT} Solver},
1241 + booktitle = {Tools and Algorithms for the Construction and Analysis of Systems --- TACAS 2008},
1242 + series = {Lecture Notes in Computer Science},
1243 + volume = {4963},
1244 + publisher = {Springer},
1245 + year = {2008},
1246 + pages = {337--340},
1247 + doi = {10.1007/978-3-540-78800-3_24}
1248 +}
1249 +
1250 +@incollection{tseitin1983complexity,
1251 + author = {Tseitin, Grigori S.},
1252 + title = {On the Complexity of Derivation in Propositional Calculus},
1253 + booktitle = {Automation of Reasoning: 2: Classical Papers on Computational Logic 1967--1970},
1254 + publisher = {Springer},
1255 + year = {1983},
1256 + pages = {466--483},
1257 + doi = {10.1007/978-3-642-81955-1_28},
1258 + note = {Originally published in 1968 (Russian), translated 1970}
1259 +}
1260 +
1261 +@incollection{demoura2021lean,
1262 + author = {de Moura, Leonardo and Ullrich, Sebastian},
1263 + title = {The {Lean} 4 Theorem Prover and Programming Language},
1264 + booktitle = {Automated Deduction --- CADE 28},
1265 + series = {Lecture Notes in Computer Science},
1266 + volume = {12699},
1267 + publisher = {Springer},
1268 + year = {2021},
1269 + pages = {625--635},
1270 + doi = {10.1007/978-3-030-79876-5_37}
1271 +}
1272 +
1273 +@article{boving2025interactive,
1274 + author = {B{\"o}ving, Henrik and Bhat, Siddharth and Cicolini, Luisa and Keizer, Alex and Frenot, Leon and Mohamed, Abdalrhman and Stefanesco, Leo and Khan, Harun and Clune, Josh and Barrett, Clark and Grosser, Tobias},
1275 + title = {Interactive Bitvector Reasoning Using Verified Bit-Blasting},
1276 + journal = {Proceedings of the ACM on Programming Languages},
1277 + year = {2025},
1278 + volume = {9},
1279 + number = {OOPSLA2},
1280 + pages = {3259--3285},
1281 + doi = {10.1145/3763167},
1282 + note = {The Lean 4 \texttt{bv\_decide} tactic}
1283 +}
1284 +
1285 +@article{fawzi2022discovering,
1286 + author = {Fawzi, Alhussein and Balog, Matej and Huang, Aja and Hubert, Thomas and Romera-Paredes, Bernardino and Barekatain, Mohammadamin and Novikov, Alexander and Ruiz, Francisco J. R. and Schrittwieser, Julian and Swirszcz, Grzegorz and Silver, David and Hassabis, Demis and Kohli, Pushmeet},
1287 + title = {Discovering Faster Matrix Multiplication Algorithms with Reinforcement Learning},
1288 + journal = {Nature},
1289 + year = {2022},
1290 + volume = {610},
1291 + pages = {47--53},
1292 + doi = {10.1038/s41586-022-05172-4}
1293 +}
1294 +
1295 +@article{romeraparedes2024mathematical,
1296 + author = {Romera-Paredes, Bernardino and Barekatain, Mohammadamin and Novikov, Alexander and Balog, Matej and Kumar, M. Pawan and Dupont, Emilien and Ruiz, Francisco J. R. and Ellenberg, Jordan S. and Wang, Pengming and Fawzi, Omar and Kohli, Pushmeet and Fawzi, Alhussein},
1297 + title = {Mathematical Discoveries from Program Search with Large Language Models},
1298 + journal = {Nature},
1299 + year = {2024},
1300 + volume = {625},
1301 + pages = {468--475},
1302 + doi = {10.1038/s41586-023-06924-6}
1303 +}
1304 +
1305 +@article{davies2021advancing,
1306 + author = {Davies, Alex and Veli{\v{c}}kovi{\'c}, Petar and Buesing, Lars and Blackwell, Sam and Zheng, Daniel and Toma{\v{s}}ev, Nenad and Tanburn, Richard and Battaglia, Peter and Blundell, Charles and Juh{\'a}sz, Andr{\'a}s and Lackenby, Marc and Williamson, Geordie and Hassabis, Demis and Kohli, Pushmeet},
1307 + title = {Advancing Mathematics by Guiding Human Intuition with {AI}},
1308 + journal = {Nature},
1309 + year = {2021},
1310 + volume = {600},
1311 + pages = {70--74},
1312 + doi = {10.1038/s41586-021-04086-x}
1313 +}
1314 +
1315 +@article{trinh2024solving,
1316 + author = {Trinh, Trieu H. and Wu, Yuhuai and Le, Quoc V. and He, He and Luong, Thang},
1317 + title = {Solving Olympiad Geometry without Human Demonstrations},
1318 + journal = {Nature},
1319 + year = {2024},
1320 + volume = {625},
1321 + pages = {476--482},
1322 + doi = {10.1038/s41586-023-06747-5}
1323 +}
1324 +
1325 +@misc{lu2024ai,
1326 + author = {Lu, Chris and Lu, Cong and Lange, Robert Tjarko and Foerster, Jakob and Clune, Jeff and Ha, David},
1327 + title = {The {AI} Scientist: Towards Fully Automated Open-Ended Scientific Discovery},
1328 + howpublished = {arXiv:2408.06292},
1329 + year = {2024},
1330 + url = {https://arxiv.org/abs/2408.06292}
1331 +}
1332 +
1333 +@misc{novikov2025alphaevolve,
1334 + author = {Novikov, Alexander and V{\~u}, Ng{\^a}n and Eisenberger, Marvin and Dupont, Emilien and Huang, Po-Sen and Wagner, Adam Zsolt and Shirobokov, Sergey and Kozlovskii, Borislav and others},
1335 + title = {{AlphaEvolve}: A Coding Agent for Scientific and Algorithmic Discovery},
1336 + howpublished = {arXiv:2506.13131},
1337 + year = {2025},
1338 + url = {https://arxiv.org/abs/2506.13131}
1339 +}
1340 +
1341 +@incollection{sica2002analysis,
1342 + author = {Sica, Francesco and Ciet, Mathieu and Quisquater, Jean-Jacques},
1343 + title = {Analysis of the {G}allant-{L}ambert-{V}anstone Method Based on Efficient Endomorphisms: Elliptic and Hyperelliptic Curves},
1344 + booktitle = {Selected Areas in Cryptography --- SAC 2002},
1345 + series = {Lecture Notes in Computer Science},
1346 + volume = {2595},
1347 + publisher = {Springer},
1348 + year = {2003},
1349 + pages = {21--36},
1350 + doi = {10.1007/3-540-36492-7_3}
1351 +}
1352 +
1353 +@article{galbraith2011endomorphisms,
1354 + author = {Galbraith, Steven D. and Lin, Xibin and Scott, Michael},
1355 + title = {Endomorphisms for Faster Elliptic Curve Cryptography on a Large Class of Curves},
1356 + journal = {Journal of Cryptology},
1357 + year = {2011},
1358 + volume = {24},
1359 + number = {3},
1360 + pages = {446--469},
1361 + doi = {10.1007/s00145-010-9065-y}
1362 +}
1363 +
1364 +@incollection{galbraith2009endomorphisms,
1365 + author = {Galbraith, Steven D. and Lin, Xibin and Scott, Michael},
1366 + title = {Endomorphisms for Faster Elliptic Curve Cryptography on a Large Class of Curves},
1367 + booktitle = {Advances in Cryptology --- EUROCRYPT 2009},
1368 + series = {Lecture Notes in Computer Science},
1369 + volume = {5479},
1370 + publisher = {Springer},
1371 + year = {2009},
1372 + pages = {518--535},
1373 + doi = {10.1007/978-3-642-01001-9_30}
1374 +}
1375 +
1376 +@article{cheon2012speeding,
1377 + author = {Cheon, Jung Hee and Hong, Jin and Kim, Minkyu},
1378 + title = {Accelerating {P}ollard's Rho Algorithm on Finite Fields},
1379 + journal = {Journal of Cryptology},
1380 + year = {2012},
1381 + volume = {25},
1382 + number = {2},
1383 + pages = {195--242},
1384 + doi = {10.1007/s00145-010-9093-7},
1385 + note = {Journal version of ``Speeding up the Pollard rho method on prime fields'', ASIACRYPT 2008, doi:10.1007/978-3-540-89255-7\_29 (tag tracing)}
1386 +}
1387 +
1388 +@article{harris1960probability,
1389 + author = {Harris, Bernard},
1390 + title = {Probability Distributions Related to Random Mappings},
1391 + journal = {The Annals of Mathematical Statistics},
1392 + year = {1960},
1393 + volume = {31},
1394 + number = {4},
1395 + pages = {1045--1062},
1396 + doi = {10.1214/aoms/1177705677}
1397 +}
1398 +
1399 +@misc{bailey2009breaking,
1400 + author = {Bailey, Daniel V. and Batina, Lejla and Bernstein, Daniel J. and Birkner, Peter and Bos, Joppe W. and Chen, Hsieh-Chung and Cheng, Chen-Mou and van Damme, Gauthier and de Meulenaer, Giacomo and Dominguez Perez, Luis Julian and Fan, Junfeng and G{\"u}neysu, Tim and Gurkaynak, Frank and Kleinjung, Thorsten and Lange, Tanja and Mentens, Nele and Niederhagen, Ruben and Paar, Christof and Regazzoni, Francesco and Schwabe, Peter and Uhsadel, Leif and Van Herrewege, Anthony and Yang, Bo-Yin},
1401 + title = {Breaking {ECC2K-130}},
1402 + howpublished = {IACR Cryptology ePrint Archive, Report 2009/541},
1403 + year = {2009},
1404 + url = {https://eprint.iacr.org/2009/541},
1405 + note = {Describes the planned cross-platform attack; the Certicom challenge document of November 2009 still lists ECC2K-130 as unsolved}
1406 +}
1407 +
1408 +@article{stinson2002some,
1409 + author = {Stinson, Douglas R.},
1410 + title = {Some Baby-Step Giant-Step Algorithms for the Low {H}amming Weight Discrete Logarithm Problem},
1411 + journal = {Mathematics of Computation},
1412 + year = {2002},
1413 + volume = {71},
1414 + number = {237},
1415 + pages = {379--391},
1416 + doi = {10.1090/S0025-5718-01-01310-2}
1417 +}
1418 +
1419 +@article{hasse1936theorie,
1420 + author = {Hasse, Helmut},
1421 + title = {Zur {T}heorie der abstrakten elliptischen {F}unktionenk{\"o}rper {I}, {II}, {III}},
1422 + journal = {Journal f{\"u}r die reine und angewandte Mathematik},
1423 + year = {1936},
1424 + volume = {175},
1425 + pages = {55--62, 69--88, 193--208},
1426 + doi = {10.1515/crll.1936.175.55},
1427 + note = {Parts II and III: doi:10.1515/crll.1936.175.69, doi:10.1515/crll.1936.175.193}
1428 +}
1429 +
1430 +@article{deuring1941typen,
1431 + author = {Deuring, Max},
1432 + title = {Die {T}ypen der {M}ultiplikatorenringe elliptischer {F}unktionenk{\"o}rper},
1433 + journal = {Abhandlungen aus dem Mathematischen Seminar der Universit{\"a}t Hamburg},
1434 + year = {1941},
1435 + volume = {14},
1436 + pages = {197--272},
1437 + doi = {10.1007/BF02940746}
1438 +}
added paper/sections/01_introduction.tex +75 −0
@@ -0,0 +1,75 @@
1 +\section{Introduction}
2 +\label{sec:intro}
3 +
4 +The security of Bitcoin reduces to three mathematical assumptions: that discrete logarithms in the group of
5 +rational points of the curve \secp{} cost about $\sqrt n\approx 2^{128}$ group operations, that SHA-256 is preimage-
6 +and collision-resistant at the generic $2^{256}$ and $2^{128}$ levels, and that ECDSA and BIP340 Schnorr signatures are
7 +unforgeable under these assumptions. The first of these is a ``square-root barrier'': in the generic-group model it
8 +is a theorem \citep{shoup1997lower}, and the only way past it is an algorithm that exploits the specific
9 +representation of the group. \secp{} has an unusually rich representation---a generalised-Mersenne prime, the
10 +equation $y^2=x^3+7$ with $j$-invariant~0, complex multiplication by the Eisenstein integers and an automorphism group
11 +of order six---and this has fed a persistent folklore that the curve's ``special structure'' might hide a shortcut.
12 +The known mathematics says the structure yields constant factors (a $\sqrt6$ reduction in Pollard's rho, a halving of
13 +doublings in scalar multiplication) and nothing more; the known computational records stop at 112--113 bits.
14 +
15 +This paper asks whether a systematic, automated exploration of that structure---and of the corresponding structure
16 +in the signature schemes and in SHA-256---can find anything the literature has missed, and it answers the question
17 +with the discipline the question deserves. We set up a six-stage discovery pipeline (verified literature database,
18 +structured hypotheses, exact symbolic checks, seeded experiments with provenance, machine-checked statements in
19 +Lean~4, and a six-way classification of every outcome), executed it autonomously under a written mandate, and let
20 +it evaluate 25 hypotheses across four axes: the algebraic structure of \secp{} (Axis~A), the complexity of generic
21 +and preprocessing discrete-logarithm algorithms (Axis~B), reduced-step SHA-256 (Axis~C), and the methodology itself
22 +(Axis~D). The mandate's first instruction was ``do not assume such shortcuts exist''; the paper's first result is that
23 +none were found, and that the absence is documented quantitatively rather than asserted.
24 +
25 +\paragraph{Contributions.} The contributions are of four kinds, and we label each with the classification it
26 +received.
27 +
28 +\emph{Algebraic structure (known results reproduced, with one explicit form we did not find written down).} We
29 +verify every \secp{} parameter and derived invariant twice with exact arithmetic (Python and PARI/GP), prove that
30 +$\End(E)=\Z[\omega]$ leaves no endomorphism to exploit beyond the six automorphisms, and show that the GLV lattice is
31 +the Eisenstein ideal $(\bar\pi-1)$, so that the constants hard-coded in \libsecp{} are literally
32 +$a_1=(t-f)/2-1$, $b_1=-f$, $a_2=a_1-b_1$, $b_2=a_1$ with $4p=t^2+3f^2$ (Proposition~\ref{prop:glv}). The pipeline's
33 +identity search found this on its own (as ``$t^2+3b_1^2=4p$'') among 22 exact identities and 0 false positives. We
34 +prove that the automorphism orbits admit no compatible group structure (Proposition~\ref{prop:noquotient}), closing
35 +the most natural ``beyond $\sqrt6$'' idea, and we quantify why summation-polynomial index calculus does not beat
36 +$\sqrt n$ over prime fields.
37 +
38 +\emph{Algorithmic complexity (known results reproduced with precise constants; one rejected shortcut).} A native
39 +engine solved $6\,000$ synthetic ECDLP instances of 20--56 bits, all verified. The fitted exponents are
40 +$0.48$--$0.50$; the rho constant is $1.254\sqrt n$ against the random-mapping prediction $1.2533$; the order-6
41 +automorphism classes reduce iterations by $2.42\times$ ($\sqrt6=2.449$) but, in our implementation, save no wall-clock
42 +time because canonicalisation costs as much as a group addition---a point the literature's record computations
43 +solve with engineering we did not replicate. Parallel collision search reaches $22\times$ on 28 threads;
44 +time--memory curves match $m+n/(2m)$ for BSGS and show the two-sided optimum of distinguished-point rho; discrete
45 +logarithms with preprocessing cost $1.5\sqrt{n/T}$ online after $T$ stored points. An automated sweep over 25
46 +iteration-function designs found nothing below the random-mapping constant (rejected shortcut). Batch verification
47 +of BIP340 signatures reaches $3.1\times$ over single verification in the same arithmetic and saturates for an
48 +identified reason.
49 +
50 +\emph{SHA-256 (empirical observations and known results reproduced).} Reduced-step experiments quantify diffusion
51 +and statistical regularity as functions of the step count, calibrate generic SAT and SMT encodings for preimage and
52 +collision search against the published dedicated-tool records, and reproduce the 9-step local collision in both
53 +the linearised and the real model; nothing bears on the 64-step function (Sections~\ref{sec:sha} and~\ref{sec:results}).
54 +
55 +\emph{Methodology and formal verification.} Bit-level identities that implementations rely on---the optimised forms
56 +of $\Ch$ and $\Maj$, linearity and bijectivity of the $\Sigma$ functions, the bijectivity of the SHA-256 step and
57 +the invertibility of the message schedule---and the abstract-group facts behind ECDSA malleability and Schnorr
58 +correctness are machine-checked in Lean~4 (Section~\ref{sec:formal}). The pipeline's record (Table~\ref{tab:hypotheses})
59 +is a majority of known results reproduced with quantified constants, two rejected conjectures with proofs or
60 +counterexamples, several empirical observations, and no claim of novelty beyond exposition; we argue that this is
61 +the correct output of an autonomous agent on this problem, and that the discipline---operation counts next to
62 +timings, two implementations per component, provenance on every number, formal statements kept separate from
63 +informal ones---is the transferable contribution.
64 +
65 +\paragraph{Scope and integrity.} All attacks were run on synthetic curves and keys generated by the experiments; no
66 +real key, address or blockchain datum was used; full-strength primitives were analysed only through legitimate
67 +implementations and exact algebra. The repository releases every script, result and figure source with a
68 +reproducibility statement (Appendix~\ref{app:repro}).
69 +
70 +\paragraph{Organisation.} Section~\ref{sec:foundations} fixes the mathematical foundations and the meaning of the
71 +barrier; Section~\ref{sec:lit} reviews the literature and the research gaps; Sections~\ref{sec:secp}--\ref{sec:sha}
72 +develop the theory of the three axes with the algebraic results; Section~\ref{sec:framework} describes the discovery
73 +pipeline; Sections~\ref{sec:design} and~\ref{sec:results} give the experimental design and results;
74 +Section~\ref{sec:formal} lists the formal results and the negative findings; Sections~\ref{sec:implications}
75 +and~\ref{sec:limitations} discuss implications and limitations; Section~\ref{sec:conclusion} concludes.
added paper/sections/02_foundations.tex +151 −0
@@ -0,0 +1,151 @@
1 +\section{Cryptographic Foundations of Bitcoin}
2 +\label{sec:foundations}
3 +
4 +Bitcoin's security rests on three computational assumptions that are structurally unrelated: the hardness of the
5 +discrete-logarithm problem in the group of rational points of one specific elliptic curve, \secp{}; the preimage and
6 +collision resistance of SHA-256 (used directly, doubled as SHA-256d in the proof of work, composed with RIPEMD-160
7 +in legacy addresses, and in the tagged-hash form of BIP340/BIP341); and the unforgeability of two signature schemes,
8 +ECDSA and BIP340 Schnorr, which reduce---under different assumptions and with very different proof structures---to
9 +the first two. Figure~\ref{fig:depmap} summarises the dependency structure; the dashed box contains the three
10 +hardness assumptions whose mathematical structure is the subject of this paper.
11 +
12 +\begin{figure}[htbp]
13 + \centering
14 + \input{figures/fig_dependency_map}
15 + \caption{Cryptographic dependency map of Bitcoin (illustrative diagram). Arrows point from a component to the
16 + components that depend on it. Research Axes~A--C of this paper concern the three dashed hardness assumptions; Axis~D
17 + concerns the method by which such assumptions can be probed automatically.}
18 + \label{fig:depmap}
19 +\end{figure}
20 +
21 +This section fixes notation and states the mathematical facts the rest of the paper builds on. We give proofs or
22 +exact verification scripts for the facts that are specific to \secp{} (Section~\ref{sec:secp}); classical theorems are
23 +cited to the standard references \citep{silverman2009arithmetic,washington2008elliptic,hankerson2004guide,galbraith2012mathematics}.
24 +
25 +\subsection{Finite-field arithmetic}
26 +
27 +Let $p$ be an odd prime and $\F_p = \Z/p\Z$. Addition, subtraction and multiplication are integer operations followed
28 +by reduction modulo $p$; inversion uses the extended Euclidean algorithm or Fermat's identity $a^{-1}=a^{p-2}$. For the
29 +\secp{} prime $p = 2^{256}-2^{32}-977$ the reduction can exploit the special form: writing a $512$-bit product as
30 +$c = c_1 2^{256} + c_0$ gives $c \equiv c_0 + c_1(2^{32}+977) \pmod p$, and two such folds reduce to $[0,2p)$. This is a
31 +generalised-Mersenne reduction in the sense of \citet{solinas1999generalized}; it is a constant-factor implementation
32 +advantage, not an algebraic weakness---the multiplicative group $\F_p^*$ has order $p-1 = 2\cdot 3\cdot 7\cdot 13441\cdot q$
33 +with $q$ a large prime, and no structure of $\F_p$ enters any known attack on the curve. Square roots are needed to
34 +decompress points: since $p\equiv 3 \pmod 4$, $\sqrt{a} = a^{(p+1)/4}$ whenever $a$ is a quadratic residue. All our
35 +implementations use exact integer arithmetic (Python integers, PARI/GP, Rust \code{u128} products); floating point is
36 +never used for a field operation.
37 +
38 +\subsection{Elliptic curves and the group law}
39 +
40 +An elliptic curve over $\F_p$ ($p>3$) in short Weierstrass form is
41 +\begin{equation}
42 + E:\quad y^2 = x^3 + ax + b, \qquad a,b\in\F_p,\quad \Delta = -16(4a^3 + 27b^2)\ne 0 .
43 + \label{eq:weierstrass}
44 +\end{equation}
45 +The set $E(\F_p)$ of solutions $(x,y)\in\F_p^2$ together with a point at infinity $\Oinf$ forms an abelian group with
46 +$\Oinf$ as identity and $-(x,y) = (x,-y)$. For $P=(x_1,y_1)$, $Q=(x_2,y_2)$ with $P\ne\pm Q$,
47 +\begin{equation}
48 + P+Q = (x_3,y_3),\qquad \lambda = \frac{y_2-y_1}{x_2-x_1},\quad x_3 = \lambda^2 - x_1 - x_2,\quad y_3 = \lambda(x_1-x_3)-y_1,
49 + \label{eq:add}
50 +\end{equation}
51 +and for doubling ($y_1\ne 0$) the same formulas apply with the tangent slope $\lambda = (3x_1^2+a)/(2y_1)$
52 +(Figure~\ref{fig:ecgeom}). That~\eqref{eq:add} returns a point of $E$ is a polynomial identity modulo the two curve
53 +equations; we verified it symbolically (check~S1 of \code{theory/elliptic\_curves/symbolic\_checks.py}, reduction of the
54 +numerator of $y_3^2 - x_3^3 - ax_3 - b$ by a Gr\"obner basis of the ideal generated by the curve equations gives
55 +zero). Associativity is the one group axiom that is not a one-line identity; the standard proofs go through the
56 +Riemann--Roch theorem or an explicit, lengthy polynomial verification \citep[Ch.~2]{washington2008elliptic}.
57 +
58 +\begin{figure}[htbp]
59 + \centering
60 + \includegraphics[width=0.9\textwidth]{ec_geometry.pdf}
61 + \caption{Geometry of the group law (illustrative, over $\R$): the chord through $P$ and $Q$ meets the curve in a third
62 + point whose reflection is $P+Q$; the tangent at $P$ gives $2P$. Over $\F_p$ the same formulas~\eqref{eq:add} apply
63 + with no geometric picture. Generated by \code{scripts/fig\_ec\_geometry.py}.}
64 + \label{fig:ecgeom}
65 +\end{figure}
66 +
67 +Projective or Jacobian coordinates avoid the field inversion in~\eqref{eq:add} \citep{cohen1998efficient}; our
68 +reference code counts group operations in the affine model (one inversion per operation), while the Rust engine
69 +amortises inversions across parallel walks with Montgomery's simultaneous-inversion trick \citep{montgomery1987speeding}.
70 +Operation counts (additions, doublings, inversions, multiplications) are reported next to wall-clock time throughout.
71 +
72 +\subsection{Scalar multiplication}
73 +
74 +For $k\in\Z$ and $P\in E(\F_p)$, $[k]P = P + \cdots + P$ ($k$ terms). Left-to-right double-and-add costs
75 +$\ell$ doublings and on average $\ell/2$ additions for an $\ell$-bit scalar; signed-window (wNAF) recoding with window
76 +$w$ reduces the additions to about $\ell/(w+1)$ plus $2^{w-2}$ precomputed points \citep{hankerson2004guide}.
77 +Double-scalar products $[u]P+[v]Q$, which dominate signature verification, are computed by Straus--Shamir
78 +interleaving \citep{straus1964addition} and multi-scalar products $\sum_i [a_i]P_i$ by Pippenger's bucket method
79 +\citep{pippenger1980evaluation}; see Section~\ref{sec:signatures}.
80 +
81 +\subsection{Group order, Hasse's theorem and subgroup structure}
82 +
83 +\begin{theorem}[Hasse]\label{thm:hasse}
84 +$\lvert\,\#E(\F_p) - (p+1)\,\rvert \le 2\sqrt{p}$.
85 +\end{theorem}
86 +The integer $t = p+1-\#E(\F_p)$ is the trace of the Frobenius endomorphism $\pi:(x,y)\mapsto(x^p,y^p)$, which
87 +satisfies $\pi^2 - t\pi + p = 0$ in $\End(E)$. Point counting is polynomial time by Schoof's algorithm and its
88 +Elkies--Atkin improvements \citep{schoof1985elliptic,schoof1995counting}; we use PARI/GP's \code{ellcard} to generate
89 +prime-order synthetic curves of 20--56 bits and to re-verify $\#\secp(\F_p)$. The group $E(\F_p)$ is either cyclic or a
90 +product of two cyclic groups; when $\#E(\F_p)=n$ is prime, as for \secp{}, every non-identity point generates the whole
91 +group and the cofactor is $h=1$.
92 +
93 +\subsection{Isomorphisms, twists and the $j$-invariant}
94 +
95 +Two curves $y^2=x^3+ax+b$ and $y^2=x^3+a'x+b'$ are isomorphic over $\F_p$ iff $a'=u^4a$, $b'=u^6b$ for some
96 +$u\in\F_p^*$; the $j$-invariant $j = 1728\cdot 4a^3/(4a^3+27b^2)$ classifies curves up to isomorphism over
97 +$\overline{\F}_p$. Curves with $a=0$ have $j=0$ and admit, when $p\equiv 1\pmod 3$, six isomorphisms to themselves
98 +(automorphisms) rather than the generic two. The quadratic twist $E'$ of $E$ has $\#E'(\F_p) = p+1+t$; its order
99 +matters because an implementation that does not validate inputs may be driven onto the twist (``twist security'').
100 +
101 +\subsection{Endomorphisms and complex multiplication}
102 +
103 +$\End(E)$, the ring of rational maps $E\to E$ preserving $\Oinf$, always contains $\Z$ (scalar multiplications) and
104 +$\pi$. For an ordinary curve ($t\not\equiv 0 \pmod p$), $\End(E)$ is an order in the imaginary quadratic field
105 +$K=\Q(\sqrt{t^2-4p})$ containing $\Z[\pi]$. When $j=0$ and $p\equiv 1 \pmod 3$ the map
106 +$\phi(x,y) = (\beta x, y)$ with $\beta^3=1$, $\beta\ne 1$, is an automorphism of order~3, $K = \Q(\sqrt{-3})$ and
107 +$\End(E) = \Z[\omega]$, $\omega = (-1+\sqrt{-3})/2$, the Eisenstein integers---the maximal order of $K$, so no larger
108 +endomorphism ring is possible. In particular $4p = t^2 + 3f^2$ for an integer $f$ (the conductor of $\Z[\pi]$ in
109 +$\Z[\omega]$), and every endomorphism is $u + v\phi$ with $u,v\in\Z$, of degree $u^2 - uv + v^2$. This observation
110 +is the starting point of Section~\ref{sec:secp}: the only efficiently computable endomorphisms beyond scalars are the
111 +six automorphisms, and we show that the Gallant--Lambert--Vanstone constants of \secp{} are literally the
112 +coordinates of $\bar\pi - 1$ in $\Z[\omega]$.
113 +
114 +\subsection{Scalar decomposition}
115 +
116 +If $\phi$ acts on the cyclic group $\langle G\rangle$ of prime order $n$ as $[\lambda]$ with $\lambda^2+\lambda+1\equiv 0
117 +\pmod n$, any scalar can be written $k \equiv k_1 + k_2\lambda \pmod n$ with $\lvert k_1\rvert,\lvert k_2\rvert = O(\sqrt n)$ by
118 +reducing $(k,0)$ modulo the lattice $L = \{(u,v): u+v\lambda\equiv 0 \pmod n\}$ \citep{gallant2001faster}. Then
119 +$[k]P = [k_1]P + [k_2]\phi(P)$ is a double-scalar product with half-length scalars, which halves the number of
120 +doublings. The bound $\max(\lvert k_1\rvert,\lvert k_2\rvert) < c\sqrt n$ for an explicit $c$ is proved in
121 +\citet{sica2002analysis}; we measure it exactly in Section~\ref{sec:results}.
122 +
123 +\subsection{The discrete-logarithm problem and generic-group complexity}
124 +
125 +\begin{definition}[ECDLP]
126 +Given $G$ of prime order $n$ and $Q\in\langle G\rangle$, find $d\in[0,n)$ with $Q=[d]G$.
127 +\end{definition}
128 +
129 +In the generic-group model \citep{shoup1997lower,nechaev1994complexity} an algorithm may only apply the group
130 +operation to opaque encodings of elements; any such algorithm needs $\Omega(\sqrt n)$ operations to solve the DLP in a
131 +group of prime order $n$ with constant probability. The matching upper bounds are Shanks' baby-step giant-step
132 +\citep{shanks1971class} ($\approx 2\sqrt n$ operations, $\sqrt n$ memory) and Pollard's rho \citep{pollard1978monte}
133 +($\approx\sqrt{\pi n/2}$ expected operations under the random-mapping heuristic, constant memory), parallelised with
134 +linear speed-up by distinguished points \citep{vanoorschot1999parallel}. Section~\ref{sec:dlog} reviews these methods
135 +and their refinements. The ``square-root barrier'' of our title is this generic bound: it can be beaten only by an
136 +algorithm that uses the \emph{representation} of the group---an algebraic attack. Known algebraic attacks on elliptic
137 +curves (transfer to $\F_{p^k}^*$ when the embedding degree $k$ is small, the anomalous-curve attack when $n=p$,
138 +index calculus via Weil descent or summation polynomials over extension fields) each require a structural property
139 +that \secp{} verifiably lacks (Section~\ref{sec:secp}). For prime fields with no small-degree subfield, no algorithm
140 +with complexity below $\sqrt n$ is known \citep{galbraith2016recent}.
141 +
142 +\subsection{What ``beyond the square-root barrier'' can and cannot mean}
143 +
144 +Three kinds of improvement must be kept apart. (i)~\emph{Constant-factor} improvements reduce the constant in
145 +$c\sqrt n$: the negation map ($c\to c/\sqrt2$), the order-6 automorphism group of $j=0$ curves ($c\to c/\sqrt6$), better
146 +iteration functions, batched inversions. (ii)~\emph{Non-uniform} or preprocessing improvements trade a one-time
147 +computation of size $T$ for a per-instance cost of $\sqrt{n/T}$, within the bound $ST^2=\tilde\Omega(n)$
148 +\citep{corrigangibbs2018discrete}; the total work is never below $\sqrt n$. (iii)~\emph{Asymptotic} improvements, which
149 +would break the generic bound, require a new algebraic idea and are the object of hypotheses~A1--A5. The paper
150 +measures (i) and (ii) precisely on synthetic groups and gives negative results for the specific algebraic candidates
151 +of~(iii) that we could formulate.
added paper/sections/03_literature.tex +141 −0
@@ -0,0 +1,141 @@
1 +\section{Literature Review and Research Gaps}
2 +\label{sec:lit}
3 +
4 +Stage~1 of the pipeline produced a structured database of 82 items (21 theorems, 17 algorithms, 6 security
5 +assumptions, 10 complexity bounds, 10 experimental records, 8 open problems, 10 negative results;
6 +\code{literature/db.yaml}) and a bibliography of 129 entries each verified against Crossref, IACR ePrint, a DOI
7 +record or the official standard. We summarise what is settled, what is open, and which of the mandate's questions
8 +the literature already answers.
9 +
10 +\subsection{The elliptic-curve discrete logarithm}
11 +
12 +The square-root barrier is a theorem in the generic-group model \citep{shoup1997lower,nechaev1994complexity}, matched by
13 +baby-step giant-step \citep{shanks1971class} and Pollard's rho \citep{pollard1978monte} with the random-mapping constant
14 +$\sqrt{\pi n/2}$ \citep{harris1960probability,flajolet1990random}, attained in practice by $r$-adding walks with
15 +$r\ge 20$ \citep{teske1998speeding,teske2001random} and parallelised with linear speed-up by distinguished points
16 +\citep{vanoorschot1999parallel}. With $S$ bits of curve-specific precomputation the online cost obeys
17 +$ST^2=\tilde\Omega(n)$ \citep{corrigangibbs2018discrete}, which the rho-based preprocessing of \citet{bernstein2013nonuniform}
18 +and \citet{lee2011generic} matches. Automorphisms give constant factors: $\sqrt2$ for negation and $\sqrt6$ for the
19 +$j=0$ family \citep{wiener1999faster,duursma1999speeding,gallant2000improving}, provided fruitless cycles are handled
20 +\citep{bos2010negation,bernstein2011correct}; \citet{bernstein2012two} and \citet{galbraith2010interval} sharpen the
21 +BSGS and kangaroo constants, and \citet{cheon2012speeding} show (for $\F_p^*$) that the representation of group
22 +elements can make each rho step cheaper without changing the step count. Algebraic attacks need structure that
23 +\secp{} lacks: small embedding degree \citep{menezes1993reducing,frey1994remark}, anomalous order
24 +\citep{semaev1998evaluation,smart1999discrete,satoh1998fermat}, composite order \citep{pohlig1978improved}, or an
25 +extension field for summation-polynomial index calculus \citep{semaev2004summation,gaudry2009index,diem2011discrete,petit2012elliptic};
26 +\citet{galbraith2016recent} survey the state of the art and record that no sub-exponential algorithm is known for
27 +prime fields, and \citet{bernstein2014safecurves} tabulate the exclusion criteria for standard curves.
28 +
29 +Table~\ref{tab:ecdlp-records} lists the completed ECDLP computations we could verify. The largest prime-field
30 +instance remains the 112-bit computation of \citet{bos2012solving} (July 2009); the binary-field records are 113-bit
31 +curves \citep{wenger2014solving,wenger2016harder,bernstein2016faster}; the 131-bit Certicom challenges are open
32 +\citep{certicom2009challenge}. The gap to \secp{} is a factor of about $2^{71}$ in expected group operations.
33 +
34 +\begin{table}[htbp]
35 + \centering\footnotesize
36 + \begin{threeparttable}
37 + \caption{Completed elliptic-curve discrete-logarithm computations (verified records only).}
38 + \label{tab:ecdlp-records}
39 + \begin{tabular}{@{}llllll@{}}
40 + \toprule
41 + Instance & Bits & Field & Hardware & Completed & Source \\
42 + \midrule
43 + ECCp-79/-89/-97 & 79/89/97 & prime & distributed PCs & 1997--1998 & \citet{certicom2009challenge} \\
44 + ECC2K-108 & 108 & binary, Koblitz & distributed ($1.3\cdot10^6$ machine-days) & Apr.\ 2000 & \citet{certicom2009challenge} \\
45 + ECCp-109 & 109 & prime & distributed ($9.0\cdot10^6$ machine-days) & Oct.\ 2002 & \citet{certicom2009challenge} \\
46 + ECC2-109 & 109 & binary & distributed ($2.1\cdot10^7$ machine-days) & Apr.\ 2004 & \citet{certicom2009challenge} \\
47 + secp112r1 & 112 & prime & $\approx 215$ PlayStation~3 & Jul.\ 2009 & \citet{bos2012solving} \\
48 + 113-bit Koblitz & 113 & binary, Koblitz & 18 Virtex-6 FPGAs & 2014 & \citet{wenger2014solving} \\
49 + sect113r1 & 113 & binary & 10 Kintex-7 FPGAs & 2015 & \citet{wenger2016harder} \\
50 + sect113r2 & 113 & binary & Spartan-6 FPGAs & 2016 & \citet{bernstein2016faster} \\
51 + ECCp-131, ECC2-131, ECC2K-130 & 130--131 & prime/binary & --- & open & \citet{certicom2009challenge,bailey2009breaking} \\
52 + \bottomrule
53 + \end{tabular}
54 + \begin{tablenotes}\footnotesize
55 + \item Figures as stated in the sources (\code{literature/records.md}). \secp{} has $n\approx 2^{256}$: rho with the negation map needs $\approx\sqrt{\pi n/4}\approx 2^{127.5}$ operations, about $2^{71}$ times the 112-bit computation.
56 + \end{tablenotes}
57 + \end{threeparttable}
58 +\end{table}
59 +
60 +\subsection{Signatures}
61 +
62 +Schnorr signatures are EUF-CMA under the discrete-logarithm assumption in the random-oracle model by the forking
63 +lemma \citep{pointcheval2000security}; the loss in the number of hash queries is unavoidable for algebraic reductions
64 +and the random oracle cannot be removed \citep{paillier2005discrete,seurin2012exact}. ECDSA requires an idealisation of
65 +the conversion function $R\mapsto x(R)\bmod n$ \citep{brown2005generic,vaudenay2003security,fersch2016provable,fersch2017one},
66 +extended to additive key derivation and presignatures by \citet{groth2022security}. Nonce leakage is fatal
67 +\citep{howgravegraham2001lattice,breitner2019biased,aldaya2020ladderleak,jancar2020minerva}, hence deterministic nonces
68 +\citep{pornin2013rfc6979,wuille2020bip340}. Batch verification of Schnorr-type signatures is classical
69 +\citep{naccache1994can,bellare1998fast,bernstein2012high} and rests on multi-scalar multiplication
70 +\citep{straus1964addition,pippenger1980evaluation,moller2001algorithms}; multi-signatures build on the same linearity
71 +\citep{bellare2006multi,maxwell2019simple,nick2021musig2}. The reference implementation is \libsecp{} \citep{wuille2013libsecp256k1}.
72 +
73 +\subsection{SHA-256}
74 +
75 +Beyond the Merkle--Damg\aa rd reduction \citep{merkle1989one,damgard1989design} and the structural analysis of
76 +\citet{gilbert2003security}, knowledge about SHA-256 is knowledge about reduced step counts
77 +(Table~\ref{tab:sha-lit}). Collision attacks progressed from 21--24 steps in 2008
78 +\citep{nikolic2008collisions,indesteege2008collisions,sanadhya2008new} to 27--28 practical steps
79 +\citep{mendel2011finding,mendel2013improving}, 31 practical steps \citep{li2024first} and a theoretical 37-step
80 +attack \citep{zhang2026collision}; semi-free-start collisions reach 38--39 steps \citep{mendel2013improving,li2024new};
81 +preimages reach 43 steps \citep{aoki2009preimages} and 45 steps (52 for the compression function) at complexities
82 +within a small factor of brute force \citep{khovratovich2012bicliques}. SAT-based analysis started with
83 +\citet{mironov2006applications}; pure SAT search stalls around 28 steps while SAT combined with computer algebra
84 +reproduces the 38-step semi-free-start result \citep{nejati2017adaptive,alamgir2024sha256}. The full SHA-1 collisions
85 +\citep{stevens2017first,leurent2020sha1} are the benchmark for what a mature differential attack on a 80-step design
86 +costs ($2^{63}$).
87 +
88 +\begin{table}[htbp]
89 + \centering\footnotesize
90 + \begin{threeparttable}
91 + \caption{Reduced-step cryptanalysis of SHA-256 (64 steps): published records.}
92 + \label{tab:sha-lit}
93 + \begin{tabular}{@{}lllll@{}}
94 + \toprule
95 + Type & Steps & Complexity & Practical? & Source \\
96 + \midrule
97 + Collision & 21 & $\approx 2^{19}$ & yes & \citet{nikolic2008collisions} \\
98 + Collision & 23 / 24 & $2^{18}$ / $2^{28.5}$ & yes & \citet{indesteege2008collisions} \\
99 + Collision & 23 / 24 & $2^{11.5}$ / $2^{28.5}$ & yes & \citet{sanadhya2008new} \\
100 + Collision & 27 & practical & yes & \citet{mendel2011finding} \\
101 + Collision & 28 (31) & practical ($\le 2^{65.5}$) & yes (no) & \citet{mendel2013improving} \\
102 + Collision & 31 & $\approx 1.2$ h on 64 threads & yes & \citet{li2024first} \\
103 + Collision & 37 & theoretical & no & \citet{zhang2026collision} \\
104 + Semi-free-start collision & 32 / 38 / 39 & practical & yes & \citet{mendel2011finding,mendel2013improving,li2024new} \\
105 + Semi-free-start (SAT+CAS) & 38 & solver reproduction & yes & \citet{alamgir2024sha256} \\
106 + Collision (pure SAT) & $\le 28$ & --- & yes & \citet{alamgir2024sha256} \\
107 + Preimage & 43 & $2^{254.9}$ & no & \citet{aoki2009preimages} \\
108 + Preimage (hash / compression) & 45 / 52 & $\approx 2^{256}$ (bicliques) & no & \citet{khovratovich2012bicliques} \\
109 + \bottomrule
110 + \end{tabular}
111 + \begin{tablenotes}\footnotesize
112 + \item Figures as stated in the abstracts (\code{literature/records.md}). No attack on the full 64-step function is known; the generic bounds $2^{128}$ (collision) and $2^{256}$ (preimage) stand.
113 + \end{tablenotes}
114 + \end{threeparttable}
115 +\end{table}
116 +
117 +\subsection{Automated discovery}
118 +
119 +Program-search systems have produced verified mathematical constructions---matrix-multiplication tensors
120 +\citep{fawzi2022discovering}, combinatorial bounds \citep{romeraparedes2024mathematical,novikov2025alphaevolve},
121 +olympiad geometry proofs \citep{trinh2024solving}, conjectures in knot theory and representation theory
122 +\citep{davies2021advancing}---and end-to-end ``AI scientist'' pipelines have been proposed \citep{lu2024ai}. None
123 +targets standardised cryptographic primitives, where the ground truth is dominated by impossibility results, so
124 +Axis~D has no published baseline and defines its own (rediscovery rate, false-positive rate, and the quality of the
125 +negative results). On the formal side, \citet{boving2025interactive} make bit-vector facts about hash-function steps
126 +machine-checkable in Lean~4 \citep{demoura2021lean}; we use this to separate checked identities from informal ones.
127 +
128 +\subsection{Research gaps and what the mandate already settles}
129 +
130 +Of the mandate's questions, the literature settles the following (\code{literature/research\_gaps.md}): A1 (no
131 +improvement beyond constant factors from the endomorphism), A2 (orbits give exactly $\sqrt{\lvert\Aut\rvert}$), A5
132 +(generic algorithms cannot beat $\sqrt n$, with or without preprocessing), B1 (the proof structures differ by the
133 +conversion-function idealisation), B2 (batch verification is known for Schnorr, not for ECDSA), and C1/C2/C4 up to
134 +the recorded step counts. What is not settled, and what we test, is quantitative and methodological: the measured
135 +constants and their interaction with distinguished-point overheads and fruitless cycles at 20--48 bits (gap~2); the
136 +preprocessing constant on a fixed curve (gap~3); whether an automated search over iteration-function designs or over
137 +algebraic identities finds anything the theory does not predict (gaps~1, 8); how far generic SAT encodings get on
138 +step-reduced SHA-256 compared with the dedicated tools (gap~6); and whether the elementary identities that
139 +implementations rely on can be made machine-checked at negligible cost (gap~7). The closed form of the GLV
140 +constants (Proposition~\ref{prop:glv}) is the one place where the symbolic search produced something we did not find
141 +written down, and we are careful to call it an exposition of a known structure rather than a discovery.
added paper/sections/04_secp256k1.tex +150 −0
@@ -0,0 +1,150 @@
1 +\section{Mathematical Structure of secp256k1}
2 +\label{sec:secp}
3 +
4 +\subsection{Verified domain parameters}
5 +
6 +Table~\ref{tab:secp-params} lists the standardised parameters \citep{certicom2010sec2} together with the
7 +derived quantities used in this paper. Every entry was computed twice with exact arithmetic---in our pure-Python
8 +library and in PARI/GP~2.19---and the two implementations agree on all fourteen named checks
9 +(\code{results/baselines/secp256k1\_facts.json}, hypothesis~\Hname{H01}).
10 +
11 +\begin{table}[htbp]
12 + \centering\footnotesize
13 + \begin{threeparttable}
14 + \caption{secp256k1 parameters and derived algebraic invariants (all verified exactly in Python and PARI/GP).}
15 + \label{tab:secp-params}
16 + \begin{tabular}{@{}llp{8.2cm}@{}}
17 + \toprule
18 + Quantity & Symbol & Value / verified property \\
19 + \midrule
20 + Field prime & $p$ & $2^{256}-2^{32}-977$; prime; $p\equiv 1 \pmod 3$, $p\equiv 3 \pmod 4$ \\
21 + Curve & $E$ & $y^2 = x^3 + 7$; $a=0$, $b=7$; $\Delta\ne 0$; $j(E)=0$ \\
22 + Group order & $n$ & \code{FFFF\ldots{}BAAEDCE6AF48A03BBFD25E8CD0364141}; prime; $\#E(\F_p)=n$ (SEA re-count) \\
23 + Cofactor & $h$ & $1$ \\
24 + Generator & $G$ & $x_G=$\code{79BE667E\ldots{}F81798}, $y_G=$\code{483ADA77\ldots{}10D4B8}; $G\in E$; $[n]G=\Oinf$ \\
25 + Trace of Frobenius & $t$ & $p+1-n = 432420386565659656852420866390673177327$ ($\approx 2^{128.3}$, $t^2\le 4p$) \\
26 + CM discriminant & & $t^2-4p = -3f^2$, $f = 303414439467246543595250775667605759171$ \\
27 + Endomorphism & $\beta$ & \code{7AE96A2B\ldots{}719501EE}; $\beta^3\equiv 1$, $\beta^2+\beta+1\equiv 0 \pmod p$ \\
28 + Eigenvalue & $\lambda$ & \code{5363AD4C\ldots{}1B23BD72}; $\lambda^3\equiv 1$, $\lambda^2+\lambda+1\equiv 0 \pmod n$; $(\beta x_G, y_G) = [\lambda]G$ \\
29 + GLV basis & $(a_1,b_1)$ & $a_1=$\code{3086D221A7D46BCDE86C90E49284EB15}, $b_1=-$\code{E4437ED6010E88286F547FA90ABFE4C3} \\
30 + & $(a_2,b_2)$ & $a_2=$\code{114CA50F7A8E2F3F657C1108D9D44CFD8}, $b_2=a_1$; $a_i+b_i\lambda\equiv 0 \pmod n$; $\det = n$ \\
31 + Anomalous? & & no ($n\ne p$) \\
32 + Embedding degree & $k$ & $\ord_n(p) > 10^{6}$ (PARI: $\ord_n(p)$ is a 253-bit integer); MOV/Frey--R\"uck inapplicable \\
33 + Twist order & $n'$ & $2p+2-n = 3^2\cdot 13^2\cdot 3319\cdot 22639\cdot q_{220}$, $q_{220}$ a 220-bit prime \\
34 + 2-torsion / $x=0$ & & $x^3+7$ has no root in $\F_p$ (no point of order~2); $7$ is a non-residue (no point with $x=0$) \\
35 + $n-1$, $p-1$ & & $n-1 = 2^6\cdot 3\cdot 149\cdot 631\cdot(\ldots)$; $p-1 = 2\cdot 3\cdot 7\cdot 13441\cdot(\ldots)$ (large cofactors) \\
36 + \bottomrule
37 + \end{tabular}
38 + \begin{tablenotes}\footnotesize
39 + \item Source: \code{experiments/baselines/secp256k1\_facts.py}. Hexadecimal values abbreviated; full values in the JSON result and in \code{src/wp34/secp256k1.py}.
40 + \end{tablenotes}
41 + \end{threeparttable}
42 +\end{table}
43 +
44 +\subsection{Why the special form is interesting}
45 +
46 +Three features distinguish \secp{} from a random prime-order curve: the special prime (fast reduction), $a=0$
47 +(cheaper doubling: $\lambda = 3x^2/2y$), and $j=0$ with $p\equiv 1\pmod 3$, which gives the curve complex
48 +multiplication by the Eisenstein integers and therefore an automorphism group of order~6 instead of~2. The first two
49 +are pure implementation conveniences. The third has a precise algebraic content that we now make explicit.
50 +
51 +\begin{proposition}\label{prop:end}
52 +Let $E/\F_p$ be ordinary with $j(E)=0$ and $p\equiv 1\pmod 3$. Then $\End(E)=\Z[\omega]$, every endomorphism is
53 +$u+v\phi$ with $u,v\in\Z$ and has degree $u^2-uv+v^2$, and $\Aut(E) = \{\pm 1,\pm\phi,\pm\phi^2\}\cong\Z/6$.
54 +\end{proposition}
55 +\begin{proof}
56 +$\phi$ is an automorphism of order~3 defined over $\F_p$, so $\Z[\phi]\cong\Z[\omega]\subseteq\End(E)$. Since $E$ is
57 +ordinary, $\End(E)$ is an order in the imaginary quadratic field $\End(E)\otimes\Q \supseteq \Q(\omega)=\Q(\sqrt{-3})$,
58 +hence equal to $\Q(\sqrt{-3})$, whose unique maximal order is $\Z[\omega]$. Thus $\End(E)=\Z[\omega]$. The degree of an
59 +endomorphism is its norm, $\Norm(u+v\omega)=u^2-uv+v^2$, and the units of $\Z[\omega]$ are the six sixth roots of
60 +unity \citep[Thm.~III.10.1, Ex.~III.10.?]{silverman2009arithmetic}.
61 +\end{proof}
62 +
63 +The Frobenius $\pi$ lies in $\Z[\omega]$ with $\Norm(\pi)=p$ and $\Tr(\pi)=t$, so $\pi = (t\pm f\sqrt{-3})/2$ and
64 +$4p = t^2+3f^2$---the identity the automated search of Section~\ref{sec:framework} found in the form
65 +``$t^2 + 3b_1^2 = 4p$''. For \secp{} we verified $t^2-4p=-3f^2$ exactly (Table~\ref{tab:secp-params}). Proposition~\ref{prop:end}
66 +answers research question~A1 at the structural level: there is no efficiently computable endomorphism beyond
67 +$\Z[\phi]$, because there is no endomorphism beyond $\Z[\phi]$ at all. The smallest non-unit endomorphism,
68 +$1-\phi$ of degree~3, is computable with one addition ($P - \phi(P)$) but is simply the sum of two things we already
69 +have.
70 +
71 +\subsection{The GLV lattice is the ideal $(\bar\pi-1)$}
72 +
73 +\begin{proposition}\label{prop:glv}
74 +Let $L=\{(k_1,k_2)\in\Z^2 : k_1+k_2\lambda\equiv 0 \pmod n\}$ be the GLV lattice. Under the identification
75 +$(k_1,k_2)\leftrightarrow k_1+k_2\omega$, $L$ is the principal ideal $(\bar\pi-1)\Z[\omega]$, where
76 +$\bar\pi=(t-f\sqrt{-3})/2$ is the Frobenius conjugate for which $\omega\mapsto\lambda$ sends $\bar\pi-1$ to~$0$.
77 +Consequently
78 +\begin{equation}
79 + (a_1,b_1) = \Big(\tfrac{t-f}{2}-1,\ -f\Big),\qquad (a_2,b_2) = (a_1-b_1,\ a_1) \;\;(=\omega\cdot(a_1+b_1\omega)\text{ up to sign}),
80 + \qquad a_1^2-a_1b_1+b_1^2 = n ,
81 + \label{eq:glvclosed}
82 +\end{equation}
83 +and these are exactly the constants hard-coded in \libsecp{}.
84 +\end{proposition}
85 +\begin{proof}
86 +The map $\rho:\Z[\omega]\to\Z/n\Z$, $\omega\mapsto\lambda$, is a ring homomorphism because $\lambda^2+\lambda+1\equiv 0$.
87 +Its kernel is an ideal of index $n$, hence of norm $n$; $\Z[\omega]$ is a principal ideal domain, so the kernel is
88 +$(\alpha)$ with $\Norm(\alpha)=n$. Now $n=\#E(\F_p)=\Norm(\pi-1)=\Norm(\bar\pi-1)$ and $\pi$ acts on $E(\F_p)$ as the
89 +identity, so $\pi-1$ kills every point and one of $\pi-1$, $\bar\pi-1$ lies in $\ker\rho$ (which one depends on the
90 +labelling of $\omega$ versus $\lambda$); call it $\bar\pi-1$. Since $(\bar\pi-1)$ and $\ker\rho$ are ideals of the same
91 +norm with one containing the other, they are equal. Writing $\sqrt{-3}=2\omega+1$ gives
92 +$\bar\pi-1 = \frac{t-f}{2}-1-f\omega$, i.e.\ $(a_1,b_1)$ as stated; multiplying by the unit $\omega$ gives the second
93 +basis vector; and $\Norm(\bar\pi-1)=n$ is~\eqref{eq:glvclosed}. The identification with the \libsecp{} constants is an
94 +exact integer check (\code{identity\_search.json}, field \code{closed\_form\_matches\_libsecp256k1}).
95 +\end{proof}
96 +
97 +We are not aware of a place where the \libsecp{} constants are written in this closed form (the library's comments
98 +derive them by the extended Euclidean algorithm, as in \citet{gallant2001faster}); the ideal-theoretic description of the
99 +GLV lattice for CM curves is classical \citep{sica2002analysis,galbraith2011endomorphisms}. We therefore classify
100 +Proposition~\ref{prop:glv} as a known result independently reproduced, with an explicit form that may be of
101 +expository use. It immediately yields the sharp bound $\max(\lvert k_1\rvert,\lvert k_2\rvert)\le \lvert a_1\rvert + \lvert b_1\rvert$ for
102 +Babai rounding; empirically both sub-scalars have at most 128 bits (hypothesis~\Hname{H03}).
103 +
104 +\subsection{Orbits, equivalence classes and the limits of the automorphism trick}
105 +
106 +The group $\Aut(E)$ acts on $E(\F_p)\setminus\{\Oinf\}$ freely (no non-identity automorphism fixes a point of prime
107 +order $n>3$ other than $\Oinf$), so the orbits $\{\pm P,\pm\phi P,\pm\phi^2P\}$ all have size~6 and the orbit space has
108 +$(n-1)/6$ elements. Pollard's rho can be run on orbits because a canonical representative (for instance the point with
109 +minimal $x$ among $\{x,\beta x,\beta^2 x\}$ and then minimal $y$) can be computed with two field multiplications, and
110 +the scalar bookkeeping is adjusted by powers of $\lambda$ and signs \citep{wiener1999faster,duursma1999speeding,gallant2000improving}.
111 +The random-mapping heuristic then predicts $\sqrt{\pi n/12}$ iterations instead of $\sqrt{\pi n/2}$: a factor
112 +$\sqrt6\approx 2.45$. Two costs must be paid: canonicalisation, and \emph{fruitless cycles}---short cycles of the
113 +iteration that contain no useful collision, whose frequency is $\Theta(1/r)$ for an $r$-adding walk on classes and
114 +which must be detected and escaped \citep{bos2010negation,bernstein2011correct}. Section~\ref{sec:results} measures
115 +the achieved factor and the cycle overhead (hypothesis~\Hname{H04}).
116 +
117 +Could the orbit space do more than save a constant? Hypothesis~\Hname{H05} formalises the natural conjecture that
118 +$E(\F_p)/\Aut(E)$ carries a group structure compatible with the projection, which would reduce the DLP to a group of
119 +order $\approx n/6$ (and, iterating, suggest further reductions). It is false.
120 +
121 +\begin{proposition}\label{prop:noquotient}
122 +There is no group structure on the orbit set $E(\F_p)/\Aut(E)$ for which the projection is a homomorphism.
123 +\end{proposition}
124 +\begin{proof}
125 +If the projection were a homomorphism, its kernel would be the orbit of $\Oinf$, which is $\{\Oinf\}$, so the
126 +projection would be injective---contradicting the fact that the other orbits have six elements. Equivalently,
127 +$P\sim\phi P$ and $Q\sim Q$ would force $P+Q\sim\phi P+Q$; on synthetic 16-, 20- and 24-bit $j=0$ curves this fails for
128 +every one of $1500$ random pairs (\code{results/theory/orbit\_quotient\_counterexample.json}).
129 +\end{proof}
130 +
131 +The same argument applies to any finite group of automorphisms fixing the identity: equivalence classes under
132 +automorphisms are compatible with \emph{iterations} (which is all rho needs) but never with the group law. This is
133 +the precise sense in which the structure of \secp{} yields a constant and nothing more (research questions~A2, A5).
134 +
135 +\subsection{Algebraic attacks: an exact exclusion table}
136 +
137 +The reductions that beat the generic bound each need a verifiable property. The MOV/Frey--R\"uck transfer to
138 +$\F_{p^k}^*$ needs a small embedding degree $k=\ord_n(p)$ \citep{menezes1993reducing,frey1994remark}; for \secp{}, $k$
139 +is a 253-bit integer. The anomalous-curve attack needs $n=p$ \citep{semaev1998evaluation,smart1999discrete,satoh1998fermat};
140 +here $n\ne p$. Attacks through invalid points on the twist need a smooth twist order; the twist cofactor is a 220-bit
141 +prime \citep{bernstein2014safecurves}. Weil-descent and summation-polynomial index calculus need an extension field or a
142 +subfield structure \citep{semaev2004summation,gaudry2009index,diem2011discrete,petit2012elliptic}; $\F_p$ with $p$ prime
143 +has none. Hypothesis~\Hname{H06} quantifies the last point experimentally: with a factor base $F=\{P: x(P)<\sqrt p\}$ a
144 +random point decomposes as a sum of two factor-base points with probability $1-e^{-\lvert F\rvert^2/2n}\approx 0.4$ (measured
145 +$0.31$--$0.44$ on 16--28-bit curves, in agreement with the Poisson prediction), but finding the decomposition
146 +without an oracle costs $\lvert F\rvert$ operations per trial, and even with a free oracle the $\lvert F\rvert$ relations cost
147 +$\ge 2.4\sqrt n$ and the linear algebra $\lvert F\rvert^2\approx n$. Semaev's $S_3$ polynomial, which we re-derived and
148 +verified symbolically (check~S4), has degree~2 in each variable, so the $m=2$ decomposition is a square-root problem
149 +in disguise; for $m\ge 3$ the decomposition step itself becomes the bottleneck over prime fields. No algorithm with
150 +complexity below $\sqrt n$ is known for this setting \citep{galbraith2016recent}, and our experiments give no hint of one.
added paper/sections/05_dlog_algorithms.tex +95 −0
@@ -0,0 +1,95 @@
1 +\section{Generic and Algebraic Discrete-Logarithm Algorithms}
2 +\label{sec:dlog}
3 +
4 +This section states the algorithms we implemented (twice: a transparent Python reference and a Rust engine) with their
5 +exact cost models; Section~\ref{sec:results} compares the models with measurements. Throughout, $G$ has prime order
6 +$n$ and $Q=[d]G$.
7 +
8 +\subsection{Baby-step giant-step}
9 +
10 +Shanks' method \citep{shanks1971class} stores $m$ baby steps $\{[j]G: 0\le j<m\}$ in a hash table and walks giant
11 +steps $Q-[im]G$ until a match $Q-[im]G=[j]G$ gives $d=im+j$. With $m=\lceil\sqrt n\rceil$ the cost is at most
12 +$m+\lceil n/m\rceil\approx 2\sqrt n$ group operations and $\sqrt n$ table entries; with memory $m$ the time is
13 +$m + n/m$, a deterministic time--memory trade-off. Two refinements matter: interleaving baby and giant steps halves
14 +the expected (not worst-case) cost for uniformly random $d$, and using the negation map on the baby-step table saves a
15 +further $\sqrt2$ \citep{bernstein2012two}. We benchmark the plain variant as the deterministic baseline.
16 +
17 +\subsection{Pollard's rho and $r$-adding walks}
18 +
19 +Pollard's rho \citep{pollard1978monte} iterates a pseudo-random map $f$ on $\langle G\rangle$ whose values are tracked as
20 +$a_iG+b_iQ$; a collision $X_i=X_j$ with $b_i\ne b_j$ gives $d=(a_i-a_j)/(b_j-b_i)\bmod n$. If $f$ behaved as a uniformly
21 +random mapping, the expected number of iterations before the first repetition would be $\sqrt{\pi n/2}\approx 1.2533\sqrt n$
22 +\citep{flajolet1990random}. Pollard's original three-partition map is measurably worse than random; Teske's $r$-adding
23 +walk $f(X)=X+M_{h(X)}$ with $r\approx 20$ precomputed multipliers $M_j=u_jG+v_jQ$ is within a few percent of the
24 +random-mapping constant \citep{teske1998speeding,teske2001random}, and mixed walks that include occasional doublings
25 +close the remaining gap. Cycle detection by Floyd's tortoise-and-hare costs three evaluations per step; Brent's
26 +method \citep{brent1980improved} costs about one. Our Python reference implements Floyd; the Rust engine implements
27 +Floyd, Brent and the distinguished-point variant below.
28 +
29 +\subsection{Distinguished points and parallel collision search}
30 +
31 +\citet{vanoorschot1999parallel} replace cycle detection by storing only \emph{distinguished points} (those whose
32 +$x$-coordinate has $\theta^{-1}$ specific bits equal to zero) in a shared table; $t$ processors each run independent
33 +walks from random starts $a_0G+b_0Q$, report distinguished points, and a collision between two walks with different
34 +$(a,b)$ solves the instance. The expected total work is $\sqrt{\pi n/2}+ t/\theta$ group operations (the second term is
35 +the extra distance walked after the collision point), memory is about $\theta\sqrt{\pi n/2}$ entries, and the
36 +speed-up is linear in $t$ when $t/\theta\ll\sqrt n$. This is the algorithm behind every ECDLP record
37 +\citep{bos2012solving,bernstein2016faster,bailey2009breaking}.
38 +
39 +\subsection{Equivalence classes: negation map and automorphisms}
40 +
41 +Replacing points by classes $\{\pm X\}$ halves the effective group size and divides the expected iterations by
42 +$\sqrt2$; for the $j=0$ family the order-6 classes divide them by $\sqrt6$. The price is fruitless cycles: on classes,
43 +the walk $X\mapsto X+M_j$ has probability about $1/(2r)$ of returning to $X$ after two steps (when the canonical
44 +representative flips sign), creating 2-cycles that must be detected by a one-step look-ahead and escaped
45 +\citep{bos2010negation,bernstein2011correct}; longer fruitless cycles ($4,6,\dots$) occur with probability $O(r^{-k/2})$
46 +and are handled by periodic checks and a doubling escape. Our engine implements the look-ahead and a bounded-length
47 +check and reports the number of cycles encountered.
48 +
49 +\subsection{Pollard's kangaroo for intervals}
50 +
51 +When $d$ is known to lie in an interval of width $w$, Pollard's kangaroo (lambda) method \citep{pollard1978monte,pollard2000kangaroos}
52 +runs a tame kangaroo from a known point and a wild one from $Q$, both jumping by powers of two of mean $\approx\sqrt w/2$;
53 +the wild kangaroo is caught at a trap in expected $\approx 2\sqrt w$ jumps. The van Oorschot--Wiener herd version is
54 +parallel and uses distinguished points; \citet{galbraith2010interval} improve the constant to $\approx 1.71\sqrt w$ with
55 +the negation map. We use the kangaroo as a second, structurally different baseline (and as the reason interval
56 +restrictions on nonces or keys are dangerous).
57 +
58 +\subsection{Pohlig--Hellman}
59 +
60 +If $\ord(G)=\prod q_i^{e_i}$ is smooth, the DLP reduces to the prime-order subgroups and costs
61 +$\sum_i e_i(\sqrt{q_i}+\log n)$ \citep{pohlig1978improved}; this is why $n$ must be prime (or have a large prime factor)
62 +and why the cofactor~1 of \secp{} matters. Our reference implements it on small composite-order curves for completeness.
63 +
64 +\subsection{Preprocessing (non-uniform) algorithms}
65 +
66 +\citet{bernstein2013nonuniform} observe that a one-time table of $T$ distinguished points, each ending a walk of
67 +length $W$ from a known multiple of $G$, lets every later instance be solved in about $n/(TW)$ online operations;
68 +choosing $W=\sqrt{n/T}$ gives online cost $\sqrt{n/T}$ after $\sqrt{nT}$ preprocessing. \citet{corrigangibbs2018discrete}
69 +prove that any generic algorithm with $S$ bits of advice and $T$ online queries satisfies $ST^2=\tilde\Omega(n)$, so this
70 +trade-off is optimal up to logarithmic factors. Hypothesis~\Hname{H12} measures the constant.
71 +
72 +\subsection{Lower bounds and what they rule out}
73 +
74 +Shoup's theorem \citep{shoup1997lower} is unconditional within the generic-group model: an algorithm that only uses the
75 +group operation (and equality tests) on $\langle G\rangle$ of prime order $n$ succeeds with probability $O(m^2/n)$ after
76 +$m$ operations. Every algorithm above is generic, so none can have complexity $o(\sqrt n)$; constant-factor gains
77 +from equivalence classes are the only room left (classes of size $c$ give at most $\sqrt c$, and $c\le 6$ for an
78 +elliptic curve over $\F_p$ with $p>3$ since $\lvert\Aut(E)\rvert\le 6$). The automated search of
79 +hypothesis~\Hname{H09} lives entirely in this model and therefore cannot find a shortcut; what it can find is a better
80 +constant, and we report that it does not.
81 +
82 +\subsection{Cost model and empirical complexity}
83 +
84 +For each run we record the number of group additions and doublings, the number of field inversions (one per affine
85 +operation in the reference; amortised in the engine), wall-clock time, CPU time summed over threads, table size and
86 +peak resident memory. The empirical model
87 +\begin{equation}
88 + T(n) = a\,n^{\alpha}(\log_2 n)^{\beta}
89 + \label{eq:empirical}
90 +\end{equation}
91 +is fitted by least squares on $\log T$ with $\beta$ either free or fixed to~0, and the speed-up of a candidate over a
92 +baseline at size $n$ is $S(n)=T_{\text{baseline}}(n)/T_{\text{candidate}}(n)$ with a bootstrap confidence interval over
93 +instances. For rho-type algorithms the number of iterations until collision has a Rayleigh-like distribution with
94 +coefficient of variation $\sqrt{(4-\pi)/\pi}\approx 0.52$, so at least 20 instances per size are needed for a
95 +$\pm 23\%$ standard error on the mean; we use 20--40.
added paper/sections/06_signatures.tex +93 −0
@@ -0,0 +1,93 @@
1 +\section{ECDSA and Schnorr: Security and Computational Structure}
2 +\label{sec:signatures}
3 +
4 +\subsection{ECDSA}
5 +
6 +Let $H$ be SHA-256 and $z$ the integer obtained from the leftmost $\lceil\log_2 n\rceil$ bits of $H(m)$ (for \secp{} all
7 +256 bits; $z$ may exceed $n$ and is reduced). With private key $d\in[1,n)$ and public key $P=[d]G$, signing picks a
8 +nonce $k\in[1,n)$, computes
9 +\begin{equation}
10 + R=[k]G,\qquad r = x(R)\bmod n,\qquad s = k^{-1}(z+rd)\bmod n ,
11 + \label{eq:ecdsa-sign}
12 +\end{equation}
13 +and outputs $(r,s)$ with $r,s\ne0$. Verification computes $w=s^{-1}$, $u_1=zw$, $u_2=rw$, $X=[u_1]G+[u_2]P$ and
14 +accepts iff $x(X)\bmod n=r$ \citep{nist2023fips186,certicom2009sec1}. Correctness: $X=[s^{-1}(z+rd)]G=[k]G=R$.
15 +Bitcoin additionally requires \emph{low-$s$} ($s\le n/2$, BIP~62/146) because $(r,n-s)$ is also valid: the verification
16 +point becomes $-X$, whose $x$-coordinate is unchanged (check~S5; formalised in Section~\ref{sec:formal}).
17 +
18 +\paragraph{Where the security lives.} The nonce $k$ must be uniform and secret: $k$ known gives $d=(sk-z)r^{-1}$;
19 +$k$ reused across two messages gives $k=(z_1-z_2)/(s_1-s_2)$ and then $d$ (our reference implements this textbook
20 +recovery on synthetic keys, \code{recover\_private\_key\_from\_nonce\_reuse}); and a few bits of bias in many nonces
21 +suffice for lattice recovery \citep{howgravegraham2001lattice,breitner2019biased}. RFC~6979 \citep{pornin2013rfc6979}
22 +derives $k$ deterministically by HMAC-DRBG from $(d,H(m))$, which removes the randomness requirement at signing time
23 +(the nonce is still secret because $d$ is). Timing and cache side channels in the scalar multiplication by $k$ are
24 +the practical threat \citep{kocher1996timing,brumley2011remote,aldaya2020ladderleak,jancar2020minerva}; constant-time
25 +scalar multiplication, as in \libsecp{} and the \kk{} crate, is the remedy and costs a measurable constant
26 +(Table~\ref{tab:sig-impl}).
27 +
28 +\paragraph{Proof structure.} ECDSA has no reduction to the DLP in the random-oracle model alone: the ``conversion
29 +function'' $R\mapsto x(R)\bmod n$ is not a random oracle and its algebraic structure (it is even, $x(R)=x(-R)$, which is
30 +the source of malleability) obstructs the forking-lemma argument. Known results prove security in the generic
31 +group model \citep{brown2005generic}, under the assumption that the conversion function behaves as a bijective random
32 +oracle \citep{fersch2016provable}, or for weakened unforgeability notions \citep{fersch2017one}; \citet{groth2022security}
33 +give a modern generic-group treatment that covers key derivation and presignatures. \citet{vaudenay2003security}
34 +surveys the early history. The contrast with Schnorr is structural, not quantitative (research question~B1).
35 +
36 +\subsection{BIP340 Schnorr}
37 +
38 +BIP340 \citep{wuille2020bip340} fixes the Schnorr scheme \citep{schnorr1991efficient} with three design decisions.
39 +\emph{X-only public keys}: $P$ is encoded by $x(P)$ alone and implicitly has even $y$ (the signer negates $d$ if
40 +needed), saving a byte and making keys compatible with Taproot tweaking \citep{wuille2020bip341}. \emph{Tagged hashes}:
41 +$H_{\text{tag}}(m)=\mathrm{SHA256}(\mathrm{SHA256}(\text{tag})\,\|\,\mathrm{SHA256}(\text{tag})\,\|\,m)$ for the
42 +tags \code{BIP0340/aux}, \code{BIP0340/nonce}, \code{BIP0340/challenge}, which separates domains at no cost since the
43 +64-byte prefix fills exactly one compression block that can be precomputed. \emph{Key-prefixed challenge}:
44 +$e = H_{\text{challenge}}(x(R)\,\|\,x(P)\,\|\,m)\bmod n$, which prevents related-key attacks and makes the scheme
45 +non-malleable. Signing: $k_0=H_{\text{nonce}}(t\,\|\,x(P)\,\|\,m)\bmod n$ with $t = d\oplus H_{\text{aux}}(\text{aux})$,
46 +$R=[k_0]G$, $k=k_0$ or $n-k_0$ so that $y(R)$ is even, $s=k+ed\bmod n$, signature $(x(R),s)$ in 64 bytes. Verification:
47 +lift $x(P)$ and check
48 +\begin{equation}
49 + [s]G = R + [e]P \quad\text{with } y(R) \text{ even and } x(R) \text{ as signed.}
50 + \label{eq:schnorr-verify}
51 +\end{equation}
52 +Correctness is the one-line identity $s=k+ed\Rightarrow [s]G=[k]G+[e][d]G$ (check~S6, formalised in
53 +Section~\ref{sec:formal}). Our pure-Python implementation passes all 19 official test vectors, including the negative
54 +vectors for $x\ge p$, $s\ge n$, odd $y(R)$ and wrong $R$; the Rust benchmark harness checks the same vectors with
55 +both \libsecp{} and \kk{}, and 1000 signatures produced by each library are byte-identical and verify under the other
56 +and under the Python reference (hypothesis~\Hname{H23}).
57 +
58 +\paragraph{Proof structure.} Schnorr signatures are EUF-CMA secure under the discrete-logarithm assumption in the
59 +random-oracle model via the forking lemma \citep{pointcheval2000security}; the reduction loses a factor of the number
60 +of hash queries, and \citet{seurin2012exact} shows this loss is essentially unavoidable for forking-based proofs and
61 +that the random oracle cannot be dispensed with for a natural class of reductions. The linear structure $s=k+ed$ is
62 +also what makes batch verification and multi-signatures \citep{bellare2006multi,maxwell2019simple,nick2021musig2}
63 +possible; the same linearity is what ECDSA's $k^{-1}$ destroys.
64 +
65 +\subsection{Batch verification and multi-scalar multiplication}
66 +
67 +Given $b$ signatures $(R_i,s_i)$ on messages $m_i$ under keys $P_i$, choose random weights $a_i$ ($a_1=1$, 128 bits
68 +otherwise) and check
69 +\begin{equation}
70 + \Big[\sum_i a_i s_i\Big]G = \sum_i [a_i]R_i + \sum_i [a_i e_i]P_i .
71 + \label{eq:batch}
72 +\end{equation}
73 +If any signature is invalid the check fails except with probability $2^{-128}$ \citep{naccache1994can,bellare1998fast,bernstein2012high}.
74 +The right-hand side is a multi-scalar multiplication with $2b$ terms, computed by Straus interleaving
75 +(cost $\approx 256$ doublings plus $2b\cdot 256/(w+1)$ additions with wNAF windows) or Pippenger's bucket method
76 +(cost $\approx 2b\cdot\lceil 256/c\rceil$ additions plus $2^c$ buckets per window, asymptotically $O(b\cdot 256/\log b)$)
77 +\citep{straus1964addition,pippenger1980evaluation,moller2001algorithms}. Batch verification does not change consensus:
78 +it accepts exactly the sets of signatures that individual verification accepts (with the stated negligible error).
79 +Hypothesis~\Hname{H13} measures the gain with our own instrumented Straus and Pippenger implementations on \kk{}
80 +arithmetic (Section~\ref{sec:results}); the answer to~B2 is a saturating constant-factor speed-up of $3.1\times$
81 +over single verification in the same arithmetic, bounded by the non-MSM work (square root for $R_i$, tagged hash).
82 +
83 +\subsection{What can be simplified and what cannot (B3, B4)}
84 +
85 +The algebraic identities behind the implementations---$x(-P)=x(P)$, $(\beta x,y)=[\lambda](x,y)$, Shamir's trick,
86 +batch verification by random linear combination---are all consequences of the $\Z$-module structure of
87 +$\langle G\rangle$ and of the shape of the curve equation; the symbolic checks of Section~\ref{sec:framework} and the
88 +Lean statements of Section~\ref{sec:formal} confirm them but find nothing beyond them. We looked specifically for a
89 +verification shortcut that avoids one of the two scalar multiplications in~\eqref{eq:schnorr-verify}: none exists
90 +without changing the predicate, because any procedure that accepts exactly the solutions of $[s]G-[e]P=R$ must
91 +evaluate a group element that depends on both $s$ and $e$, and in the generic-group model this needs $\Omega(\log n)$
92 +group operations for each. The practical levers are the ones in Table~\ref{tab:sig-impl}: fixed-base tables for $G$,
93 +GLV for $P$, Jacobian coordinates with a single final inversion, and batching.
added paper/sections/07_sha256.tex +72 −0
@@ -0,0 +1,72 @@
1 +\section{SHA-256: Mathematical Structure and Reduced-Round Analysis}
2 +\label{sec:sha}
3 +
4 +\subsection{Construction}
5 +
6 +SHA-256 \citep{nist2015fips180} is a Merkle--Damg\aa rd iteration \citep{merkle1989one,damgard1989design} of a
7 +compression function $f:\{0,1\}^{256}\times\{0,1\}^{512}\to\{0,1\}^{256}$. The message is padded with a 1-bit, zeros and
8 +the 64-bit length to a multiple of 512 bits; the chaining value starts at the fixed IV $H_0$ (fractional parts of
9 +$\sqrt{2},\dots,\sqrt{19}$); and $H_i = f(H_{i-1}, M_i)$ with $f(H,M)=H\boxplus\mathrm{Steps}_{64}(H,M)$, the
10 +``feed-forward'' Davies--Meyer construction that makes $f$ non-invertible. Inside, 32-bit words are combined with
11 +modular addition $\boxplus$, XOR, AND, rotations $\rotr$ and shifts $\shr$ through
12 +\begin{equation}
13 +\begin{aligned}
14 + \SigZ(x) &= \rotr^{2}(x)\oplus\rotr^{13}(x)\oplus\rotr^{22}(x), & \SigO(x) &= \rotr^{6}(x)\oplus\rotr^{11}(x)\oplus\rotr^{25}(x),\\
15 + \sigz(x) &= \rotr^{7}(x)\oplus\rotr^{18}(x)\oplus\shr^{3}(x), & \sigo(x) &= \rotr^{17}(x)\oplus\rotr^{19}(x)\oplus\shr^{10}(x),\\
16 + \Ch(x,y,z) &= (x\wedge y)\oplus(\neg x\wedge z), & \Maj(x,y,z) &= (x\wedge y)\oplus(x\wedge z)\oplus(y\wedge z).
17 +\end{aligned}
18 +\label{eq:shafuncs}
19 +\end{equation}
20 +The message schedule expands $M=W_0\|\cdots\|W_{15}$ by $W_t=\sigo(W_{t-2})\boxplus W_{t-7}\boxplus\sigz(W_{t-15})\boxplus W_{t-16}$
21 +for $16\le t<64$, and step $t$ updates the state $(a,b,c,d,e,f,g,h)$ as in Figure~\ref{fig:sha-arch}:
22 +$T_1 = h\boxplus\SigO(e)\boxplus\Ch(e,f,g)\boxplus K_t\boxplus W_t$, $T_2=\SigZ(a)\boxplus\Maj(a,b,c)$, then
23 +$(a,b,c,d,e,f,g,h)\leftarrow(T_1\boxplus T_2,\,a,\,b,\,c,\,d\boxplus T_1,\,e,\,f,\,g)$.
24 +
25 +\begin{figure}[htbp]
26 + \centering
27 + \resizebox{\textwidth}{!}{\input{figures/fig_sha256_architecture}}
28 + \caption{One step of the SHA-256 compression function (exact data flow of FIPS 180-4; illustrative layout).
29 + Rotations and shifts are free in hardware and in CNF (they are variable renamings); the nonlinearity comes from
30 + $\Ch$, $\Maj$ and the carries of the modular additions.}
31 + \label{fig:sha-arch}
32 +\end{figure}
33 +
34 +\subsection{Elementary structure}
35 +
36 +Three facts, all machine-checked in Section~\ref{sec:formal}, organise the analysis. (i)~$\SigZ,\SigO,\sigz,\sigo$ are
37 +$\F_2$-linear bijections of $\{0,1\}^{32}$---including $\sigz$ and $\sigo$, whose logical shift makes each of their two
38 +summands singular but not their sum (we had conjectured the opposite; the machine-checked refutation is
39 +hypothesis~\Hname{H22}). (ii)~For fixed $(K_t,W_t)$ the step function is a bijection of the 256-bit state, with explicit inverse
40 +$h = T_1\ominus\SigO(e)\ominus\Ch(e,f,g)\ominus K_t\ominus W_t$ where $T_1=a_{t+1}\ominus T_2$; and the
41 +schedule recurrence can be solved for $W_{t-16}$. Hence, without the feed-forward, the 64-step map is invertible
42 +for any chosen message: pseudo-preimages of the compression function (choose the IV) are free, and ``free-start''
43 +collisions are a different, much easier problem than collisions of the hash (hypothesis~\Hname{H20}). (iii)~The
44 +step-$t$ nonlinearity is weak: $\Ch$ and $\Maj$ are bitwise (no inter-bit mixing), so all diffusion across bit
45 +positions comes from the $\Sigma$ functions and from carries.
46 +
47 +\subsection{Generic bounds and the meaning of reduced-step results}
48 +
49 +For an ideal 256-bit hash a preimage costs $\approx2^{256}$ evaluations, a collision $\approx 2^{128}$ by the birthday
50 +paradox, and these are the targets any attack must beat. Reduced-step analysis studies $\mathrm{Steps}_s$ for $s<64$ to
51 +quantify the security margin; a result on $s$ steps says nothing about $64$ steps unless it extends. The published
52 +state of the art (Table~\ref{tab:sha-lit}, Section~\ref{sec:lit}) is: practical collisions for 28 steps
53 +\citep{dobraunig2015analysis} and 31 steps \citep{li2024first}, with a theoretical 37-step collision announced in 2026 \citep{zhang2026collision}; semi-free-start collisions for 38--39 steps
54 +\citep{mendel2013improving,li2024new}; preimages for 43 steps \citep{aoki2009preimages} and 45 steps of the hash
55 +(52 of the compression function) with bicliques \citep{khovratovich2012bicliques}, all far above $2^{128}$ in cost.
56 +Differential attacks use \emph{local collisions}: the 9-step pattern of \citet{chabaud1998differential} adapted to
57 +SHA-256 by \citet{gilbert2003security} injects a difference $\delta$ into $W_t$ and cancels its propagation with
58 +corrections in $W_{t+1},\dots,W_{t+8}$; in the XOR-linearised model the cancellation is exact, and the real step
59 +function follows the pattern with a probability that depends on the carries, which dedicated tools raise by imposing
60 +conditions on message and state bits \citep{nikolic2008collisions,mendel2011finding}.
61 +
62 +\subsection{What we measure}
63 +
64 +Axis~C runs four families of experiments on the reference implementation (verified against \code{hashlib} for all
65 +message lengths): diffusion of single-bit differences through the steps (\Hname{H15}); statistical detectability of
66 +input--output correlations as a function of the step count (\Hname{H16}); SAT (CaDiCaL, Kissat) and SMT (Z3 bit-vector)
67 +preimage and collision searches on step-reduced compression functions with Tseitin encodings
68 +(\Hname{H17}, \Hname{H18}) \citep{mironov2006applications,nejati2017adaptive,alamgir2024sha256}; and the 9-step local
69 +collision in the linearised and the real model (\Hname{H19}). The questions~C1--C5 are answered in
70 +Section~\ref{sec:results}; we state in advance that nothing in these experiments bears on the security of the full
71 +64-step hash, and that our generic SAT encodings are expected to reach far fewer steps than the dedicated
72 +characteristic-search tools of the literature.
added paper/sections/08_discovery_framework.tex +71 −0
@@ -0,0 +1,71 @@
1 +\section{Autonomous Mathematical Discovery Framework}
2 +\label{sec:framework}
3 +
4 +The methodological object of this paper is a six-stage pipeline (Figure~\ref{fig:pipeline}) run by an autonomous
5 +agent (Claude Code with the Claude Fable~5.1 model) under the written mandate reproduced in
6 +\code{docs/MANDATE.md}. We describe the stages as implemented, the artefacts each produces, and the rules that keep
7 +the pipeline honest. The pipeline is deliberately simple; its value is measured in Section~\ref{sec:results} by what
8 +it rediscovered, what it rejected, and what it did not claim.
9 +
10 +\begin{figure}[htbp]
11 + \centering
12 + \resizebox{\textwidth}{!}{\input{figures/fig_pipeline}}
13 + \caption{The six-stage discovery pipeline (illustrative diagram of the implemented workflow; file names refer to
14 + the repository). Dashed arrows are the two feedback loops: counterexamples send a hypothesis back to symbolic
15 + analysis, and evaluation sends it back to generation for refinement or rejection.}
16 + \label{fig:pipeline}
17 +\end{figure}
18 +
19 +\paragraph{Stage 1: literature extraction.} A structured database (\code{literature/db.yaml}) records theorems,
20 +algorithms, assumptions, complexity bounds, experimental records, open problems and negative results, each with a
21 +source key that must exist in the verified BibTeX file. Verification means that the entry was confirmed against
22 +Crossref, IACR ePrint, a DOI, or the official standard; unconfirmed items are listed separately and never cited.
23 +
24 +\paragraph{Stage 2: hypothesis generation.} Each hypothesis is a file with sixteen mandatory fields (statement,
25 +motivation, assumptions, expected contribution, related work, possible counterexamples, verification strategy,
26 +computational budget, status, classification, evidence, verdict, \ldots), validated by \code{registry.py}, which
27 +also emits the discovery log and Table~\ref{tab:hypotheses}. Hypotheses were generated from three sources: the
28 +mandate's research questions, gaps noted in Stage~1, and the outputs of Stage~3's identity search.
29 +
30 +\paragraph{Stage 3: symbolic investigation.} Two tools. SymPy performs exact polynomial reductions modulo the curve
31 +equations (the six checks S1--S6 of \code{symbolic\_checks.py}: group-law closure, $\phi$ is an endomorphism, the
32 +$x$-only differential addition formula, Semaev's $S_3$, ECDSA malleability, Schnorr correctness). The
33 +\emph{identity search} enumerates 58\,064 candidate relations among twelve constants of \secp{}---binary quadratic
34 +forms $u^2+cuv+dv^2$ against targets $\{p,n,2p,3p,4p,\dots\}$, modular quadratics, linear relations in the GLV basis,
35 +and two negative-control spaces (cross-modulus relations between $\beta$ and $\lambda$; small linear relations
36 +involving the generator's coordinates)---and tests each with exact integer arithmetic. It found 22 identities and
37 +0 control hits; every identity is explained by $\beta,\lambda$ being cube roots of unity and by
38 +Proposition~\ref{prop:glv}, which the search itself prompted when it reported $t^2+3b_1^2=4p$ (hypotheses~\Hname{H02},
39 +\Hname{H07}).
40 +
41 +\paragraph{Stage 4: experimental evaluation.} Every experiment is a script that takes a seed and an output path and
42 +writes a JSON document with a provenance block (host, CPU, RAM, OS, library versions, command line, timestamp, elapsed
43 +time) through a single helper (\code{wp34.provenance.record}). Expensive computations use a Rust engine for the ECDLP
44 +and native SAT solvers for SHA-256, distributed over the MacLustr cluster (Section~\ref{sec:design}); the laptop runs
45 +reference checks and figures. A result that is not backed by a saved JSON file does not appear in the paper.
46 +
47 +\paragraph{Stage 5: formal verification.} Statements that are decidable at the bit level are proved in Lean~4 with
48 +\code{bv\_decide}, which compiles a bit-vector goal to SAT, calls CaDiCaL and checks the returned LRAT proof inside the
49 +kernel. Statements about abstract groups are proved against an explicit axiomatisation. Section~\ref{sec:formal}
50 +lists exactly what is machine-checked; everything else in the paper is an informal proof or a measurement, and
51 +conjectures are labelled as such.
52 +
53 +\paragraph{Stage 6: scientific evaluation.} Each hypothesis receives one of six labels---new verified result, known
54 +result independently reproduced, conditional result, empirical observation, rejected conjecture, unresolved
55 +direction---after a literature comparison. The rubric is applied conservatively: a result is ``new'' only if we found no
56 +prior statement of it and it is proved; a reproduction with sharper constants is still ``known''. The complete log is
57 +\code{research\_log/discovery\_log.jsonl}.
58 +
59 +\paragraph{Integrity rules.} Synthetic instances only; no real keys, addresses or blockchain data; no claim that a
60 +computation ran unless its output is saved; operation counts next to wall-clock; at least one negative control per
61 +experimental family; two independent implementations for every arithmetic component. Two development bugs caught
62 +by the last rule (a kangaroo reference whose retry ignored the seed, and a PARI output-parsing error) are logged as
63 +evidence that the rule does work (\Hname{H23}).
64 +
65 +\paragraph{What the framework is not.} It does not search a space of programs or proofs by itself in the sense of
66 +\citet{fawzi2022discovering}, \citet{romeraparedes2024mathematical} or \citet{trinh2024solving}: the hypotheses are
67 +written by the agent, the identity search is a bounded enumeration, and the design-space search of
68 +\Hname{H09} is a parameter sweep. Its contribution is the discipline---structured records, exact checks, provenance,
69 +formal verification, and an honest classifier---applied end to end by one autonomous agent on a problem where the
70 +temptation to over-claim is strong. We assess in Section~\ref{sec:results} whether this discipline produced anything a
71 +careful literature reading would not have.
added paper/sections/09_experimental_design.tex +82 −0
@@ -0,0 +1,82 @@
1 +\section{Experimental Design}
2 +\label{sec:design}
3 +
4 +\subsection{Environment}
5 +
6 +All computations ran on Apple-silicon machines of the author's laboratory cluster (Table~\ref{tab:env} in
7 +Appendix~\ref{app:env}): a laptop (Apple M5 Max, 18 cores, 48~GB) for development, reference checks, SymPy/PARI
8 +verification and figure generation; two Mac Studio nodes (Apple M3 Ultra, 28 cores, 96~GB) and one Mac Studio
9 +(Apple M2 Ultra, 24 cores, 64~GB) for the ECDLP studies, the SAT/SMT grids and the second hardware point of the
10 +signature benchmarks. No GPU was used. Toolchains: Python~3.12 with exact integer arithmetic (SymPy~1.14, NumPy~2.5,
11 +python-sat~1.9 with CaDiCaL bindings, Z3~4.15), native CaDiCaL and Kissat binaries, PARI/GP~2.19 for point counting,
12 +Rust~1.95/1.96 for the ECDLP engine and the signature harness (\kk{}~0.14, rust-secp256k1~0.33 binding \libsecp{}),
13 +Lean~4.35 for the formal layer. Every result file records the host, versions, seed and command line.
14 +
15 +\subsection{Baselines and validation}
16 +
17 +Before any novel configuration was tried, the standard algorithms were implemented twice and cross-validated
18 +(Hypothesis~\Hname{H23}): a pure-Python reference (\code{src/wp34}, affine coordinates, one inversion per group
19 +operation, explicit operation counters) and a Rust engine (\code{rho\_engine}: Montgomery arithmetic for $p<2^{62}$,
20 +simultaneous inversion across batched walks, ChaCha20-seeded multipliers, per-thread counters and CPU timers). The
21 +engine passes 8 Rust unit/integration tests (including $1\,052$ brute-forced small-curve instances) and 553 Python
22 +tests on a fixture of 60 prime-order curves of 20--32 bits, every answer being re-verified by the Python reference as
23 +$[d]G=Q$; each engine result additionally carries an \code{ok} flag set by an in-process re-verification. The signature
24 +harness passes the 19 BIP340 vectors with both libraries, $1\,000$ cross-library checks per scheme and 200 checks
25 +against the Python reference. SHA-256 is verified against \code{hashlib} for ten message lengths spanning the padding
26 +boundaries, and the CNF encoder against the reference for $r\in\{1,4,16,64\}$ steps.
27 +
28 +\subsection{Synthetic ECDLP instances}
29 +
30 +Instances are random prime-order curves $y^2=x^3+ax+b$ over random primes of the target bit length, counted with
31 +PARI/GP's \code{ellcard} (SEA) and retained only if $\#E(\F_p)$ is prime and $\ne p$; the $j=0$ family uses $a=0$ and
32 +$p\equiv 1\pmod 3$, with $\beta$ and $\lambda$ computed and checked ($\phi(G)=[\lambda]G$) before use. Targets are
33 +$Q=[d]G$ for uniformly random $d$. Five studies were run (Table~\ref{tab:ecdlp-config}); all jobs are single-threaded
34 +except the parallel-efficiency study, so that operation counts are deterministic given the seed. The
35 +distinguished-point density follows the engine default $2^{-(\lfloor\text{bits}/4\rfloor-2)}$ and each thread runs
36 +128 interleaved walks, which fixes the post-collision overhead at about $128\cdot 2^{\lfloor \text{bits}/4\rfloor-2}$
37 +extra iterations---negligible above 44 bits ($<3\%$) but visible at 20--28 bits, which is why pooled constants are
38 +computed on $\ge 44$ bits and empirical exponents are also fitted on $\ge 32$ bits only.
39 +
40 +\begin{table}[htbp]
41 + \centering\footnotesize
42 + \begin{threeparttable}
43 + \caption{ECDLP experimental configuration (seed 34; all instances verified).}
44 + \label{tab:ecdlp-config}
45 + \begin{tabular}{@{}llllp{5.6cm}@{}}
46 + \toprule
47 + Study & Curves & Sizes (bits) & Instances/size & Configurations \\
48 + \midrule
49 + scaling, generic & random $a,b$ & 20--56 step 4 & 60 & BSGS ($\le 48$); rho Floyd, Brent ($\le 48$); rho DP $r=20$, $r=128$; rho DP + negation ($r=128$); kangaroo on $[0,n)$ \\
50 + scaling, $j=0$ & $a=0$, $p\equiv 1\ (3)$ & 24--52 step 4 & 60 & rho DP $r=128$; + negation; + $\phi$-orbits; + both ($r=128$ and $r=20$) \\
51 + iteration-function sweep & random & 36, 40 & 40 & $r\in\{2,3,4,8,16,20,32,64,128\}$ $\times$ mixed walk $\in\{$none$,2,4\}$ \\
52 + parallel efficiency & random & 56 & 12 & rho DP + negation, threads $\in\{1,2,4,8,16,28\}$ (M3 Ultra, 28 cores) \\
53 + time--memory & random & 36 (BSGS), 40 (rho) & 20 & BSGS memory $m\in\{2^{10},\dots,2^{20}\}$; rho DP density $2^{-2},\dots,2^{-14}$ \\
54 + preprocessing (Python) & random & 22, 26 & 60 & $T\in\{2^4,\dots,2^{12}\}$ stored distinguished points \\
55 + \bottomrule
56 + \end{tabular}
57 + \begin{tablenotes}\footnotesize
58 + \item Nodes: generic scaling, sweep and parallel study on M3U96a (Apple M3 Ultra); $j=0$ scaling and time--memory on M2U64 (Apple M2 Ultra); preprocessing on the laptop. A 20-instance pilot of the two scaling studies (kept in \code{results/synthetic\_ecdlp/pilot20/}) was used to size the final runs.
59 + \end{tablenotes}
60 + \end{threeparttable}
61 +\end{table}
62 +
63 +\subsection{Measurement and statistics}
64 +
65 +For every run we record group additions and doublings (and their sum, the \emph{group operations}), field inversions
66 +and multiplications, iterations of the walk, distinguished points stored and their bytes, fruitless cycles detected,
67 +wall-clock time, CPU time summed over threads and peak resident memory. Means are reported with bootstrap 95\%
68 +confidence intervals (2\,000 resamples); speed-ups between configurations on the same instances use paired
69 +resampling. Empirical exponents come from least squares on $\log T = \log a + \alpha\log n\ (+\beta\log\log_2 n)$ with
70 +bootstrap intervals over the size-level means. Negative controls: an $r=2$ walk (far from a random mapping) in the
71 +sweep; a random oracle in the SHA-256 diffusion and bias tests; a batch containing one invalid signature in the batch
72 +verification; and the control spaces Q4/Q5 of the identity search.
73 +
74 +\subsection{Signature and SHA-256 protocols}
75 +
76 +Signature benchmarks use ChaCha20-seeded synthetic keys, prehashed 32-byte messages, single-threaded timing with
77 +medians and interquartile ranges over 7 repetitions of 2\,000 operations, and our own instrumented Straus and
78 +Pippenger multi-scalar multiplications for batch verification (operation counts recorded). The SHA-256 experiments
79 +use the reference implementation with $r$-step compression, seeded random messages, $2\,000$ samples per diffusion
80 +configuration and $20\,000$ per bias configuration, Tseitin CNF with ripple-carry adders, and solver time limits of
81 +300~s (preimage) and 600~s (collision) with three seeds per cell; every satisfying assignment is re-hashed with the
82 +reference implementation before being counted.
added paper/sections/10_results.tex +244 −0
@@ -0,0 +1,244 @@
1 +\section{Computational Results}
2 +\label{sec:results}
3 +
4 +\subsection{Axis A: algebraic structure (H01--H07)}
5 +
6 +The verification of the \secp{} parameters, the closed form of the GLV constants, the orbit counterexample and the
7 +index-calculus probe were presented with the theory in Section~\ref{sec:secp}; Table~\ref{tab:opcount} gives the
8 +operation counts behind hypothesis~\Hname{H03}, and Table~\ref{tab:ic} the index-calculus measurements behind
9 +\Hname{H06}. The identity search tested \idTested{} candidate relations and found \idFound{}, all explained
10 +(\idUnexplained{} unexplained; negative controls Q4~$=\idQfour$, Q5~$=\idQfive$ hits, against an expected chance count
11 +of $10^{-73}$); the orbit-compatibility test failed for \orbitFails{} of \orbitTrials{} random pairs.
12 +
13 +\begin{table}[htbp]
14 + \centering\footnotesize
15 + \caption{Operation counts for one scalar multiplication on \secp{} (affine model, 200 random 256-bit scalars, mean $\pm$ sd). GLV sub-scalars never exceeded \glvKoneMax{} bits in \glvSamples{} samples.}
16 + \label{tab:opcount}
17 + \input{tables/scalar_mult_opcount}
18 +\end{table}
19 +
20 +\begin{table}[htbp]
21 + \centering\footnotesize
22 + \caption{Index-calculus probe on prime-field curves: decomposition of a random point as a sum of two factor-base points, $F=\{P: x(P)<p^{e}\}$ ($e=0.5$ or $0.6$). ``Oracle relations'' is the cost of collecting $|F|$ relations with a free decomposition oracle; ``Lin.\ alg.'' is $|F|^2$.}
23 + \label{tab:ic}
24 + \input{tables/index_calculus_probe}
25 +\end{table}
26 +
27 +\subsection{Axis A/B: ECDLP baselines and scaling (H08)}
28 +
29 +Every one of the $3\,840$ generic and $2\,400$ $j=0$ instances was solved and verified (\scalingAllOk{} verified).
30 +Figure~\ref{fig:scaling} shows group operations and wall-clock time against $n$ on log--log axes, and
31 +Figure~\ref{fig:constants} the normalised cost $T/\sqrt n$; Table~\ref{tab:scaling} lists selected sizes and
32 +Table~\ref{tab:fits} the fitted exponents.
33 +
34 +\begin{figure}[htbp]
35 + \centering
36 + \includegraphics[width=\textwidth]{ecdlp_scaling.pdf}
37 + \caption{Empirical scaling of the generic ECDLP algorithms on random prime-order curves (60 instances per size,
38 + single thread, Apple M3 Ultra). Left: group operations with bootstrap 95\% intervals and the theoretical lines
39 + $\sqrt{\pi n/2}$ and $2\sqrt n$; right: wall-clock seconds. Generated by \code{analyze\_ecdlp.py}.}
40 + \label{fig:scaling}
41 +\end{figure}
42 +
43 +\begin{figure}[htbp]
44 + \centering
45 + \includegraphics[width=0.85\textwidth]{ecdlp_constants.pdf}
46 + \caption{Normalised cost $T(n)/\sqrt n$ by size. The small-size excess of the distinguished-point variants is the
47 + post-collision overhead of 128 interleaved walks; above 44 bits the constants settle at their theoretical values.}
48 + \label{fig:constants}
49 +\end{figure}
50 +
51 +\begin{table}[htbp]
52 + \centering\scriptsize
53 + \caption{ECDLP baselines at selected sizes (mean group operations, bootstrap 95\% CI, normalised constant, theoretical constant, wall-clock).}
54 + \label{tab:scaling}
55 + \input{tables/ecdlp_scaling}
56 +\end{table}
57 +
58 +\begin{table}[htbp]
59 + \centering\footnotesize
60 + \caption{Empirical complexity exponents $\hat\alpha$ in $T(n)=a\,n^{\alpha}$ (least squares on size-level means, bootstrap 95\% CI). Fits on all sizes include the distinguished-point overhead at 20--28 bits; fits on $\ge 32$ bits and on iterations remove it.}
61 + \label{tab:fits}
62 + \input{tables/ecdlp_fits}
63 +\end{table}
64 +
65 +\paragraph{Exponents.} Restricted to $\ge 32$ bits, every algorithm has $\hat\alpha\in[\alphaGMin,\alphaGMax]$ with
66 +confidence intervals containing or adjacent to $0.5$; on iterations the rho variants give $\hat\alpha$ between
67 +$0.49$ and $0.50$. The log-factor model $a\,n^\alpha(\log n)^\beta$ does not improve the fit ($R^2>0.99$ already) and
68 +returns $\beta$ indistinguishable from~0. There is no evidence of any deviation from square-root scaling in this
69 +range, as expected.
70 +
71 +\paragraph{Constants.} Pooled over the \rhoConstPooledN{} rho runs at $\ge\pooledMinBits$ bits, the number of
72 +iterations is $\rhoConstPooled\,\sqrt n$ with 95\% CI $[\rhoConstPooledLo,\rhoConstPooledHi]$---the random-mapping
73 +prediction is $\sqrt{\pi/2}=1.2533$, which the $r$-adding walk with $r\in\{20,128\}$ reproduces to three digits. With the
74 +negation map the constant is $\negConstPooled$ $[\negConstPooledLo,\negConstPooledHi]$ against the prediction
75 +$\sqrt{\pi/4}=0.8862$: a paired speed-up of $\negSpeedupPooled$ $[\negSpeedupPooledLo,\negSpeedupPooledHi]$ versus the
76 +ideal $\sqrt2=1.414$, the shortfall being the residual fruitless-cycle handling and the look-ahead rejections
77 +(Section~\ref{sec:dlog}). BSGS costs $\bsgsConstPooled\,\sqrt n$ $[\bsgsConstPooledLo,\bsgsConstPooledHi]$ group
78 +operations (the engine's $x$-keyed table resolves $\pm j$, so the giant phase costs $n/(2m)$ and the total is
79 +$1.5\sqrt n$ rather than $2\sqrt n$); the kangaroo on the full interval costs $\kangConstPooled\,\sqrt n$
80 +$[\kangConstPooledLo,\kangConstPooledHi]$, close to the $2\sqrt w$ prediction. Floyd's cycle detection costs
81 +$\floydConstBig\,\sqrt n$ (three evaluations per step) and Brent's $\brentConstBig\,\sqrt n$ at 48 bits, both as expected.
82 +
83 +\subsection{Axis A: automorphism classes on $j=0$ curves (H04)}
84 +
85 +Table~\ref{tab:j0} and Figure~\ref{fig:endo} give the result of hypothesis~\Hname{H04} on \jzeroPooledN{} paired
86 +instances at $\ge\jzeroPooledMinBits$ bits (Apple M2 Ultra). In \emph{iterations}, the order-6 orbits deliver a
87 +speed-up of $\jzeroNegAutSpeedup$ $[\jzeroNegAutSpeedupLo,\jzeroNegAutSpeedupHi]$ with $r=128$ and
88 +$\jzeroNegAutRtwentySpeedup$ $[\jzeroNegAutRtwentySpeedupLo,\jzeroNegAutRtwentySpeedupHi]$ with $r=20$ against the ideal
89 +$\sqrt6=2.449$; the normalised constant with $r=20$ is $\jzeroNegAutRtwentyConst\,\sqrt n$ against the prediction
90 +$\sqrt{\pi/12}=0.5117$. The negation map alone and the $\phi$-orbits alone give $\jzeroNegSpeedup$ and
91 +$\jzeroAutSpeedup$ against $\sqrt2$ and $\sqrt3$. Fruitless cycles are rare at these parameters
92 +(\jzeroFruitlessPerK{} per 1000 iterations with $r=128$, \jzeroFruitlessPerKRtwenty{} with $r=20$) because the
93 +look-ahead removes most 2-cycles before they form. The theory is therefore reproduced in the quantity it speaks
94 +about.
95 +
96 +In \emph{wall-clock time}, however, the picture is different: canonicalisation raises the cost of one iteration from
97 +\jzeroPlainNsIter{}~ns (plain) to \jzeroNegNsIter{}~ns (negation), \jzeroAutNsIter{}~ns ($\phi$-orbits) and
98 +\jzeroNegAutNsIter{}~ns (both) in our engine, so the measured wall-clock speed-ups are $\jzeroNegWall$,
99 +$\jzeroAutWall$ and $\jzeroNegAutWall$ respectively---the order-6 trick roughly breaks even and the negation map alone
100 +\emph{loses} time. The reason is implementation-specific and instructive: our canonical representative literally
101 +computes $\min\{x,\beta x,\beta^2x\}$ and $\min\{y,p-y\}$ with two field multiplications and several Montgomery
102 +conversions per step, whereas the record-setting implementations fold the comparison into the batched inversion
103 +and use cheaper sign conventions \citep{bos2010negation,bernstein2011correct,bos2012solving}. The lesson for the
104 +mandate's question A2 is that equivalence classes reduce \emph{iterations} by exactly the predicted factor, and that
105 +turning this into a time saving is an engineering problem whose outcome depends on the per-step cost of
106 +canonicalisation relative to a group addition (here about $0.6$--$1.1$ additions).
107 +
108 +\begin{figure}[htbp]
109 + \centering
110 + \includegraphics[width=\textwidth]{ecdlp_endomorphism.pdf}
111 + \caption{Endomorphism benchmarking on synthetic $j=0$ curves. Left: paired iteration speed-up over plain rho by
112 + size, with the ideal $\sqrt2$, $\sqrt3$, $\sqrt6$ (dashed); right: fruitless cycles detected per 1000 iterations.}
113 + \label{fig:endo}
114 +\end{figure}
115 +
116 +\begin{table}[htbp]
117 + \centering\footnotesize
118 + \caption{Equivalence classes on $j=0$ curves: iterations, paired iteration speed-up, cost per iteration and wall-clock speed-up (pooled over sizes $\ge \jzeroPooledMinBits$ bits for iterations, $\ge 40$ bits for time; Apple M2 Ultra, one thread).}
119 + \label{tab:j0}
120 + \input{tables/ecdlp_j0}
121 +\end{table}
122 +
123 +\subsection{Axis B: automated search over iteration functions (H09)}
124 +
125 +The sweep over $r$ and mixed walks (\sweepConfigs{} configurations $\times$ \sweepInstances{} instances;
126 +Figure~\ref{fig:sweep}, Table~\ref{tab:sweep}) reproduces Teske's findings: $r=2$ and $r=3$ walks need
127 +$\sweepRtwo\,\sqrt n$ and $\sweepRthree\,\sqrt n$ iterations (the negative control behaves as a bad random mapping
128 +should), $r\ge 8$ walks cluster around the random-mapping constant, and mixed walks make no measurable difference
129 +at these sizes. The best cell (\sweepBestCfg{}, $\sweepBestConst$ $[\sweepBestLo,\sweepBestHi]$) is not
130 +distinguishable from the random-mapping value given the width of the intervals, and no configuration is
131 +significantly below it. This is the expected negative result: an automated search confined to generic iteration
132 +functions can at best recover the random-mapping constant, which Shoup's bound makes the floor up to the class-size
133 +factor. We record it as a rejected shortcut, with the positive by-product that the search ranks the designs
134 +correctly and quantifies the penalty for small~$r$.
135 +
136 +\begin{figure}[htbp]
137 + \centering
138 + \includegraphics[width=0.85\textwidth]{ecdlp_sweep_r.pdf}
139 + \caption{Iteration-function design sweep: iterations$/\sqrt n$ against the number of partitions $r$ for plain and
140 + mixed $r$-adding walks at 36 and 40 bits (40 instances per cell, bootstrap 95\% CI). The dashed line is the
141 + random-mapping constant $\sqrt{\pi/2}$.}
142 + \label{fig:sweep}
143 +\end{figure}
144 +
145 +\begin{table}[htbp]
146 + \centering\footnotesize
147 + \caption{Iteration-function sweep (iterations$/\sqrt n$, mean of 40 instances per cell).}
148 + \label{tab:sweep}
149 + \input{tables/ecdlp_sweep_r}
150 +\end{table}
151 +
152 +\subsection{Axis B: parallel efficiency and time--memory trade-offs (H10, H11)}
153 +
154 +Distinguished-point collision search on 56-bit instances scales to \parSpeedupSixteen$\times$ on 16 threads
155 +(efficiency \parEffSixteen) and \parSpeedupMax$\times$ on \parMaxThreads{} threads (efficiency \parEffMax) of the
156 +Apple M3 Ultra, with total group operations within a factor \parOpsRatioMax{} of the single-thread count
157 +(Figure~\ref{fig:parallel}, Table~\ref{tab:parallel}); the efficiency loss at 28 threads reflects the mix of
158 +performance and efficiency cores and the shared table, not an algorithmic limit. The time--memory study
159 +(Figure~\ref{fig:tmto}, Table~\ref{tab:tmto}) confirms the deterministic BSGS trade-off $m+n/(2m)$ within
160 +\tmtoBsgsMaxDev\% over memory sizes from $\tmtoBsgsMinM$ to $\tmtoBsgsMaxM$ entries, and shows the two-sided
161 +trade-off of distinguished-point rho: dense distinguished points (density $2^{-2}$) store many points and pay a
162 +restart cost per point, sparse ones (density $2^{-14}$) pay the post-collision overhead, and the optimum at 40 bits
163 +lies between densities $2^{-8}$ and $2^{-10}$, where the cost is within 5\% of $\sqrt{\pi n/2}$ with one to four thousand stored points.
164 +
165 +\begin{figure}[htbp]
166 + \centering
167 + \includegraphics[width=0.95\textwidth]{ecdlp_parallel.pdf}
168 + \caption{Parallel collision search (56-bit instances, rho with negation map, Apple M3 Ultra, 12 instances per point): throughput speed-up and total work relative to one thread.}
169 + \label{fig:parallel}
170 +\end{figure}
171 +
172 +\begin{figure}[htbp]
173 + \centering
174 + \includegraphics[width=\textwidth]{ecdlp_tmto.pdf}
175 + \caption{Time--memory trade-off. Left: BSGS at 36 bits against the memory $m$ (dashed: $m+n/(2m)$). Right: distinguished-point rho at 40 bits against the number of stored points (labels: DP density), with bootstrap 95\% intervals; dashed: $\sqrt{\pi n/2}$.}
176 + \label{fig:tmto}
177 +\end{figure}
178 +
179 +\begin{table}[htbp]
180 + \centering\footnotesize
181 + \caption{Parallel efficiency of distinguished-point rho (56 bits, negation map, Apple M3 Ultra).}
182 + \label{tab:parallel}
183 + \input{tables/ecdlp_parallel}
184 +\end{table}
185 +
186 +\begin{table}[htbp]
187 + \centering\footnotesize
188 + \caption{Time--memory trade-off measurements (means over 20 instances).}
189 + \label{tab:tmto}
190 + \input{tables/ecdlp_tmto}
191 +\end{table}
192 +
193 +\subsection{Axis B: preprocessing (H12)}
194 +
195 +Table~\ref{tab:preproc} reports the Python reference experiment on 22- and 26-bit groups (\ppInstances{} instances per
196 +cell). The online cost is $\ppRatioMean\,\sqrt{n/T}$ on average (range \ppRatioMin--\ppRatioMax{} across $T$ from
197 +$2^4$ to $2^{12}$), so the per-instance speed-up over plain rho grows as $\sqrt T$ and reaches \ppSpeedupMax$\times$ at
198 +$T=2^{12}$---at the price of an offline phase of about $\sqrt{nT}$ operations. The constant $\approx 1.5$ rather
199 +than $1$ comes from the walk-length distribution (a walk from $Q$ must travel to a distinguished point \emph{and}
200 +that point must have been stored) and matches the heuristic analysis of \citet{bernstein2013nonuniform}. The
201 +trade-off is exactly the $ST^2=\tilde\Omega(n)$ curve of \citet{corrigangibbs2018discrete} and offers nothing below
202 +$\sqrt n$ in total work.
203 +
204 +\begin{table}[htbp]
205 + \centering\footnotesize
206 + \caption{Discrete logarithms with preprocessing (Python reference, 20-partition walk independent of the target).}
207 + \label{tab:preproc}
208 + \input{tables/preprocessing}
209 +\end{table}
210 +
211 +\subsection{Axis B: signature benchmarks (H13, H14)}
212 +
213 +Table~\ref{tab:sig-impl} and Figure~\ref{fig:sig} summarise the signature measurements. Three results stand out.
214 +(i)~Implementation choice spans three orders of magnitude: the pure-Python reference verifies a BIP340 signature
215 +in \sigPurePythonSchnorrVerifyMs~ms, \libsecp{} in \sigSchnorrVerifyLibsecpUs~$\mu$s
216 +(\sigPurePythonVsLibsecp$\times$), with \kk{} at \sigSchnorrVerifyKkUs~$\mu$s and coincurve (\libsecp{}
217 +through Python) essentially at the C speed. (ii)~ECDSA and Schnorr verification cost the same in \libsecp{} within
218 +5\%, and signing differs between libraries more than between schemes: \kk{} signs faster (table-based
219 +fixed-base multiplication) while \libsecp{} verifies $1.9\times$ faster (variable-base multiplication with GLV and
220 +its optimised field arithmetic). (iii)~Batch verification with our Pippenger multi-scalar multiplication reaches
221 +\batchBestUs~$\mu$s per signature at batch size \batchBestSize, a speed-up of $\batchSpeedupK$
222 +$[\batchSpeedupKLo,\batchSpeedupKHi]$ over single verification in the same arithmetic and $\batchSpeedupLib\times$ over
223 +\libsecp{}'s single verification; Straus interleaving plateaus near $2\times$ from batch size 32, and Pippenger
224 +overtakes it only beyond about 256 signatures. About $3~\mu$s per signature of non-MSM work (the square root in
225 +\code{lift\_x} and the tagged hash) bounds the attainable gain; the batch with one corrupted signature was rejected by
226 +every back-end. The same curves on the Apple M3 Ultra are $1.3\times$ slower with identical shape. All of this is
227 +known in kind \citep{bernstein2012high,wuille2020bip340}; the contribution is the measured, operation-counted
228 +curve for \secp{} and the identification of the saturation mechanism.
229 +
230 +\begin{table}[htbp]
231 + \centering\footnotesize
232 + \caption{Signature implementations: median time per operation in microseconds (single thread, 7 repetitions of 2\,000 operations; laptop Apple M5 Max unless stated).}
233 + \label{tab:sig-impl}
234 + \input{tables/sig_implementations}
235 +\end{table}
236 +
237 +\begin{figure}[htbp]
238 + \centering
239 + \includegraphics[width=\textwidth]{sig_batch_verification.pdf}
240 + \caption{BIP340 batch verification: time per signature against batch size for Straus, Pippenger and the \kk{} linear-combination routines on two hosts, with the single-verification reference lines, and the speed-up with bootstrap 95\% intervals. Generated by \code{figures/make\_sig\_figures.py}.}
241 + \label{fig:sig}
242 +\end{figure}
243 +
244 +\input{sections/10b_results_sha}
added paper/sections/10b_results_sha.tex +1 −0
@@ -0,0 +1 @@
1 +% SHA-256 results — written once the SAT/diffusion runs are in
added paper/sections/11_formal_negative.tex +38 −0
@@ -0,0 +1,38 @@
1 +\section{Formal Results and Negative Findings}
2 +\label{sec:formal}
3 +
4 +\input{sections/11a_formal}
5 +
6 +\subsection{Negative findings}
7 +
8 +The mandate counts a rigorously documented failure as a result (Outcome~D). Table~\ref{tab:negative} collects the
9 +negative findings of this study with the form of evidence behind each.
10 +
11 +\begin{table}[htbp]
12 + \centering\footnotesize
13 + \begin{threeparttable}
14 + \caption{Negative findings: candidate shortcuts and why they fail.}
15 + \label{tab:negative}
16 + \begin{tabular}{@{}p{4.6cm}p{5.4cm}p{4.4cm}@{}}
17 + \toprule
18 + Candidate & Why it fails & Evidence \\
19 + \midrule
20 + A further efficiently computable endomorphism of \secp{} & $\End(E)=\Z[\omega]$ is the maximal order; every endomorphism is $u+v\phi$ & Proposition~\ref{prop:end}; exact check $t^2-4p=-3f^2$ \\
21 + A group structure on the automorphism orbits (DLP of size $n/6$) & the projection would be injective; orbits are not a congruence & Proposition~\ref{prop:noquotient}; $1500/1500$ counterexamples \\
22 + Index calculus with a factor base $x(P)<p^{1/2}$ over $\F_p$ & decomposition costs $\lvert F\rvert$ per trial without an oracle; with an oracle, relations cost $\ge 2.4\sqrt n$ and linear algebra $\lvert F\rvert^2$ & Table~\ref{tab:ic}; Semaev $S_3$ check S4 \\
23 + A transfer attack (MOV/Frey--R\"uck, anomalous, twist) & embedding degree 253-bit; $n\ne p$; twist cofactor a 220-bit prime & Table~\ref{tab:secp-params} \\
24 + A better generic iteration function for rho & no design in a 25-point space beats the random-mapping constant; Shoup's bound is the floor & Figure~\ref{fig:sweep}, Table~\ref{tab:sweep} \\
25 + Beating $\sqrt n$ in total work by preprocessing & online $1.5\sqrt{n/T}$ after $\sqrt{nT}$ offline; $ST^2=\tilde\Omega(n)$ & Table~\ref{tab:preproc} \\
26 + A verification shortcut avoiding one scalar multiplication & any predicate equivalent to $[s]G=R+[e]P$ needs a group element depending on both $s$ and $e$ & Section~\ref{sec:signatures} (argument, not a theorem) \\
27 + An unexplained identity among the \secp{} constants & all 22 identities found follow from $\beta^2+\beta+1\equiv 0$, $\lambda^2+\lambda+1\equiv 0$ and the ideal structure; 0 control hits & \code{identity\_search.json} \\
28 + A SHA-256 regularity surviving to the full round count & diffusion complete and correlations undetectable within a small number of steps; SAT/SMT searches stall far below the published 31-step records & Section~\ref{sec:results-sha} \\
29 + \bottomrule
30 + \end{tabular}
31 + \end{threeparttable}
32 +\end{table}
33 +
34 +Two remarks keep these findings in proportion. First, each negative result is relative to a stated family of
35 +candidates: the endomorphism result is unconditional, the orbit result is a theorem, but the iteration-function and
36 +SAT results are statements about the spaces we searched. Second, none of the findings is surprising to a specialist;
37 +their value is that the pipeline produced them with evidence rather than with citations, and that it produced
38 +\emph{no} positive claim it could not support.
added paper/sections/11a_formal.tex +48 −0
@@ -0,0 +1,48 @@
1 +\subsection{Machine-checked statements}
2 +\label{sec:formal-checked}
3 +
4 +The formal layer is a Lean~4.35 project (\code{formal/}, core library only, no Mathlib) that builds from a clean
5 +state in 32~s on the laptop with no \code{sorry} and no user axiom other than the explicit fields of the abstract
6 +group structure. Bit-vector goals are closed by \code{bv\_decide}, which bit-blasts to an and-inverter graph, calls
7 +the CaDiCaL solver bundled with Lean, and re-checks the returned LRAT certificate with Lean's verified checker
8 +\citep{boving2025interactive,demoura2021lean}; a static test (\code{tests/test\_formal\_lean.py}) enforces the absence of
9 +\code{sorry}, \code{axiom}, \code{native\_decide} and Mathlib, and optionally re-runs the build. Table~\ref{tab:formal}
10 +lists the headline theorems; \code{formal/README.md} lists all 80-odd lemmas with the size of each SAT instance,
11 +the exact build output and a record of the attempts that timed out.
12 +
13 +\begin{table}[htbp]
14 + \centering\footnotesize
15 + \begin{threeparttable}
16 + \caption{Machine-checked statements (Lean 4.35, \code{bv\_decide} with LRAT certificates unless stated). AIG = nodes of the bit-blasted instance.}
17 + \label{tab:formal}
18 + \begin{tabular}{@{}p{3.6cm}p{7.2cm}p{3.4cm}@{}}
19 + \toprule
20 + Theorem(s) & Statement & Method (AIG) \\
21 + \midrule
22 + \code{Ch\_opt}, \code{Maj\_opt\_or}, \code{Maj\_opt\_xor} & $\Ch(x,y,z)=((y\oplus z)\wedge x)\oplus z$; $\Maj=(x\wedge(y\vee z))\vee(y\wedge z)=((x\oplus y)\wedge(y\oplus z))\oplus y$ & SAT (608--768) \\
23 + \code{Sigma0\_xor}, \ldots, \code{sigma1\_xor} & $\F_2$-linearity of $\SigZ,\SigO,\sigz,\sigo$ & SAT (870--960) \\
24 + \code{Sigma0\_injective}, \code{Sigma1\_injective}, \code{sigma0\_injective}, \code{sigma1\_injective} & all four maps are injective on 32 bits & SAT, UNSAT certificates (644--704) \\
25 + \code{*\_bijective} (4) & explicit inverses: $\SigZ^{-1}$ = XOR of 17 rotations, $\sigz^{-1},\sigo^{-1}$ as $32\times32$ $\F_2$ matrices; both compositions are the identity & byte-split SAT (33--4\,817) + rewriting \\
26 + \code{shr3\_not\_injective}, \code{sigma0\_rotations\_not\_injective} & controls: each summand of $\sigz$ is singular & \code{decide} \\
27 + \code{stepInv\_step}, \code{step\_stepInv}, \code{step\_bijective}, \code{steps\_bijective} & the SHA-256 step is a bijection of the 256-bit state for fixed $(K_t,W_t)$, and so is any sequence of steps & two 160-bit SAT lemmas (2\,048, 2\,420) + rewriting; direct 320-bit SAT timed out \\
28 + \code{scheduleStep\_inv}, \code{schedule\_backward} & $W_{t-16}=W_t\ominus\sigo(W_{t-2})\ominus W_{t-7}\ominus\sigz(W_{t-15})$; any 16 consecutive schedule words determine all 64 & SAT (1\,921) + recursion \\
29 + \code{localCollision\_absorb}, \code{localCollision\_xor3} & in the XOR-linearised step (with $\Ch=\Maj=0$, resp.\ $\Ch=\Maj=x\oplus y\oplus z$), for all states, constants, messages and $\delta$: the correction pattern $(\delta,\ \SigO\delta\oplus\SigZ\delta,\ \SigO(\SigZ\delta),\ 0,\ \delta,\ \SigZ\delta\oplus\SigO\delta,\ 0,\ 0,\ \delta)$ cancels after 9 steps & difference version by byte-split SAT (5\,049 / 8\,769), lifted by proven linearity of the 9-step map \\
30 + \code{absorb\_corrections\_not\_real\_collision} & control: the same corrections do \emph{not} give a collision of the real (modular) 9-step map on a concrete input & \code{decide} \\
31 + \code{schnorr\_correct}, \code{ecdsa\_correct}, \code{ecdsa\_malleable} & in an axiomatised cyclic group with $x(-P)=x(P)$: $s\equiv k+ed\Rightarrow [s]G=R+[e]P$; ECDSA verification is correct; $(r,s)$ valid $\Rightarrow(r,n-s)$ valid & rewriting from the structure fields (no SAT) \\
32 + \bottomrule
33 + \end{tabular}
34 + \begin{tablenotes}\footnotesize
35 + \item Every \code{bv\_decide} theorem depends on the per-theorem axiom Lean~4.35 records for the natively executed LRAT check (same trust base as \code{Lean.ofReduceBool}); the group-theoretic theorems depend only on \code{propext} and \code{Quot.sound}. Full \code{\#print axioms} output in \code{formal/build.log}.
36 + \end{tablenotes}
37 + \end{threeparttable}
38 +\end{table}
39 +
40 +Three points deserve comment. First, the formal layer corrected a belief we held when writing the hypothesis
41 +portfolio: we expected $\sigz$ and $\sigo$ to be non-injective because they contain a logical shift, and the SAT
42 +solver returned UNSAT for the injectivity goal---both are bijections (hypothesis~\Hname{H22}, rejected), which
43 +matters for any analysis that inverts the message schedule. Second, the 9-step local collision required a correction
44 +vector slightly different from the one we had first written down from memory ($\delta W_{i+1}$ must cancel both
45 +$\SigO\delta$ from the $e$-register and $\SigZ\delta$ from the $a$-register); the formal derivation is in the file
46 +header and the Python simulation of Axis~C agrees. Third, what is \emph{not} formalised is as important as what
47 +is: curve arithmetic, the random-mapping analysis, the probability of the real local collision and every timing claim
48 +remain informal, and no conjecture appears as a theorem.
added paper/sections/12_implications.tex +52 −0
@@ -0,0 +1,52 @@
1 +\section{Implications for Cryptographic Security}
2 +\label{sec:implications}
3 +
4 +\paragraph{For secp256k1.} Nothing in this study weakens the curve. The three structural features that make
5 +\secp{} ``special''---the generalised-Mersenne prime, $a=0$, and complex multiplication by $\Z[\omega]$---translate into
6 +exactly the constant factors the literature predicts and we measured: faster field reduction, cheaper doubling, a
7 +$2\times$ reduction in doublings for scalar multiplication through GLV, and a $\sqrt6$ reduction in the expected
8 +number of rho iterations (Section~\ref{sec:results}). Proposition~\ref{prop:end} shows that the endomorphism ring is as
9 +small as it can be given $j=0$, so there is no further endomorphism to exploit; Proposition~\ref{prop:noquotient}
10 +shows that the automorphism orbits cannot be turned into a smaller group; and the exact exclusion table
11 +(Section~\ref{sec:secp}) confirms that every known algebraic transfer is inapplicable. The generic bound therefore
12 +stands: solving one \secp{} instance costs about $\sqrt{\pi n/12}\approx 2^{126.7}$ group operations with the full
13 +automorphism trick, roughly $2^{70}$ times the largest ECDLP computation ever completed. Preprocessing does not
14 +change this conclusion: the per-instance cost $\sqrt{n/T}$ after a table of $T$ points requires $\sqrt{nT}$ work up
15 +front, and a table reducing each instance to $2^{80}$ operations would itself cost $2^{174}$ operations and $2^{93}$
16 +entries. Our measurements (Hypothesis~\Hname{H12}) put the constant at about $1.5$, which does not alter the
17 +exponent.
18 +
19 +\paragraph{For the signature schemes.} The security difference between ECDSA and BIP340 is structural: Schnorr's
20 +linear response $s=k+ed$ admits a clean reduction and enables batch verification, aggregation and threshold
21 +protocols; ECDSA's $k^{-1}$ obstructs all three and forces the proofs into stronger idealisations. Operationally
22 +the two schemes cost the same to verify in \libsecp{} (within $5\%$), so the choice is about provable security and
23 +protocol features, not speed. Batch verification is the one verification-side improvement that is both real and
24 +consensus-neutral; its gain is bounded by the non-linear-algebra work per signature (square root, hashing) and
25 +saturates near $3\times$ in our measurements. The quantities that actually decide security in practice are
26 +nonce generation and side-channel resistance, which deterministic nonces and constant-time scalar multiplication
27 +address; none of the algebra in this paper changes that assessment.
28 +
29 +\paragraph{For SHA-256.} Our reduced-step experiments reproduce the easy end of the known picture: full diffusion
30 +within a few steps, no detectable bias beyond a handful of steps, SAT preimages and collisions feasible only far
31 +below the published dedicated-tool records. The step function being a bijection and the schedule being invertible
32 +are reminders that the compression function alone offers no preimage resistance---the Merkle--Damg\aa rd
33 +feed-forward does---and that ``free-start'' results must not be confused with attacks on the hash. The security
34 +margin of SHA-256 is the distance between 31 (practical collision), 37 (theoretical collision), 45 (theoretical
35 +preimage) and 64 steps; nothing here narrows it.
36 +
37 +\paragraph{For automated discovery.} The pipeline's value was not a new attack but the discipline it imposed: every
38 +candidate shortcut was either explained by existing theory, measured to be a constant, or refuted. An autonomous
39 +agent working on cryptographic primitives is exposed to a specific failure mode---mistaking a constant-factor or
40 +representation improvement for a complexity improvement---and the classification rubric, the operation counts and
41 +the formal layer are the controls against it. We regard the fact that the agent produced zero claims of a break, while
42 +producing a closed-form explanation of a set of implementation constants and a dozen quantified reproductions, as
43 +the correct outcome.
44 +
45 +\paragraph{Responsible-disclosure assessment.} No security-relevant result emerged that would require disclosure.
46 +All instances attacked were synthetic; no real key, address or blockchain datum was used or produced; the code
47 +released with the paper attacks only the reduced-size curves it generates itself.
48 +
49 +\paragraph{Quantum computing.} The analysis above is classical. Shor's algorithm \citep{shor1997polynomial} solves the
50 +ECDLP in polynomial time on a fault-tolerant quantum computer, with concrete resource estimates for 256-bit curves
51 +in the range of a few thousand logical qubits \citep{roetteler2017quantum}; this, not any classical shortcut, is the
52 +known threat to \secp{}, and it is independent of the curve's special structure.
added paper/sections/13_limitations.tex +27 −0
@@ -0,0 +1,27 @@
1 +\section{Limitations}
2 +\label{sec:limitations}
3 +
4 +\begin{itemize}
5 +\item \textbf{Scale.} The ECDLP experiments run on groups of at most $2^{48}$--$2^{56}$ elements. The fitted
6 +exponents and constants describe this range; extrapolation to $2^{256}$ rests on the random-mapping heuristic and on
7 +Shoup's bound, not on our data. We state no result about \secp{} itself that is not an exact algebraic verification.
8 +\item \textbf{Generic design space.} The iteration-function search (\Hname{H09}) explores a bounded parameter space of
9 +$r$-adding and mixed walks; it cannot find a non-generic idea, and its negative result is a statement about that space.
10 +\item \textbf{SHA-256 encodings.} Our CNF and SMT encodings are generic (Tseitin, ripple-carry adders) and unguided by
11 +differential characteristics; the step counts they reach are therefore far below the literature's and should be read
12 +as a calibration of generic solvers, not as a limit of SAT-based cryptanalysis \citep{alamgir2024sha256}.
13 +\item \textbf{Formal coverage.} The Lean statements cover bit-level identities and abstract-group facts. They do not
14 +formalise the curve arithmetic, the random-mapping analysis or any probabilistic claim; those remain informal.
15 +\item \textbf{Timing measurements.} Wall-clock figures come from two Apple Silicon machines and depend on the
16 +toolchains used; operation counts are the portable quantity. Batch-verification speed-ups are relative to the same
17 +arithmetic library and would differ against a hand-optimised assembly implementation.
18 +\item \textbf{Novelty.} The one algebraic statement we did not find written explicitly (the closed form of the
19 +\libsecp{} GLV constants) is a direct consequence of classical CM theory; we claim exposition, not discovery, and a
20 +reader who knows a prior statement of it should treat it as a reproduction.
21 +\item \textbf{Single agent, single run.} The pipeline was executed once by one autonomous agent; we do not measure
22 +variance across runs or agents, and the methodological assessment (\Hname{H24}) is qualitative. A controlled
23 +comparison against a baseline agent without the pipeline's discipline is left to future work.
24 +\item \textbf{Literature verification.} Bibliographic entries were verified against public records; where a venue
25 +record omitted a field (volume numbers of some Springer series), it was filled from series knowledge and flagged in
26 +\code{literature/unverified.md}.
27 +\end{itemize}
added paper/sections/14_conclusion.tex +32 −0
@@ -0,0 +1,32 @@
1 +\section{Conclusion}
2 +\label{sec:conclusion}
3 +
4 +We asked whether an autonomous, hypothesis-driven exploration of the mathematics behind Bitcoin's primitives could
5 +find a shortcut past the square-root barrier, and we built the apparatus to answer the question honestly: a verified
6 +literature base, twenty-five falsifiable hypotheses, exact symbolic checks, a native experimental engine with
7 +operation counts and provenance, machine-checked statements, and a classification rubric that does not reward
8 +enthusiasm. The answer is no, with the following specifics.
9 +
10 +The algebraic structure of \secp{} is fully accounted for by complex multiplication by $\Z[\omega]$: it yields the
11 +$\sqrt6$ reduction in rho iterations and the halving of doublings in scalar multiplication, it determines the GLV
12 +constants of \libsecp{} in closed form, and it leaves no room for a further endomorphism or for a smaller quotient
13 +group. The generic algorithms behave exactly as the random-mapping theory predicts on $6\,000$ synthetic instances:
14 +$T(n)\propto n^{0.5}$, $1.254\sqrt n$ iterations for rho, $\sqrt6$ from the automorphisms in iterations---though not,
15 +in our engine, in seconds---and the trade-offs of memory, parallelism and preprocessing that the literature describes.
16 +An automated search over iteration-function designs cannot beat the random-mapping floor. ECDSA and Schnorr differ
17 +in proof structure, not in verification cost; batch verification is the one real, consensus-neutral verification
18 +speed-up and it saturates at about $3\times$ for an identifiable reason. Reduced-step SHA-256 behaves as the
19 +published analyses say and our generic solvers reach only the easy regime. The bit-level identities that
20 +implementations rely on are now machine-checked, and so are the two textbook facts about the signature equations.
21 +
22 +What the exercise shows about automated discovery is perhaps more useful than any single number. On a problem
23 +where the correct answer is ``nothing new'', an autonomous agent is at risk of producing confident
24 +over-interpretations---a constant mistaken for an exponent, a reproduction mistaken for a discovery, a
25 +compression-function property mistaken for a hash property. The controls that prevented this here were cheap:
26 +operation counts next to timings, two implementations per component, a negative control per family, exact
27 +arithmetic, provenance on every number, formal statements separated from informal ones, and a classifier that
28 +required a literature match before the word ``new'' could be used. The one genuinely clarifying algebraic statement
29 +the pipeline produced---that the \libsecp{} constants are the coordinates of $\bar\pi-1$---came from a bounded
30 +enumeration followed by a human-readable proof, which is the pattern we would expect future automated
31 +mathematical research on cryptographic standards to follow: automation proposes, exact verification disposes, and
32 +the literature has the last word on novelty.
added paper/sections/abstract.tex +1 −0
@@ -0,0 +1 @@
1 +Placeholder abstract.
\ No newline at end of file
added paper/sections/titlepage.tex +77 −0
@@ -0,0 +1,77 @@
1 +% ============================================================================
2 +% Title Page
3 +% ============================================================================
4 +\thispagestyle{empty}
5 +
6 +\begin{center}
7 +
8 +% --- Logo ---
9 +\includegraphics[width=3.5cm]{uq_logo.jpg}
10 +
11 +\vspace{0.6cm}
12 +
13 +{\footnotesize\textsc{Université du Québec en Outaouais}}\\[0.15cm]
14 +{\footnotesize\textsc{Département des sciences administratives}}
15 +
16 +\vspace{0.8cm}
17 +
18 +{\footnotesize\textsc{Working Paper No.~\WPnumber}}
19 +
20 +\vspace{1.2cm}
21 +
22 +% --- Title ---
23 +{\LARGE\bfseries \WPtitle\par}
24 +
25 +\ifx\WPsubtitle\empty\else
26 + \vspace{0.3cm}
27 + {\large\itshape \WPsubtitle\par}
28 +\fi
29 +
30 +\vspace{1.2cm}
31 +
32 +% --- Author ---
33 +{\large \WPauthor}\\[0.3cm]
34 +{\normalsize \WPaffiliation}\\[0.15cm]
35 +{\normalsize \href{mailto:\WPemail}{\WPemail}}\\[0.15cm]
36 +{\small \WPaddress}
37 +
38 +\vspace{0.8cm}
39 +
40 +% --- Date & Version ---
41 +{\normalsize \WPdate}\\[0.1cm]
42 +{\small Version~\WPversion}
43 +
44 +\end{center}
45 +
46 +\vfill
47 +
48 +\newpage
49 +
50 +% ============================================================================
51 +% Abstract Page
52 +% ============================================================================
53 +\thispagestyle{empty}
54 +
55 +\vspace*{1cm}
56 +
57 +\noindent\rule{\textwidth}{0.4pt}
58 +\vspace{0.3cm}
59 +
60 +\noindent\textbf{Abstract}
61 +
62 +\vspace{0.15cm}
63 +
64 +\noindent\WPabstract
65 +
66 +\vspace{0.4cm}
67 +
68 +\noindent\textbf{Keywords:} \WPkeywords
69 +
70 +\vspace{0.15cm}
71 +
72 +\noindent\textbf{JEL Classification:} \WPjel
73 +
74 +\vspace{0.3cm}
75 +\noindent\rule{\textwidth}{0.4pt}
76 +
77 +\newpage
added paper/tables/ecdlp_fits.tex +12 −0
@@ -0,0 +1,12 @@
1 +\resizebox{\textwidth}{!}{%
2 +\begin{tabular}{@{}lrrrrrr@{}}
3 +\toprule Algorithm & $\hat\alpha$ (all sizes, ops) & 95\% CI & $R^2$ & $\hat\alpha$ ($\ge 32$ bits, ops) & 95\% CI & $\hat\alpha$ ($\ge 32$ bits, iterations) \\ \midrule
4 +BSGS & 0.471 & [0.445, 0.496] & 0.9975 & 0.500 & [0.495, 0.506] & 0.509 [0.490, 0.526] \\
5 +kangaroo (full interval) & 0.493 & [0.487, 0.499] & 0.9998 & 0.498 & [0.487, 0.509] & 0.498 [0.487, 0.510] \\
6 +rho, Brent & 0.482 & [0.467, 0.500] & 0.9988 & 0.500 & [0.491, 0.511] & 0.501 [0.491, 0.513] \\
7 +rho, DP ($r=20$) & 0.450 & [0.425, 0.479] & 0.9959 & 0.478 & [0.463, 0.495] & 0.490 [0.476, 0.502] \\
8 +rho, DP + negation ($r=128$) & 0.438 & [0.404, 0.478] & 0.9922 & 0.480 & [0.469, 0.498] & 0.494 [0.486, 0.508] \\
9 +rho, DP ($r=128$) & 0.445 & [0.413, 0.483] & 0.9940 & 0.483 & [0.463, 0.506] & 0.496 [0.482, 0.509] \\
10 +rho, Floyd & 0.489 & [0.480, 0.504] & 0.9993 & 0.492 & [0.483, 0.502] & 0.493 [0.483, 0.502] \\
11 +\bottomrule
12 +\end{tabular}}
added paper/tables/ecdlp_j0.tex +10 −0
@@ -0,0 +1,10 @@
1 +\resizebox{\textwidth}{!}{%
2 +\begin{tabular}{@{}lrrrrr@{}}
3 +\toprule Classes & Theory & Iterations$/\sqrt n$ (95\% CI) & Iteration speed-up (paired, 95\% CI) & ns per iteration & Wall-clock speed-up \\ \midrule
4 +none (plain rho, $r=128$) & $\sqrt{\pi/2} = 1.253$ & 1.206 [1.114, 1.299] & 1 & 38 & 1 \\
5 +$\{\pm P\}$ (negation) & $\sqrt2 = 1.414$ & 0.933 [0.865, 1.006] & 1.293 [1.158, 1.439] & 56 & 0.73 [0.65, 0.83] \\
6 +$\{P,\phi P,\phi^2P\}$ & $\sqrt3 = 1.732$ & 0.797 [0.742, 0.849] & 1.515 [1.375, 1.674] & 71 & 0.75 [0.65, 0.86] \\
7 +order-6 orbits, $r=128$ & $\sqrt6 = 2.449$ & 0.546 [0.510, 0.581] & 2.209 [2.001, 2.450] & 80 & 0.95 [0.84, 1.09] \\
8 +order-6 orbits, $r=20$ & $\sqrt6 = 2.449$ & 0.499 [0.462, 0.536] & 2.419 [2.185, 2.702] & 83 & 0.95 [0.83, 1.08] \\
9 +\bottomrule
10 +\end{tabular}}
added paper/tables/ecdlp_parallel.tex +11 −0
@@ -0,0 +1,11 @@
1 +\resizebox{\textwidth}{!}{%
2 +\begin{tabular}{@{}rrrrrr@{}}
3 +\toprule Threads & Wall (s) & Group ops ($\times 10^6$) & Mops/s & Speed-up & Efficiency \\ \midrule
4 +1 & 8.78 & 229.6 & 26 & 1.00 & 1.00 \\
5 +2 & 3.04 & 155.5 & 51 & 1.96 & 0.98 \\
6 +4 & 1.39 & 139.4 & 100 & 3.83 & 0.96 \\
7 +8 & 0.81 & 158.9 & 195 & 7.46 & 0.93 \\
8 +16 & 0.52 & 200.3 & 388 & 14.83 & 0.93 \\
9 +28 & 0.41 & 235.3 & 578 & 22.11 & 0.79 \\
10 +\bottomrule
11 +\end{tabular}}
added paper/tables/ecdlp_scaling.tex +48 −0
@@ -0,0 +1,48 @@
1 +\resizebox{\textwidth}{!}{%
2 +\begin{tabular}{@{}llrrrrrr@{}}
3 +\toprule Algorithm & Bits & Inst. & Group ops (mean) & 95\% CI & ops$/\sqrt n$ & Theory & Wall (s) \\ \midrule
4 +BSGS & 20 & 60 & 2\,679 & [2\,603\, 2\,758] & 3.01 & 2.00 & 0.000 \\
5 + & 28 & 60 & 23\,209 & [22\,061\, 24\,380] & 1.65 & 2.00 & 0.001 \\
6 + & 36 & 60 & 350\,878 & [331\,254\, 370\,841] & 1.53 & 2.00 & 0.009 \\
7 + & 44 & 60 & 5\,388\,261 & [5\,127\,106\, 5\,639\,773] & 1.50 & 2.00 & 0.162 \\
8 + & 48 & 60 & 21\,353\,478 & [20\,208\,948\, 22\,505\,958] & 1.47 & 2.00 & 0.787 \\
9 +kangaroo (full interval) & 20 & 60 & 1\,993 & [1\,773\, 2\,215] & 2.24 & 2.00 & 0.000 \\
10 + & 28 & 60 & 29\,593 & [26\,329\, 33\,187] & 2.10 & 2.00 & 0.001 \\
11 + & 36 & 60 & 438\,187 & [382\,001\, 500\,060] & 1.91 & 2.00 & 0.012 \\
12 + & 44 & 60 & 6\,871\,559 & [6\,144\,280\, 7\,617\,726] & 1.91 & 2.00 & 0.195 \\
13 + & 48 & 60 & 26\,808\,039 & [23\,569\,625\, 30\,122\,184] & 1.85 & 2.00 & 0.770 \\
14 + & 52 & 60 & 106\,707\,359 & [93\,650\,879\, 120\,096\,160] & 1.84 & 2.00 & 3.074 \\
15 + & 56 & 60 & 488\,403\,991 & [431\,328\,685\, 544\,229\,758] & 2.12 & 2.00 & 13.349 \\
16 +rho\, Brent & 20 & 60 & 2\,734 & [2\,433\, 3\,059] & 3.08 & 1.44 & 0.000 \\
17 + & 28 & 60 & 27\,973 & [24\,404\, 31\,616] & 1.98 & 1.44 & 0.002 \\
18 + & 36 & 60 & 447\,331 & [389\,033\, 506\,702] & 1.95 & 1.44 & 0.049 \\
19 + & 44 & 60 & 6\,899\,435 & [5\,901\,830\, 7\,907\,650] & 1.92 & 1.44 & 0.890 \\
20 + & 48 & 60 & 28\,059\,030 & [23\,390\,277\, 33\,116\,242] & 1.93 & 1.44 & 3.867 \\
21 +rho\, DP ($r=20$) & 20 & 60 & 4\,750 & [4\,387\, 5\,123] & 5.34 & 1.25 & 0.000 \\
22 + & 28 & 60 & 28\,110 & [25\,070\, 31\,425] & 1.99 & 1.25 & 0.002 \\
23 + & 36 & 60 & 360\,440 & [312\,002\, 414\,020] & 1.57 & 1.25 & 0.015 \\
24 + & 44 & 60 & 4\,978\,245 & [4\,403\,399\, 5\,638\,782] & 1.38 & 1.25 & 0.164 \\
25 + & 48 & 60 & 19\,966\,687 & [17\,089\,674\, 23\,087\,087] & 1.38 & 1.25 & 0.625 \\
26 + & 52 & 60 & 61\,520\,224 & [52\,474\,644\, 71\,426\,525] & 1.06 & 1.25 & 1.872 \\
27 + & 56 & 60 & 300\,438\,016 & [263\,457\,604\, 340\,736\,432] & 1.31 & 1.25 & 8.767 \\
28 +rho\, DP + negation ($r=128$) & 20 & 60 & 5\,202 & [4\,949\, 5\,473] & 5.85 & 0.89 & 0.000 \\
29 + & 28 & 60 & 23\,719 & [21\,611\, 25\,763] & 1.68 & 0.89 & 0.002 \\
30 + & 36 & 60 & 241\,754 & [214\,928\, 267\,329] & 1.05 & 0.89 & 0.014 \\
31 + & 44 & 60 & 3\,387\,252 & [2\,965\,303\, 3\,843\,106] & 0.94 & 0.89 & 0.165 \\
32 + & 48 & 60 & 14\,680\,781 & [13\,034\,824\, 16\,416\,680] & 1.01 & 0.89 & 0.693 \\
33 + & 52 & 60 & 58\,546\,774 & [51\,602\,632\, 65\,730\,359] & 1.01 & 0.89 & 2.729 \\
34 + & 56 & 60 & 191\,425\,086 & [165\,120\,361\, 220\,056\,040] & 0.83 & 0.89 & 8.628 \\
35 +rho\, DP ($r=128$) & 20 & 60 & 5\,706 & [5\,398\, 6\,008] & 6.42 & 1.25 & 0.000 \\
36 + & 28 & 60 & 31\,644 & [28\,414\, 34\,838] & 2.24 & 1.25 & 0.002 \\
37 + & 36 & 60 & 356\,473 & [312\,663\, 401\,282] & 1.55 & 1.25 & 0.015 \\
38 + & 44 & 60 & 4\,307\,374 & [3\,767\,110\, 4\,915\,064] & 1.20 & 1.25 & 0.144 \\
39 + & 48 & 60 & 17\,878\,425 & [15\,065\,049\, 20\,998\,708] & 1.23 & 1.25 & 0.571 \\
40 + & 52 & 60 & 71\,624\,501 & [62\,610\,474\, 81\,330\,698] & 1.24 & 1.25 & 2.202 \\
41 + & 56 & 60 & 318\,624\,520 & [280\,613\,624\, 358\,951\,118] & 1.39 & 1.25 & 9.104 \\
42 +rho\, Floyd & 20 & 60 & 3\,738 & [3\,336\, 4\,173] & 4.21 & 3.76 & 0.000 \\
43 + & 28 & 60 & 40\,290 & [34\,737\, 45\,384] & 2.86 & 3.76 & 0.003 \\
44 + & 36 & 60 & 736\,600 & [626\,244\, 849\,037] & 3.21 & 3.76 & 0.075 \\
45 + & 44 & 60 & 10\,929\,879 & [9\,223\,312\, 12\,761\,485] & 3.04 & 3.76 & 1.309 \\
46 + & 48 & 60 & 43\,544\,917 & [37\,332\,520\, 50\,531\,812] & 3.00 & 3.76 & 5.612 \\
47 +\bottomrule
48 +\end{tabular}}
added paper/tables/ecdlp_sweep_r.tex +17 −0
@@ -0,0 +1,17 @@
1 +\resizebox{\textwidth}{!}{%
2 +\begin{tabular}{@{}lrrrrrr@{}}
3 +\toprule $r$ & \multicolumn{3}{c}{36 bits: iterations$/\sqrt n$} & \multicolumn{3}{c}{40 bits: iterations$/\sqrt n$} \\
4 + & plain & mixed 2 & mixed 4 & plain & mixed 2 & mixed 4 \\ \midrule
5 +2 & 3.462 & --- & --- & 3.775 & --- & --- \\
6 +3 & 2.340 & 1.775 & 2.340 & 2.262 & 1.609 & 2.262 \\
7 +4 & 1.688 & 1.479 & 1.569 & 1.668 & 1.720 & 1.540 \\
8 +8 & 1.341 & 1.566 & 1.351 & 1.571 & 1.471 & 1.277 \\
9 +16 & 1.248 & 1.684 & 1.389 & 1.323 & 1.586 & 1.292 \\
10 +20 & 1.484 & 1.479 & 1.212 & 1.531 & 1.295 & 1.226 \\
11 +32 & 1.095 & 1.522 & 1.328 & 1.384 & 1.335 & 1.444 \\
12 +64 & 1.369 & 1.385 & 1.577 & 1.244 & 1.432 & 1.241 \\
13 +128 & 1.293 & 1.333 & 1.462 & 1.366 & 1.572 & 1.112 \\
14 +\midrule
15 +random-mapping constant $\sqrt{\pi/2}$ & 1.253 & & & 1.253 & & \\
16 +\bottomrule
17 +\end{tabular}}
added paper/tables/ecdlp_tmto.tex +19 −0
@@ -0,0 +1,19 @@
1 +\resizebox{\textwidth}{!}{%
2 +\begin{tabular}{@{}lrrrrr@{}}
3 +\toprule Algorithm / parameter & Memory (entries) & Memory (MB) & Group ops & Predicted & Wall (s) \\ \midrule
4 +BSGS\, $m=2^{10}$ (36 bits) & 1\,023 & 0.0 & 29\,225\,447 & 27\,125\,293 & 0.613 \\
5 +BSGS\, $m=2^{12}$ (36 bits) & 4\,095 & 0.1 & 7\,311\,605 & 6\,785\,163 & 0.161 \\
6 +BSGS\, $m=2^{14}$ (36 bits) & 16\,383 & 0.5 & 1\,844\,512 & 1\,711\,651 & 0.041 \\
7 +BSGS\, $m=2^{16}$ (36 bits) & 65\,535 & 1.9 & 523\,838 & 489\,353 & 0.015 \\
8 +BSGS\, $m=2^{18}$ (36 bits) & 262\,143 & 7.4 & 378\,053 & 368\,098 & 0.013 \\
9 +BSGS\, $m=2^{20}$ (36 bits) & 1\,048\,575 & 29.8 & 1\,078\,839 & 1\,075\,065 & 0.031 \\
10 +\midrule
11 +rho\, DP density $2^{-2}$ (40 bits) & 227\,779 & 28.88 & 4\,946\,673 & 1\,128\,106 & 0.499 \\
12 +rho\, DP density $2^{-4}$ (40 bits) & 69\,926 & 7.22 & 2\,360\,214 & 1\,128\,106 & 0.181 \\
13 +rho\, DP density $2^{-6}$ (40 bits) & 18\,867 & 1.80 & 1\,547\,360 & 1\,128\,106 & 0.076 \\
14 +rho\, DP density $2^{-8}$ (40 bits) & 4\,250 & 0.45 & 1\,165\,676 & 1\,128\,106 & 0.042 \\
15 +rho\, DP density $2^{-10}$ (40 bits) & 1\,270 & 0.11 & 1\,336\,710 & 1\,128\,106 & 0.042 \\
16 +rho\, DP density $2^{-12}$ (40 bits) & 352 & 0.06 & 1\,468\,980 & 1\,128\,106 & 0.043 \\
17 +rho\, DP density $2^{-14}$ (40 bits) & 128 & 0.06 & 2\,085\,998 & 1\,128\,106 & 0.062 \\
18 +\bottomrule
19 +\end{tabular}}
added paper/tables/hypotheses.tex +32 −0
@@ -0,0 +1,32 @@
1 +% generated by experiments/automated_discovery/registry.py — do not edit
2 +\begin{longtable}{@{}p{0.8cm}p{0.5cm}p{4.2cm}p{2.6cm}p{6.4cm}@{}}
3 +\caption{Hypothesis portfolio and evaluation outcome. Classification vocabulary follows the mandate (new verified result; known result independently reproduced; conditional result; empirical observation; rejected conjecture; unresolved direction).}\label{tab:hypotheses}\\
4 +\toprule ID & Axis & Hypothesis & Classification & Verdict \\ \midrule \endfirsthead
5 +\toprule ID & Axis & Hypothesis & Classification & Verdict \\ \midrule \endhead
6 +\bottomrule \endfoot
7 +H01 & A & Standardised secp256k1 parameters and GLV constants are exactly consistent & Known result reproduced & All 14 facts agree between Python and PARI/GP; ($\beta$, $\lambda$$^2$) mismatch confirmed as expected. \\
8 +H02 & A & End(E) = Z[$\omega$] and the GLV basis is the Eisenstein ideal ($\pi$̄ -- 1): closed form of the libsecp256k1 constants & Known result reproduced & Closed form holds exactly; all 22 identities found by the search are explained by CM theory. Known in principle (ideal structure of the GLV lattice), the explicit identification with the published constants is a useful clarification, not new mathematics. \\
9 +H03 & A & GLV decomposition halves doublings; sub-scalars never exceed 128 bits & Known result reproduced & Max sub-scalar size 128 bits; 184 vs 304 group ops (GLV+wNAF vs wNAF), 2.07$\times$ vs double-and-add. \\
10 +H04 & A & Automorphism orbits give exactly $\sqrt{\ }$6 on Pollard rho, with measurable fruitless-cycle overhead & Known result reproduced & 180 paired instances at 44--52 bits: iteration speed-up 2.21 [2.00, 2.45] (r = 128) and 2.42 [2.19, 2.70] (r = 20) vs $\sqrt{\ }$6 = 2.449; constant 0.499$\cdot$$\sqrt{\ }$n vs $\sqrt{\ }$($\pi$/12) = 0.512; negation alone 1.29 vs $\sqrt{\ }$2, $\varphi$-orbits alone 1.52 vs $\sqrt{\ }$3; fruitless cycles $\le$ 0.11 per 1000 iterations. Wall-clock speed-up only 0.95 (order-6) and 0.73 (negation) because canonicalisation costs 56--83 ns vs 38 ns per plain iteration in this engine. \\
11 +H05 & A & The automorphism orbit set cannot be made a group of order n/6 (rejected conjecture) & Rejected conjecture & Compatibility fails for 1500/1500 random pairs; proof: a congruence would make the orbit of O a subgroup H with cosets the other orbits, but orbits have size 6 ≠ |H| = 1. \\
12 +H06 & A & No known algebraic attack applies to secp256k1; prime-field index calculus with m = 2 costs ≫ $\sqrt{\ }$n & Known result reproduced & All exclusion criteria verified; measured decomposition rates match the Poisson prediction within sampling error; even with a free decomposition oracle the relation stage costs $\ge$ 2.4$\sqrt{\ }$n and the linear algebra |F|$^2$ ≫ $\sqrt{\ }$n. \\
13 +H07 & A & An exhaustive small-coefficient identity search finds only CM-explained relations and no cross-modulus or generator-specific relation & Empirical observation & 22 identities found, 22 explained, 0 unexplained, 0 false positives (expected chance hits 10^-73). \\
14 +H08 & B & Measured ECDLP cost scales as n^0.5 with constants matching random-mapping theory & Known result reproduced & 3 840 verified instances, 20--56 bits: fitted α $\in$ [0.478, 0.500] ($\ge$ 32 bits) for all seven configurations with CIs touching 0.5; pooled rho constant 1.254$\cdot$$\sqrt{\ }$n [1.21, 1.30] on 480 runs vs $\sqrt{\ }$($\pi$/2) = 1.2533; BSGS 1.50$\cdot$$\sqrt{\ }$n (x-keyed ±j table); kangaroo 1.94$\cdot$$\sqrt{\ }$n; Floyd 2.67$\cdot$$\sqrt{\ }$n, Brent 1.65$\cdot$$\sqrt{\ }$n. \\
15 +H09 & B & Automated search over iteration-function designs cannot beat the random-mapping constant for a given class size & Rejected conjecture & 25 configurations $\times$ 80 instances: r = 2 and r = 3 walks cost 3.77$\cdot$$\sqrt{\ }$n and 2.26$\cdot$$\sqrt{\ }$n (negative control), r $\ge$ 8 cluster at 1.1--1.5$\cdot$$\sqrt{\ }$n with CIs overlapping $\sqrt{\ }$($\pi$/2); mixed walks indistinguishable; best cell r32 at 1.10 [0.92, 1.28] not significantly below the random-mapping constant. No generic iteration design beats the random-mapping floor. \\
16 +H10 & B & Parallel distinguished-point collision search has near-linear speed-up up to 28 threads & Known result reproduced & 56-bit instances on an Apple M3 Ultra: 14.8$\times$ on 16 threads (efficiency 0.93), 22.1$\times$ on 28 threads (0.79, P+E cores); total group operations within a factor 1.0 of single-thread. \\
17 +H11 & B & Time--memory trade-off: BSGS with memory m costs m + n/m operations; rho with DP density θ stores $\approx$ θ$\cdot$$\sqrt{\ }$($\pi$n/2) points & Known result reproduced & BSGS cost matches m + n/(2m) within 7.8 \% for m from 2^10 to 2^20 at 36 bits; DP-rho at 40 bits spans 1.17--4.95 M operations as the DP density goes from 2^-12 (optimum, $\approx$ $\sqrt{\ }$($\pi$n/2)) to 2^-2 (restart-dominated) and 2^-14 (overshoot-dominated). \\
18 +H12 & B & Discrete logarithms with preprocessing: online cost $\approx$ 1.5$\cdot$$\sqrt{\ }$(n/T) after storing T distinguished points & Known result reproduced & Online/$\sqrt{\ }$(n/T) ratio 1.48--1.67 across T = 2^4 … 2^12 at 22 and 26 bits; per-instance speed-up 2.6$\times$ $\to$ 42$\times$ as predicted by $\sqrt{\ }$T scaling. \\
19 +H13 & B & BIP340 batch verification reduces per-signature cost by a factor that grows with batch size and saturates & Known result reproduced & Per-signature cost falls from 24.5 µs (single, k256) to 7.9 µs at b = 2048 with Pippenger (S = 3.10 [3.05, 3.16]; 1.67$\times$ vs libsecp256k1 single verify); Straus plateaus at $\approx$ 2$\times$; $\approx$ 3 µs/sig of non-MSM work (lift\_x, tagged hash) caps the gain; negative control rejected; same shape on M3 Ultra. \\
20 +H14 & B & Implementation choice dominates signature performance: three orders of magnitude between pure Python and libsecp256k1 & Empirical observation & Schnorr verify: 7.4 ms (pure Python) vs 24.5 µs (k256) vs 13.2 µs (libsecp256k1): 560$\times$ spread; libsecp256k1 verifies 1.9$\times$ faster than k256 while k256 signs 1.8$\times$ faster (table-based generator multiplication 8.3 µs vs blinded ecmult\_gen 19.8 µs); ECDSA and Schnorr verification within 5 \% in libsecp256k1. \\
21 +H15 & C & A single-bit message difference reaches full avalanche within a small number of steps & Pending & pending \\
22 +H16 & C & Statistically detectable input--output correlations vanish after a small number of steps & Pending & pending \\
23 +H17 & C & SAT preimage search on step-reduced SHA-256 scales exponentially in the number of steps and in the number of free bits & Pending & pending \\
24 +H18 & C & Generic SAT encodings find step-reduced collisions only far below the published dedicated-tool records & Pending & pending \\
25 +H19 & C & The 9-step local collision holds with probability 1 in the XOR-linearised model and with low, position-dependent probability in the real step function & Pending & pending \\
26 +H20 & C & The SHA-256 step is a bijection on the state and the message schedule is invertible, so free-start and pseudo-preimage problems on the compression function are trivial & Known result reproduced & Machine-checked: stepInv ∘ step = id and step ∘ stepInv = id (per-component SAT lemmas, AIG 2048/2420), steps\_bijective for any number of rounds, scheduleStep\_inv and schedule\_backward; executable demonstration in tests/test\_core.py. Consequence (pseudo-preimages of the feed-forward-free compression function are free) stated informally. \\
27 +H21 & D & Lean 4 bv\_decide machine-checks the SHA-256 word-level identities used by optimised implementations & Known result reproduced & Lean 4.35 bv\_decide (LRAT certificates re-checked in Lean): Ch\_opt, Maj\_opt\_or, Maj\_opt\_xor, GF(2)-linearity of $\Sigma$0, $\Sigma$1, $\sigma$0, $\sigma$1, injectivity and explicit inverses ($\Sigma$0⁻¹ = XOR of 17 rotations) hence bijectivity; clean build 32 s, no sorry, no user axiom, no Mathlib. \\
28 +H22 & D & $\sigma$0 and $\sigma$1 are not injective on 32-bit words (kernel of positive dimension), unlike $\Sigma$0 and $\Sigma$1 & Rejected conjecture & Conjecture refuted: $\sigma$0 and $\sigma$1 are bijective (bv\_decide UNSAT certificates for injectivity; GF(2) rank 32 by two independent eliminations; explicit 32$\times$32 inverse matrices verified in Lean). The shift makes each summand singular (x≫3 has rank 29; rotr7$\oplus$rotr18 kills 0xFFFFFFFF) but the sum is a unit. A wrong belief caught by the formal layer. \\
29 +H23 & D & Cross-implementation agreement is a necessary gate: every arithmetic component is checked against at least one independent implementation & Empirical observation & 11 reference tests pass; agreement with 5 independent implementations; two development bugs caught by the gate (kangaroo retry loop, PARI output parsing). \\
30 +H24 & D & The six-stage pipeline rediscovers known structure at a high rate and reports zero validated novel shortcuts & Pending & pending \\
31 +H25 & D & ECDSA malleability and Schnorr correctness follow from the abstract group axioms alone (formalised) & Known result reproduced & Machine-checked in an axiomatised CyclicGroup structure (fields listed in formal/README.md): schnorr\_correct (s = k + e$\cdot$d mod n ⟹ s•G = R + e•P), ecdsa\_correct, ecdsa\_malleable ((r,s) valid ⟹ (r, n--s) valid, modular inverse as witness). Only propext/Quot.sound used. \\
32 +\end{longtable}
added paper/tables/index_calculus_probe.tex +12 −0
@@ -0,0 +1,12 @@
1 +\resizebox{\textwidth}{!}{%
2 +\begin{tabular}{@{}rrrrrrrr@{}}
3 +\toprule Bits & $|F|$ & $|F|/\sqrt n$ & Trials & Rate (meas.) & Rate (pred.) & Oracle relations$/\sqrt n$ & Lin.\ alg.$/\sqrt n$ \\ \midrule
4 +16 & 272 & 1.06 & 400 & 0.435 & 0.433 & 2.45 & 290 \\
5 +16 & 596 & 3.00 & 400 & 0.990 & 0.989 & 3.03 & 1786 \\
6 +20 & 1\,022 & 1.00 & 300 & 0.393 & 0.395 & 2.55 & 1025 \\
7 +20 & 3\,222 & 3.83 & 300 & 1.000 & 0.999 & 3.83 & 12346 \\
8 +24 & 3\,872 & 0.99 & 200 & 0.375 & 0.389 & 2.65 & 3842 \\
9 +24 & 17\,262 & 5.11 & 200 & 1.000 & 1.000 & 5.11 & 88234 \\
10 +28 & 15\,002 & 1.00 & 120 & 0.308 & 0.394 & 3.25 & 15025 \\
11 +\bottomrule
12 +\end{tabular}}
added paper/tables/numbers.tex +68 −0
@@ -0,0 +1,68 @@
1 +% generated by scripts/make_tables.py — do not edit
2 +\newcommand{\secpAllAgree}{yes}
3 +\newcommand{\secpNumChecks}{14}
4 +\newcommand{\secpTwistCofactorBits}{220}
5 +\newcommand{\secpEmbeddingDegreeBits}{254}
6 +\newcommand{\opsDbladdTotal}{381.3}
7 +\newcommand{\opsDbladdAdd}{127.2}
8 +\newcommand{\opsDbladdDbl}{254.1}
9 +\newcommand{\opsDbladdSpeedup}{1.00}
10 +\newcommand{\opsWnafFourTotal}{308.5}
11 +\newcommand{\opsWnafFourAdd}{53.7}
12 +\newcommand{\opsWnafFourDbl}{254.8}
13 +\newcommand{\opsWnafFourSpeedup}{1.24}
14 +\newcommand{\opsWnafFiveTotal}{303.4}
15 +\newcommand{\opsWnafFiveAdd}{49.1}
16 +\newcommand{\opsWnafFiveDbl}{254.3}
17 +\newcommand{\opsWnafFiveSpeedup}{1.26}
18 +\newcommand{\opsGlvFourTotal}{184.4}
19 +\newcommand{\opsGlvFourAdd}{56.6}
20 +\newcommand{\opsGlvFourDbl}{127.8}
21 +\newcommand{\opsGlvFourSpeedup}{2.07}
22 +\newcommand{\opsGlvFiveTotal}{183.6}
23 +\newcommand{\opsGlvFiveAdd}{56.2}
24 +\newcommand{\opsGlvFiveDbl}{127.4}
25 +\newcommand{\opsGlvFiveSpeedup}{2.08}
26 +\newcommand{\glvKoneMax}{128}
27 +\newcommand{\glvKtwoMax}{128}
28 +\newcommand{\glvSamples}{2000}
29 +\newcommand{\opsGlvVsWnafSpeedup}{1.65}
30 +\newcommand{\icMinOracleRatio}{2.45}
31 +\newcommand{\icRateMin}{0.31}
32 +\newcommand{\icRateMax}{0.43}
33 +\newcommand{\ppRatioMin}{1.48}
34 +\newcommand{\ppRatioMax}{1.67}
35 +\newcommand{\ppRatioMean}{1.55}
36 +\newcommand{\ppSpeedupMax}{42}
37 +\newcommand{\ppInstances}{60}
38 +\newcommand{\idTested}{58\,064}
39 +\newcommand{\idFound}{22}
40 +\newcommand{\idUnexplained}{0}
41 +\newcommand{\idQfour}{0}
42 +\newcommand{\idQfive}{0}
43 +\newcommand{\orbitFails}{1500}
44 +\newcommand{\orbitTrials}{1500}
45 +\newcommand{\sigEcdsaSignKkUs}{15.3}
46 +\newcommand{\sigEcdsaSignLibsecpUs}{23.6}
47 +\newcommand{\sigEcdsaVerifyKkUs}{25.0}
48 +\newcommand{\sigEcdsaVerifyLibsecpUs}{12.6}
49 +\newcommand{\sigSchnorrSignKkUs}{11.6}
50 +\newcommand{\sigSchnorrSignLibsecpUs}{20.5}
51 +\newcommand{\sigSchnorrVerifyKkUs}{24.5}
52 +\newcommand{\sigSchnorrVerifyLibsecpUs}{13.2}
53 +\newcommand{\sigStudioEcdsaSignKkUs}{18.8}
54 +\newcommand{\sigStudioEcdsaSignLibsecpUs}{29.2}
55 +\newcommand{\sigStudioEcdsaVerifyKkUs}{33.5}
56 +\newcommand{\sigStudioEcdsaVerifyLibsecpUs}{16.7}
57 +\newcommand{\sigStudioSchnorrSignKkUs}{14.8}
58 +\newcommand{\sigStudioSchnorrSignLibsecpUs}{24.7}
59 +\newcommand{\sigStudioSchnorrVerifyKkUs}{32.5}
60 +\newcommand{\sigStudioSchnorrVerifyLibsecpUs}{16.9}
61 +\newcommand{\batchBestSize}{2048}
62 +\newcommand{\batchBestUs}{7.9}
63 +\newcommand{\batchSpeedupK}{3.10}
64 +\newcommand{\batchSpeedupKLo}{3.05}
65 +\newcommand{\batchSpeedupKHi}{3.16}
66 +\newcommand{\batchSpeedupLib}{1.67}
67 +\newcommand{\sigPurePythonSchnorrVerifyMs}{7.4}
68 +\newcommand{\sigPurePythonVsLibsecp}{562}
added paper/tables/numbers_ecdlp.tex +142 −0
@@ -0,0 +1,142 @@
1 +% generated by experiments/synthetic_ecdlp/analyze_ecdlp.py
2 +\newcommand{\alphabsgs}{0.471}
3 +\newcommand{\alphaLobsgs}{0.445}
4 +\newcommand{\alphaHibsgs}{0.496}
5 +\newcommand{\alphaGbsgs}{0.500}
6 +\newcommand{\alphaGLobsgs}{0.495}
7 +\newcommand{\alphaGHibsgs}{0.506}
8 +\newcommand{\alphakangaroo}{0.493}
9 +\newcommand{\alphaLokangaroo}{0.487}
10 +\newcommand{\alphaHikangaroo}{0.499}
11 +\newcommand{\alphaGkangaroo}{0.498}
12 +\newcommand{\alphaGLokangaroo}{0.487}
13 +\newcommand{\alphaGHikangaroo}{0.509}
14 +\newcommand{\alpharhobrent}{0.482}
15 +\newcommand{\alphaLorhobrent}{0.467}
16 +\newcommand{\alphaHirhobrent}{0.500}
17 +\newcommand{\alphaGrhobrent}{0.500}
18 +\newcommand{\alphaGLorhobrent}{0.491}
19 +\newcommand{\alphaGHirhobrent}{0.511}
20 +\newcommand{\alpharhodp}{0.450}
21 +\newcommand{\alphaLorhodp}{0.425}
22 +\newcommand{\alphaHirhodp}{0.479}
23 +\newcommand{\alphaGrhodp}{0.478}
24 +\newcommand{\alphaGLorhodp}{0.463}
25 +\newcommand{\alphaGHirhodp}{0.495}
26 +\newcommand{\alpharhodpneg}{0.438}
27 +\newcommand{\alphaLorhodpneg}{0.404}
28 +\newcommand{\alphaHirhodpneg}{0.478}
29 +\newcommand{\alphaGrhodpneg}{0.480}
30 +\newcommand{\alphaGLorhodpneg}{0.469}
31 +\newcommand{\alphaGHirhodpneg}{0.498}
32 +\newcommand{\alpharhodprbig}{0.445}
33 +\newcommand{\alphaLorhodprbig}{0.413}
34 +\newcommand{\alphaHirhodprbig}{0.483}
35 +\newcommand{\alphaGrhodprbig}{0.483}
36 +\newcommand{\alphaGLorhodprbig}{0.463}
37 +\newcommand{\alphaGHirhodprbig}{0.506}
38 +\newcommand{\alpharhofloyd}{0.489}
39 +\newcommand{\alphaLorhofloyd}{0.480}
40 +\newcommand{\alphaHirhofloyd}{0.504}
41 +\newcommand{\alphaGrhofloyd}{0.492}
42 +\newcommand{\alphaGLorhofloyd}{0.483}
43 +\newcommand{\alphaGHirhofloyd}{0.502}
44 +\newcommand{\alphaGMin}{0.478}
45 +\newcommand{\alphaGMax}{0.500}
46 +\newcommand{\scalingMaxBits}{56}
47 +\newcommand{\scalingInstances}{60}
48 +\newcommand{\rhoDpConstBig}{1.31}
49 +\newcommand{\rhoDpIterConstBig}{1.30}
50 +\newcommand{\rhoNegIterConstBig}{0.82}
51 +\newcommand{\bsgsConstBig}{1.47}
52 +\newcommand{\kangarooConstBig}{2.12}
53 +\newcommand{\floydConstBig}{3.00}
54 +\newcommand{\brentConstBig}{1.93}
55 +\newcommand{\scalingAllOk}{all}
56 +\newcommand{\scalingHost}{Apple M3 Ultra}
57 +\newcommand{\negSpeedupBig}{1.68}
58 +\newcommand{\negSpeedupBigLo}{1.37}
59 +\newcommand{\negSpeedupBigHi}{2.06}
60 +\newcommand{\pooledMinBits}{44}
61 +\newcommand{\pooledN}{240}
62 +\newcommand{\rhoConstPooled}{1.254}
63 +\newcommand{\rhoConstPooledLo}{1.195}
64 +\newcommand{\rhoConstPooledHi}{1.313}
65 +\newcommand{\rhoConstPooledN}{480}
66 +\newcommand{\negConstPooled}{0.925}
67 +\newcommand{\negConstPooledLo}{0.865}
68 +\newcommand{\negConstPooledHi}{0.986}
69 +\newcommand{\bsgsConstPooled}{1.500}
70 +\newcommand{\bsgsConstPooledLo}{1.463}
71 +\newcommand{\bsgsConstPooledHi}{1.539}
72 +\newcommand{\kangConstPooled}{1.938}
73 +\newcommand{\kangConstPooledLo}{1.827}
74 +\newcommand{\kangConstPooledHi}{2.049}
75 +\newcommand{\negSpeedupPooled}{1.35}
76 +\newcommand{\negSpeedupPooledLo}{1.23}
77 +\newcommand{\negSpeedupPooledHi}{1.48}
78 +\newcommand{\jzeroNegSpeedup}{1.29}
79 +\newcommand{\jzeroNegSpeedupLo}{1.16}
80 +\newcommand{\jzeroNegSpeedupHi}{1.44}
81 +\newcommand{\jzeroNegWall}{0.73}
82 +\newcommand{\jzeroNegWallLo}{0.65}
83 +\newcommand{\jzeroNegWallHi}{0.83}
84 +\newcommand{\jzeroNegNsIter}{56}
85 +\newcommand{\jzeroNegConst}{0.933}
86 +\newcommand{\jzeroAutSpeedup}{1.51}
87 +\newcommand{\jzeroAutSpeedupLo}{1.38}
88 +\newcommand{\jzeroAutSpeedupHi}{1.67}
89 +\newcommand{\jzeroAutWall}{0.75}
90 +\newcommand{\jzeroAutWallLo}{0.65}
91 +\newcommand{\jzeroAutWallHi}{0.86}
92 +\newcommand{\jzeroAutNsIter}{71}
93 +\newcommand{\jzeroAutConst}{0.797}
94 +\newcommand{\jzeroNegAutSpeedup}{2.21}
95 +\newcommand{\jzeroNegAutSpeedupLo}{2.00}
96 +\newcommand{\jzeroNegAutSpeedupHi}{2.45}
97 +\newcommand{\jzeroNegAutWall}{0.95}
98 +\newcommand{\jzeroNegAutWallLo}{0.84}
99 +\newcommand{\jzeroNegAutWallHi}{1.09}
100 +\newcommand{\jzeroNegAutNsIter}{80}
101 +\newcommand{\jzeroNegAutConst}{0.546}
102 +\newcommand{\jzeroNegAutRtwentySpeedup}{2.42}
103 +\newcommand{\jzeroNegAutRtwentySpeedupLo}{2.18}
104 +\newcommand{\jzeroNegAutRtwentySpeedupHi}{2.70}
105 +\newcommand{\jzeroNegAutRtwentyWall}{0.95}
106 +\newcommand{\jzeroNegAutRtwentyWallLo}{0.83}
107 +\newcommand{\jzeroNegAutRtwentyWallHi}{1.08}
108 +\newcommand{\jzeroNegAutRtwentyNsIter}{83}
109 +\newcommand{\jzeroNegAutRtwentyConst}{0.499}
110 +\newcommand{\jzeroPlainNsIter}{38}
111 +\newcommand{\jzeroPlainConst}{1.206}
112 +\newcommand{\jzeroPooledN}{180}
113 +\newcommand{\jzeroPooledMinBits}{44}
114 +\newcommand{\jzeroMaxBits}{52}
115 +\newcommand{\jzeroHost}{Apple M2 Ultra}
116 +\newcommand{\jzeroAllOk}{all}
117 +\newcommand{\jzeroFruitlessPerK}{0.00}
118 +\newcommand{\jzeroFruitlessPerKRtwenty}{0.11}
119 +\newcommand{\sweepBestCfg}{r32\_m0}
120 +\newcommand{\sweepBestConst}{1.095}
121 +\newcommand{\sweepBestLo}{0.921}
122 +\newcommand{\sweepBestHi}{1.284}
123 +\newcommand{\sweepRtwo}{3.77}
124 +\newcommand{\sweepRthree}{2.26}
125 +\newcommand{\sweepRtwenty}{1.531}
126 +\newcommand{\sweepRhundred}{1.366}
127 +\newcommand{\sweepConfigs}{25}
128 +\newcommand{\sweepInstances}{80}
129 +\newcommand{\parMaxThreads}{28}
130 +\newcommand{\parSpeedupMax}{22.1}
131 +\newcommand{\parEffMax}{0.79}
132 +\newcommand{\parSpeedupSixteen}{14.8}
133 +\newcommand{\parEffSixteen}{0.93}
134 +\newcommand{\parOpsRatioMax}{1.02}
135 +\newcommand{\parHost}{Apple M3 Ultra}
136 +\newcommand{\parCores}{28}
137 +\newcommand{\tmtoBsgsMinM}{2^{10}}
138 +\newcommand{\tmtoBsgsMaxM}{2^{20}}
139 +\newcommand{\tmtoBsgsMaxDev}{7.8}
140 +\newcommand{\tmtoRhoDpMin}{2}
141 +\newcommand{\tmtoRhoDpMax}{14}
142 +\newcommand{\tmtoRhoOpsRange}{1.17--4.95}
added paper/tables/preprocessing.tex +15 −0
@@ -0,0 +1,15 @@
1 +\resizebox{\textwidth}{!}{%
2 +\begin{tabular}{@{}rrrrrrrr@{}}
3 +\toprule Bits & $T$ & $W$ & Offline ops & Online ops (mean $\pm$ 95\% CI) & $\sqrt{n/T}$ & Ratio & Speed-up per instance \\ \midrule
4 +22 & 16 & 463 & 8\,077 & 687 $\pm$ 110 & 464 & 1.48 & 2.7$\times$ \\
5 +22 & 64 & 226 & 28\,781 & 376 $\pm$ 66 & 226 & 1.66 & 4.8$\times$ \\
6 +22 & 256 & 123 & 61\,532 & 195 $\pm$ 31 & 124 & 1.58 & 10.2$\times$ \\
7 +22 & 1024 & 55 & 160\,700 & 85 $\pm$ 14 & 55 & 1.54 & 20.8$\times$ \\
8 +22 & 4096 & 31 & 389\,668 & 48 $\pm$ 11 & 31 & 1.55 & 41.3$\times$ \\
9 +26 & 16 & 1600 & 51\,315 & 2429 $\pm$ 431 & 1601 & 1.52 & 2.6$\times$ \\
10 +26 & 64 & 866 & 79\,772 & 1305 $\pm$ 192 & 866 & 1.51 & 5.3$\times$ \\
11 +26 & 256 & 488 & 262\,246 & 724 $\pm$ 119 & 489 & 1.48 & 10.8$\times$ \\
12 +26 & 1024 & 226 & 480\,415 & 378 $\pm$ 63 & 226 & 1.67 & 19.2$\times$ \\
13 +26 & 4096 & 114 & 1\,083\,775 & 173 $\pm$ 34 & 114 & 1.51 & 42.3$\times$ \\
14 +\bottomrule
15 +\end{tabular}}
added paper/tables/scalar_mult_opcount.tex +10 −0
@@ -0,0 +1,10 @@
1 +\resizebox{\textwidth}{!}{%
2 +\begin{tabular}{@{}lrrrr@{}}
3 +\toprule Method & Additions & Doublings & Total & Speed-up vs.\ double-and-add \\ \midrule
4 +Double-and-add & 127.2 & 254.1 & 381.3 $\pm$ 8.2 & 1.00$\times$ \\
5 +wNAF, $w=4$ & 53.7 & 254.8 & 308.5 $\pm$ 2.9 & 1.24$\times$ \\
6 +wNAF, $w=5$ & 49.1 & 254.3 & 303.4 $\pm$ 3.0 & 1.26$\times$ \\
7 +GLV + interleaved wNAF, $w=4$ & 56.6 & 127.8 & 184.4 $\pm$ 2.7 & 2.07$\times$ \\
8 +GLV + interleaved wNAF, $w=5$ & 56.2 & 127.4 & 183.6 $\pm$ 2.4 & 2.08$\times$ \\
9 +\bottomrule
10 +\end{tabular}}
added paper/tables/sig_implementations.tex +13 −0
@@ -0,0 +1,13 @@
1 +\resizebox{\textwidth}{!}{%
2 +\begin{tabular}{@{}lrrrr@{}}
3 +\toprule Implementation & ECDSA sign & ECDSA verify & Schnorr sign & Schnorr verify \\ \midrule
4 +\libsecp{} (C, via rust-secp256k1) & 23.6 & 12.6 & 20.5 & 13.2 \\
5 +\kk{} (Rust, RustCrypto) & 15.3 & 25.0 & 11.6 & 24.5 \\
6 +\libsecp{}, Mac Studio M3 Ultra & 29.2 & 16.7 & 24.7 & 16.9 \\
7 +\kk{}, Mac Studio M3 Ultra & 18.8 & 33.5 & 14.8 & 32.5 \\
8 +\midrule
9 +pure Python reference (\code{src/wp34}, affine) & 3812.6 & 7237.1 & 14713.1 & 7417.2 \\
10 +python-ecdsa (pure Python, ECDSA only) & 118.0 & 419.4 & --- & --- \\
11 +coincurve (\libsecp{} via Python/CFFI) & 11.9 & 12.7 & 28.3 & 12.5 \\
12 +\bottomrule
13 +\end{tabular}}
added paper/uq_logo.jpg +0 −0

Binary file not shown.

added requirements.txt +15 −0
@@ -0,0 +1,15 @@
1 +sympy>=1.14
2 +numpy>=2.0
3 +scipy>=1.14
4 +matplotlib>=3.9
5 +pandas>=2.2
6 +gmpy2>=2.2
7 +z3-solver>=4.13
8 +python-sat>=1.8
9 +ecdsa>=0.19
10 +coincurve>=20
11 +statsmodels>=0.14
12 +networkx>=3.3
13 +pytest>=8
14 +tabulate
15 +pyyaml
added research_log/2026-11-01-ecdlp-studies.md +8 −0
@@ -0,0 +1,8 @@
1 +# 2026-11-01 — ECDLP studies (Axis A/B, H04, H08–H12)
2 +
3 +- Engine: `experiments/synthetic_ecdlp/rho_engine` (Rust, built by a sub-agent; 8 cargo tests, 553 pytest fixtures). Driver `ecdlp_study.py` generates instances with PARI/GP (laptop), `run_batch_remote.py` runs K engine processes on a node and records provenance there.
4 +- Pilot (20 instances/size) on M3U96a + M2U64 showed noisy class speed-ups (CV ≈ 0.52 per instance); final runs use 60 instances/size (3 840 generic jobs on M3U96a in ≈ 5 min with 26 processes; 2 400 j=0 jobs on M2U64 in 56 s). Pilot files kept in `results/synthetic_ecdlp/pilot20/`.
5 +- Bug found: `memory` must be a decimal string in the batch format (first tmto run failed; fixed and rerun).
6 +- Parallel study moved from 48-bit (runs of 0.03 s at 28 threads, start-up dominated, efficiency 0.72) to 56-bit instances (8.8 s single-thread): 22.1× on 28 threads.
7 +- Findings: rho constant 1.254·√n (theory 1.2533); α ∈ [0.478, 0.500]; order-6 orbits 2.42× in iterations (√6 = 2.449) but ≈ 1.0× in wall time in this engine (canonicalisation cost); negation alone 0.73× wall; BSGS 1.5·√n because the x-keyed table resolves ±j; DP-rho time–memory curve two-sided (restart cost vs overshoot).
8 +- Classification: H04/H08/H10/H11 known_reproduced; H09 rejected shortcut (no iteration design beats the random-mapping constant); H12 known_reproduced (Python).
added research_log/2026-11-01-formal.md +71 −0
@@ -0,0 +1,71 @@
1 +# 2026-11-01 — Stage 5: formal verification (Lean 4.35.0, core only, `bv_decide`)
2 +
3 +Host: Apple M5 Max laptop, macOS 27, elan toolchain `leanprover/lean4:v4.35.0`
4 +(Lean commit `eb6cafb7`, Lake 5.0.0). Project: `formal/` (library `Wp34`, no Mathlib).
5 +Command: `cd formal && lake clean && lake build` → exit 0, **32 s wall** (log: `formal/build.log`).
6 +Gate: `tests/test_formal_lean.py` (11 passed with `WP34_LEAN_BUILD=1`).
7 +
8 +## What was proven (all `theorem`, no `sorry`, no user `axiom`; see `formal/README.md` for the table)
9 +
10 +1. **SHA-256 word identities** (`Wp34/Sha256Words.lean`, 2.5 s): optimised `Ch`/`Maj` forms;
11 + GF(2)-linearity of Σ0, Σ1, σ0, σ1; injectivity of all four by `bv_decide`; **explicit inverses**
12 + (Σ0⁻¹, Σ1⁻¹ = XOR of 17 rotations each; σ0⁻¹, σ1⁻¹ = 32×32 GF(2) matrices computed by
13 + `formal/scripts/gf2_rank.py`), hence bijectivity of all four.
14 + *Finding that contradicts the brief:* σ0 and σ1 **are** bijective on 32 bits (rank 32, trivial
15 + kernel, no counter-example — the brief anticipated non-bijectivity and a counter-example). The
16 + controls `shr3_not_injective` and `sigma0_rotations_not_injective` show that each of the two
17 + summands of σ0 is singular while their sum is a unit. This is a known property of SHA-256.
18 +2. **Step bijection** (`Wp34/Sha256Step.lean`, 27 s): `stepInv (step s k w) k w = s` and the
19 + converse, plus the iterated version (`steps_bijective`: any number of rounds with known
20 + `(K_t, W_t)` is a bijection of the 256-bit state).
21 +3. **Message schedule** (`Wp34/Schedule.lean`): `W_{t−16} = W_t − σ1(W_{t−2}) − W_{t−7} − σ0(W_{t−15})`
22 + both as a one-liner on `scheduleStep` and on the well-founded `schedule` definition.
23 +4. **9-step linearised local collision** (`Wp34/LocalCollision.lean`): for two explicit
24 + linearisations of Ch/Maj (`absorb`: Ch = Maj = 0, Gilbert–Handschuh-style absorbing conditions;
25 + `xor3`: Ch = Maj = x⊕y⊕z, Chabaud–Joux-style), the full-value statement
26 + `lin9 f s K W = lin9 f s K (W ⊕ corr δ)` for **all** `s, K, W, δ` (864 bits), obtained from the
27 + difference-only version by the proven linearity of `lin9`. Correction vectors:
28 + `absorb`: (δ, Σ1δ⊕Σ0δ, Σ1Σ0δ, 0, δ, Σ0δ⊕Σ1δ, 0, 0, δ). The brief's sketch δW_{i+1} = Σ1(δ) is
29 + incomplete (the Σ0(δ) term coming from the `a` register must be cancelled too).
30 + Negative control, machine-checked by `decide`: the same corrections are **not** a collision of the
31 + real modular step (zero state, δ = 1); `scripts/local_collision.py` sees 0/20 000 random hits.
32 +5. **Abstract signatures** (`Wp34/Group.lean`): axiomatised `CyclicGroup G` (all fields listed in
33 + the README); Schnorr correctness `s•G = R + e•P` for `s = (k + e·d) mod n`; ECDSA correctness;
34 + **ECDSA malleability** `(r, s) ⟹ (r, n − s)` with the modular inverse as an explicit witness and
35 + `Nat` modular arithmetic proven from core lemmas (`sub_mul_sub_mod`).
36 +
37 +Trust base: Lean kernel + Lean compiler (the LRAT certificate checker runs natively; Lean 4.35
38 +records each such use as an axiom `<thm>._native.bv_decide.ax_…`, listed by `Wp34/Audit.lean` in
39 +`build.log`). CaDiCaL itself is not trusted. Non-SAT theorems (`rotr_xor`, all of `Group.lean`, the
40 +`decide` counter-example) depend only on `propext`/`Quot.sound`/`Classical.choice`.
41 +
42 +## What failed and why
43 +
44 +* Direct `bv_decide` on every **pure-parity** goal timed out (10 s default; 240 s + `acNf` for
45 + `Sigma0Inv_Sigma0`): inverse compositions (51 XOR terms per bit), linearity of the 17-rotation
46 + inverses, the 864-bit local-collision statements, the `xor3` difference version, `corrXor3_xor`.
47 + CDCL solvers have no XOR reasoning; these identities need Gaussian elimination, which we replaced
48 + by rewriting with proven linearity lemmas (`rotr_xor`, `Sigma0_xor`, `linStep_xor_*`) and
49 + `ac_rfl`, plus SAT only on byte-restricted instances (8 free variables).
50 +* The direct 320-bit step-bijection goal timed out; `bv_omega` on the abstracted goal did not
51 + terminate (killed at 35 CPU-min). Resolution: abstract Σ0+Maj, Σ1, Ch and prove the two
52 + addition-chain lemmas (`add4_sub4`, `sub4_add4`, ~2 s each) by SAT — the "per-component split"
53 + the mandate anticipated.
54 +* 16-bit SAT splits produced ≈ 100 MB LRAT certificates each (`Sha256Words.olean` reached 1.3 GB);
55 + byte splits bring it to 31 MB. `.lake/` is ≈ 470 MB after a clean build (certificates live in
56 + the oleans; `Sha256Step.olean` 150 MB).
57 +* The Python→Lean handoff had no failures: every rank/inverse/correction vector computed in
58 + Python was confirmed by `bv_decide` or `decide` as stated.
59 +
60 +## Classification
61 +
62 +All items: **`known_reproduced`** (none of these statements is new mathematics; the contribution is
63 +the machine-checked status, the explicit sigma inverses, and the tractability map for `bv_decide`).
64 +No conjecture is represented as a theorem: the Gilbert–Handschuh probability estimate and the
65 +historical attributions are confined to comments and the README prose.
66 +
67 +## Artefacts
68 +
69 +`formal/` (Lake project, `build.log`, `README.md`, `scripts/{gf2_rank,local_collision,aig_sizes}.py`,
70 +`scripts/aig_sizes.tsv`), `tests/test_formal_lean.py`. Note: the repository `.gitignore` ignores
71 +`*.log`, so `formal/build.log` needs a `!formal/build.log` exception to be committed.
added research_log/2026-11-01-literature.md +48 −0
@@ -0,0 +1,48 @@
1 +# 2026-11-01 — Stage 1: literature extraction
2 +
3 +**Scope.** Mandate section 7, Stage 1 (structured research database) and Phase I of section 17
4 +(literature database, research-gap report). Files written: `paper/references.bib`,
5 +`literature/db.yaml`, `literature/records.md`, `literature/research_gaps.md`, `literature/unverified.md`.
6 +
7 +**Method.**
8 +1. Built a polite Crossref query helper (`query.bibliographic`, serial requests, retry on HTTP 429,
9 + on-disk cache) and ran four batches covering the mandate's required coverage (ECC foundations,
10 + ECDLP algorithms and lower bounds, signatures and side channels, SHA-2 cryptanalysis, SAT/formal
11 + tools, automated discovery). Each entry's title, authors, year, venue, volume/pages and DOI were
12 + taken from the Crossref record or the DOI landing record.
13 +2. Items without a Crossref record were verified through IACR ePrint pages (2004/031, 2011/187,
14 + 2012/318, 2016/382, 2020/014, 2026/232, 2024/349, 2015/350, 2014/368, 2015/143, 2017/1113,
15 + 2008/131, 2008/270, 2014/302, 2016/374, 2009/541), the arXiv API (2406.20072, 2408.06292,
16 + 2506.13131), official standard URLs (SEC 1 v2.0 and SEC 2 v2.0 title pages, FIPS 180-4 PDF,
17 + FIPS 186-5 DOI, RFC 6979, BIP 340/341 headers from the bitcoin/bips repository, bitcoin.pdf,
18 + SafeCurves, libsecp256k1 `src/scalar_impl.h`), the CEUR-WS Vol-3717 index, the JKU record of the
19 + SAT Competition 2020 description, the Certicom ECC Challenge PDF (solved-challenge tables) and
20 + web searches for the Satoh–Araki 1998 bibliographic data.
21 +3. Numeric claims used in `db.yaml` / `records.md` (step counts, complexities, hardware, dates) were
22 + read from abstracts (ePrint/arXiv) or from the Certicom tables, not from memory. Where a requested
23 + figure could not be found (the "117.35-bit" ECDLP, a post-2009 prime-field record, Blackburn–Murphy
24 + 1998) it was left out and documented in `unverified.md`.
25 +4. A scratch LaTeX build (`\nocite{*}`, `aer.bst`, pdflatex + bibtex + pdflatex ×2 in /tmp) was run
26 + to make sure the bibliography compiles without BibTeX errors or warnings; see the build note below.
27 +
28 +**Outcome.** 129 BibTeX entries, all verified (see the summary in the final report); 82 database
29 +items; one ECDLP-records table and one SHA-2 records table; a two-page gap report that lists which
30 +mandate claims are already settled (√6 endomorphism bound, no sub-exponential prime-field ECDLP,
31 +Schnorr/ECDSA proof structure, SHA-256 frontier 31/37/39 steps) and eight candidate directions D1–D8
32 +for Stage 2.
33 +
34 +**Corrections to the brief discovered during verification.** LadderLeak is by Aranha, Novaes,
35 +Takahashi, Tibouchi and Yarom (key kept as `aldaya2020ladderleak` at the coordinator's request);
36 +Lee–Cheon–Hong 2011 is ePrint 2011/187 with a different title; Cheon–Hong–Kim's J. Cryptology 2012
37 +paper is titled "Accelerating Pollard's rho algorithm on finite fields"; the 2016 FPGA record is
38 +sect113r2 with no "117.35-bit" figure; "Bailey et al. 2009" describes a planned attack on ECC2K-130
39 +that the Certicom document still lists as unsolved.
40 +
41 +**Next.** Stage 2 hypothesis generation from `research_gaps.md` section 4; the discovery log
42 +(`research_log/discovery_log.jsonl`) starts when the first hypothesis is evaluated.
43 +
44 +**Build note.** `/tmp/bibtest`: `pdflatex` + `bibtex` (BibTeX 0.99e, TeX Live 2026, `aer.bst`) +
45 +`pdflatex` ×2 with `\nocite{*}` produced 129 `\harvarditem` entries, BibTeX exit 0, no warnings, no
46 +undefined citations. Verification tally: 109 entries carry a DOI, 18 are URL-verified
47 +(ePrint/arXiv/standards/BIPs/software), 2 have neither (Satoh–Araki 1998, confirmed through citing
48 +sources; the SAT Competition 2020 description, confirmed through the JKU publication record).
added research_log/2026-11-01-rho-engine.md +123 −0
@@ -0,0 +1,123 @@
1 +# 2026-11-01 — Native ECDLP engine `rho_engine` (Research Axis A, synthetic instances)
2 +
3 +**Deliverable.** `experiments/synthetic_ecdlp/rho_engine/` (Rust 1.95 crate, library + binary), fixture
4 +`tests/fixtures/ecdlp_instances.json`, pytest `tests/test_rho_engine.py`. Documentation of the iteration
5 +functions, canonical representatives and cycle handling with references: `rho_engine/README.md`.
6 +
7 +## Design decisions
8 +
9 +1. **Field arithmetic.** p < 2^62 in Montgomery form (R = 2^64, u128 products, one conditional subtraction);
10 + extended-Euclid inversion on i64; Montgomery's simultaneous inversion for every batched phase. Rejected
11 + alternative: `u128 % p` per multiplication (compiles to `__umodti3`, ≈ 10× slower). Exact integer
12 + arithmetic everywhere; floats appear only in the `expected_iterations` figure and timers.
13 +2. **Affine points, per-thread counters** `{add, dbl, inv, mul}` with the same semantics as `wp34.ec.OpCounter`
14 + (1 inversion per add/dbl; 3 resp. 4 multiplications), plus the batch-inversion multiplications and the
15 + Montgomery conversions used by the canonical representatives, so the Rust `mul` is an upper bound.
16 +3. **Rho iteration.** Teske r-adding walk (r = 20 default), partition from a splitmix64 mix of the
17 + Montgomery x residue (multiply-high, no division); distinguished points from the low bits of the same mix.
18 + Walk starts ‖uG + vQ‖ via two 16×16 comb tables (≤ 31 additions, counted). vOW parallelism: `--walks`
19 + walks per thread advanced with one simultaneous inversion, shared `Mutex<HashMap>` of distinguished points,
20 + restart after each distinguished point, abandon after 64·2^dp steps. Floyd and Brent single-walk versions
21 + use one inversion per step (no batching possible on a sequential chain).
22 +4. **Negation map.** Literal rule `y = min(y, p − y)` on the standard form (one Montgomery conversion per
23 + step; a residue-ordering rule would save it but was not chosen, to keep the representative as specified).
24 + 2-cycles: Bos–Kleinjung–Lenstra "next partition" look-ahead (f stays a function of P). Observation: this rule
25 + leaves residual 2-cycles at rate ≈ 1/(2r³) per step (a second rejection from R re-uses the partition that
26 + produced R); measured 96 two-cycles / 26 649 steps at r = 4 and 1 / 6 569 at r = 20, consistent. They
27 + are caught by the periodic check (one remembered point every K = 32 steps, one comparison per step), as
28 + are the 4-, 6-, …-cycles (rate ≈ 1/(4r²)) and the 3-cycles of the order-3 automorphism. Escape by
29 + doubling the minimum point of the cycle (Bernstein–Lange–Schwabe 2011), restart if the same cycle is met
30 + twice. The expected overhead with r = 20 is visible: at 48 bits `--neg` with r = 20 produced 2 743
31 + fruitless cycles and 181 k look-ahead rejections for 3.3 M steps, versus 162 cycles / 86 k rejections for
32 + 10.9 M steps with r = 128 — use `--r 128` or more with `--neg`, as BLS recommend.
33 +5. **Order-3 automorphism.** `--aut3` requires β, λ from the caller (Python computes them) and verifies
34 + β³ = 1 (p), λ³ = 1 (n) and (β·gx, gy) = λ·G at start-up (wrong pairs abort with exit 2). Canonical x =
35 + min{x, βx, β²x} (standard form), scalars × λ^i; composed with the negation rule for the order-6 orbit.
36 +6. **Kangaroo.** vOW tame/wild herds with distinguished points, power-of-two jumps with mean ≈ N√w/4
37 + (Pollard 2000), positions kept mod n, same-herd merges and runaway kangaroos (travel > 3w + 4·m·2^dp)
38 + restart at a fresh random position. The runaway guard was added after a test on a 199-element group hung:
39 + the jump map's functional graph had disjoint cycles, so the herds never met.
40 +7. **Determinism.** Every RNG is ChaCha20 seeded from (`--seed`, thread index, purpose); 1-thread runs are
41 + bit-for-bit reproducible (tested). Multi-thread runs are reproducible only in distribution.
42 +8. **Memory / time.** Per-thread CPU time by `CLOCK_THREAD_CPUTIME_ID` (summed into `cpu_s`), wall-clock,
43 + `getrusage` peak RSS, hash-table capacities × entry size. The binary links only `libSystem`/`libiconv`
44 + (arm64 Mach-O, 880 kB), no OpenSSL, so it can be copied to the Mac Studio nodes.
45 +9. **No `rayon`.** `std::thread::scope` suffices (one worker per thread, shared tables behind a mutex);
46 + it avoids a dependency with no benefit for this access pattern.
47 +
48 +## Correctness tests (all passing, 2026-11-01)
49 +
50 +* `cargo test --release`: 8 tests — Montgomery arithmetic, batch inversion, Tonelli–Shanks, group law vs
51 + repeated addition; **small curves** (primes 101…32771, prime-order curves found by Legendre point counting,
52 + 1 052 (curve, d) pairs brute-forced, each solved by bsgs ×3 settings, rho ×8 settings (Floyd, Brent, dp 1/2
53 + threads, neg, neg+Brent r = 3, neg+mixed, mixed) and kangaroo ×3 interval/thread settings); **fixtures**
54 + (60 instances, 20–32 bits, 10 of them j = 0 per size, generated by `scripts/gen_fixtures.py` from
55 + `src/wp34/ec.py` + PARI/GP, seed 34): bsgs (default and `--memory 1000 --threads 2`), rho floyd / brent /
56 + dp 1 thread / dp 4 threads / neg / neg+brent / mixed 4, aut3 / neg+aut3 / neg+aut3 floyd / neg+aut3 4
57 + threads on j = 0 curves, kangaroo on [0, n), 4 threads, and on a tight interval; 1-thread determinism;
58 + wrong β / wrong λ rejected. Runtime 2.2 s.
59 +* `.venv/bin/python -m pytest tests/test_rho_engine.py`: **553 passed in 3.5 s** — the release binary on every
60 + fixture × {bsgs, rho floyd, rho brent, rho dp 1 thread, rho dp 4 threads, rho neg, rho mixed, kangaroo
61 + [0, n)} and × {aut3, neg+aut3, neg+aut3 brent} on the 20 j = 0 instances; each answer re-verified in pure
62 + Python (`Curve.mul(d, G) == Q`); determinism; batch mode; interval / memory overrides; wrong β abort.
63 +* Two bugs found and fixed by the small-curve tests: (i) Brent/Floyd meeting an earlier point of the
64 + *augmented* trajectory (after an escape) is not an f-cycle — `cycle_length` is now bounded and the walk
65 + restarts; (ii) the kangaroo runaway guard above.
66 +
67 +## Measured throughput (laptop, Apple M5 Max, 18 cores, macOS 27.0, rustc 1.95.0; single runs, seed 1 unless noted)
68 +
69 +Instances: `rho_engine/bench/instances.json` (40/48/56/62-bit prime-order curves, seed 3434); raw JSON of
70 +every run with host provenance: `rho_engine/bench/results-laptop-2026-11-01.json`. These are engine
71 +validation numbers (one run each), not the repeated-measurement scaling experiment, which belongs to the
72 +scaling driver.
73 +
74 +| instance | algorithm / options | threads | group ops | ops / CPU-s per thread | ops / wall-s | iterations / expected |
75 +|---|---|---|---|---|---|---|
76 +| 48b | rho dp (r 20, 128 walks), seeds 1–3 | 1 | 15.5–31.5 M | **45.8 M** | 45.6 M | 0.97 / 1.58 / 1.97 |
77 +| 48b | rho dp `--neg --r 128` | 1 | 11.2 M | 30.7 M | 30.6 M | 0.99 |
78 +| 48b | rho dp `--neg --r 512` | 1 | 8.1 M | 30.6 M | 30.3 M | 0.72 |
79 +| 48b | kangaroo [0, n) | 1 | 28.4 M | 48.9 M | 48.3 M | 1.14 |
80 +| 48b | bsgs (m = 2^24, 288 MB RSS) | 1 | 24.8 M | 41.1 M | 39.2 M | — |
81 +| 48b | rho Brent (single walk, 1 inversion/step) | 1 | 33.9 M | 9.0 M | 8.9 M | 2.17 |
82 +| 56b | rho dp | 1 | 126 M | **48.5 M** | 48.4 M | 0.44 |
83 +| 56b | rho dp `--neg --r 128` | 1 | 189 M | 30.5 M | 30.1 M | 0.91 |
84 +| 56b | kangaroo [0, n) | 1 | 757 M | 48.4 M | 47.8 M | 1.65 |
85 +| 56b | rho dp | 2 / 4 / 8 / 12 / 16 | 253 / 503 / 438 / 466 / 620 M | 45.9 / 45.5 / 41.8 / 41.0 / 40.5 M | 89 / 178 / 327 / 483 / **613 M** | 0.87–2.14 |
86 +| 62b | rho dp | 16 | 1.82 G | 40.3 M | 608 M | 0.86 (wall 3.0 s) |
87 +| 62b | rho dp `--neg --r 128` | 16 | 1.45 G | 26.8 M | 397 M | 0.96 (wall 3.6 s) |
88 +| 62b-j0 | rho dp `--neg --aut3 --r 128` | 16 | 0.70 G | 18.2 M | 275 M | 0.76 (wall 2.6 s) |
89 +| 62b | kangaroo [0, n) | 16 | 4.61 G | 39.1 M | 580 M | 1.37 (wall 8.0 s) |
90 +
91 +40-bit (runs of 25–60 ms, dominated by start-up): rho 36 M, `--neg` 25.5 M, `--neg --aut3` 19.4 M,
92 +kangaroo 46.6 M, bsgs 38.8 M ops / CPU-s per thread; 16 threads 330 M ops / wall-s.
93 +
94 +* **Target (≥ 20 M group additions / s / thread on a 40-bit curve): met** for every mode except the
95 + order-6 class (`--neg --aut3`, 18–19 M/s), whose literal canonical rule costs 2 extra multiplications and
96 + 4 extra Montgomery conversions per step; it still *solves* faster per unit time than `--neg` alone
97 + because of the √3 reduction in iterations (62b-j0: 2.6 s vs 3.6 s wall).
98 +* Per-thread rate falls from 48 M (1 thread) to 40 M (16 threads): the M5 Max has performance and
99 + efficiency cores and the DP table mutex is taken once per batch; wall-clock scaling 1 → 16 threads is
100 + 12.6× on the 56-bit instance.
101 +* The iterations/expected ratios (0.3–2.2 on single runs) are what the √(πn/2) heavy-tailed distribution
102 + predicts; the scaling study must use ≥ 10 seeds per point (CONVENTIONS.md §4).
103 +* Kangaroo on the full interval costs ≈ 2√n vs ≈ 1.25√n for rho, as expected; its per-step cost is lower
104 + (no canonicalisation, no restarts).
105 +
106 +## Usage for the scaling driver
107 +
108 +```
109 +BIN=experiments/synthetic_ecdlp/rho_engine/target/release/rho_engine
110 +$BIN solve --p P --a A --b B --n N --gx GX --gy GY --qx QX --qy QY --alg rho --neg --r 128 --dp 10 \
111 + --threads 16 --seed 7 --expected-d D --json
112 +$BIN batch --file jobs.json --out results.json --verbose # jobs: JSON array with the same keys
113 +```
114 +Each result carries `d, ok, ops{add,dbl,inv,mul}, iterations, dp_count, dp_bytes, fruitless_cycles,
115 +threads, wall_s, cpu_s, table_entries, peak_rss_bytes, params` (big integers as decimal strings).
116 +
117 +## Classification
118 +
119 +Engine construction and validation — `known_reproduced` (BSGS, Pollard rho with r-adding walks, vOW
120 +distinguished points, negation and order-3/6 classes with fruitless-cycle handling, Pollard kangaroo); no
121 +hypothesis evaluated yet. Open items for later sessions: a residue-ordering variant of the canonical
122 +representatives (speed), Monte-Carlo estimate of the constant in front of √n for each class size with ≥ 10
123 +seeds per point, and the negative control (random mapping) for the rho family.
added research_log/2026-11-01-sha256.md +94 −0
@@ -0,0 +1,94 @@
1 +# 2026-11-01 — Axis C (SHA-256): diffusion, statistical regularity, SAT/SMT preimage & collision scaling, 9-step local collision
2 +
3 +Scripts: `experiments/reduced_round_sha256/` (README there lists commands, hosts and durations). Results: `results/reduced_round_sha256/`
4 +(`summary.json` aggregates the numbers quoted below). Figures: `figures/sha256_*.pdf` (regenerate with
5 +`figures/make_sha256_figures.py`). Reference implementation: `src/wp34/sha256.py` (untouched; every SAT/SMT answer and the
6 +vectorised step are checked against it).
7 +
8 +## 1. Diffusion / avalanche (C1) — `diffusion.py`, laptop (M5 Max), 3 seeds x N = 2000, ~12 s per seed
9 +
10 +Single-bit difference, state tracked after every step without feed-forward. Mean flip ratio reaches 0.49 ("full
11 +diffusion") at step **6** for a bit of W_0 (0.015, 0.097, 0.217, 0.341, 0.451, 0.494, 0.499 ...), step **11** for W_5
12 +(= 6 steps after the word enters), step **21** for W_15 (6 steps after it enters at step 16), and step **8** for an IV bit
13 +(0.018, 0.072, 0.162, 0.271, 0.378, 0.448, 0.483, 0.497). Identical across the three seeds. After 64 steps the Hamming
14 +weight of the difference is 128.0 +/- 8.0 (sd), indistinguishable from the random-oracle control (flip ratio 0.500,
15 +sd 0.031 = 0.5/sqrt(256)). The heatmap (fig. `sha256_diffusion.pdf`) shows the familiar structure: the word `a`/`e`
16 +registers flip first, `b,c,d`/`f,g,h` lag by one step each (register shift), hence the staircase on the bit axis.
17 +Classification: **known_reproduced** (textbook avalanche behaviour; no anomaly). A per-step avalanche of
18 +~6 steps per injected word is the quantitative figure the paper can quote.
19 +
20 +## 2. Statistical regularity (C1) — `bias_test.py`, laptop, 3 seeds x N = 20 000, ~8 s per seed
21 +
22 +Output = r-step compression with feed-forward, standard IV. Thresholds: bit bias 4 sigma = 0.0141, correlation
23 +4/sqrt(N) = 0.0283, chi-square on 32 output bytes with Bonferroni alpha = 0.01/32; expected number of
24 +(input bit, output bit) pairs above 4 sigma under uniformity = 1.04 (64 x 256 pairs).
25 +
26 +| r | bits with bias > 4 sigma (3 seeds) | pairs with corr > 4 sigma | bytes rejected (chi2) |
27 +|---|---|---|---|
28 +| 1 | 192 / 192 / 192 | 13 / 5 / 10 (max corr 1.0) | 24 / 24 / 24 |
29 +| 2 | 128 | 12 / 7 / 10 (1.0) | 16 |
30 +| 3 | 64 | 13 / 5 / 10 (1.0) | 8 |
31 +| 4 | 0 | 13 / 6 / 10 (**1.0**) | 0 |
32 +| 5 | 0 | 0 / 0 / 0 (0.028) | 0 |
33 +| 6–64 | 0 (one 4.1-sigma bit at r = 24 seed 2 and one at r = 64 seed 1) | 0–3 per seed | 0 |
34 +| random oracle | 0 | 1 / 0 / 0 | 0 |
35 +
36 +Reading: for r <= 3 whole output words are IV words (untouched registers), hence 64 x (4 - r) biased bits. At r = 4
37 +every register has been written once, bit biases and byte chi-square are already at noise level, **but** 6–13 pairs
38 +still have |corr| = 1: with a fixed IV the LSB of e_1 = d + T1 is an affine function of the LSB of W_0 (no carry into
39 +bit 0), and e_1 is still exposed in register h after 4 steps. From r = 5 on, every statistic is at the level of the
40 +random-oracle control (0–3 pairs above 4 sigma, max |corr| about 0.03, no chi-square rejection). The isolated 4-sigma bits
41 +at r = 24 and r = 64 are what one expects from 14 x 256 x 3 = 10 752 tests (expected 0.7). Classification:
42 +**known_reproduced / empirical_observation** — no regularity of the full compression beyond 4 steps is detectable with
43 +N = 20 000 single-bit statistics; this is a sanity floor for anything claimed later, not evidence of structure.
44 +
45 +## 3. SAT/SMT (C2, C4) — `cnf_sha256.py`, `preimage_sat.py`, `collision_sat.py`, run on M3U96b (M3 Ultra, 28 c)
46 +
47 +Encoder: Tseitin, constants folded, modular addition = ripple-carry (14 clauses/bit, chained for multi-operand sums);
48 +64-step compression = 70 801 variables / 313 792 clauses (ff adds 480 vars). Unit test `tests/test_sha256_cnf.py`
49 +(r in {1,4,16,64}, 3 messages, with and without feed-forward, plus a free-IV case) passes. One bug found and fixed
50 +during development: a gate-folding rule compared a DIMACS literal to a Python bool (`1 == True`), which silently
51 +replaced Ch(e, x_1, True) by x_1 — caught by the self-test (3 of 24 mismatches), not by inspection. A second issue:
52 +pysat's `interrupt()` does **not** stop CaDiCaL 1.9.5 (a 5 s limit ran for 10+ minutes), so CaDiCaL is run as the
53 +standalone 3.0.1 binary with `-t`, Kissat 4.0.4 with `--time`, both as subprocesses (one thread each); the driver has a
54 +kill guard at timeout + 120 s.
55 +
56 +SAT-SCALING-PLACEHOLDER
57 +
58 +## 4. 9-step local collision (C3, C4) — `local_collision.py`, laptop, N = 100 000 per bit, 943 s + 2nd run
59 +
60 +Linearised model: + -> XOR and Boolean functions absorb all differences; the propagation is then GF(2)-linear,
61 +dstate' = A dstate + B dW with A (256 x 256) built from the Sigma matrices and the register shifts. Derived pattern for a
62 +perturbation d in W_i (docstring of the script has the step-by-step derivation):
63 +(d, S0(d)^S1(d), S1(S0(d)), 0, d, S0(d)^S1(d), 0, 0, d) — the a-register carries a difference only at step i+1, the
64 +e-register at i+1 (d), i+2 (S0(d)) and i+5 (d). Verified by matrix arithmetic for all 32 single-bit d and 20 random
65 +multi-bit d (the map is linear so the 32 basis checks are exhaustive); dropping any correction leaves a nonzero
66 +difference (`tests/test_sha256_local_collision.py`). For d = 2^31 the pattern is
67 +(80000000, 22140240, 42851098, 0, 80000000, 22140240, 0, 0, 80000000).
68 +
69 +Real (modular) step, random state and words, XOR pattern applied blindly: **0 collisions in 32 x 100 000 samples**
70 +(rule-of-three upper bound 3e-5 per position; second seed, bits 31 and 0: also 0). The survival of the intended
71 +difference after step i is 0.25 for bits 0..30 and **1.0 for bit 31**: at step i only W_i differs, so the modular
72 +difference of T1 is +/-2^j and the XOR difference of a' = T1 + T2 and of e' = d + T1 equals 2^j iff no carry
73 +propagates in each of the two sums (probability 1/2 each, 1/4 overall); for the MSB no carry is possible, so the
74 +first step always conforms. After step i+1 survival drops to
75 +3e-5 – 5.4e-4 (the 6-bit correction S0(d)^S1(d) must be absorbed through three additions and two Boolean functions), and
76 +after step i+2 it is 0 in every sample. Classification: **known_reproduced** (the pattern holds only under message /
77 +state conditions; the raw probability of the XOR characteristic is far below 2^-17 per step i+1 alone).
78 +
79 +SAT (CaDiCaL binary, free 256-bit state and free W_i..W_{i+8}, W' = W ^ pattern, equal state after 9 steps,
80 +15 218 vars / 66 208 clauses, K_0..K_8): a conforming pair was found and verified with the reference for **all 32 bit
81 +positions**, in 4.4 s (bit 29) to 118 s (bit 0); bit 31 in 5.1 s / 328 k conflicts (second seed 9.0 s), bit 0 in
82 +118 s / 7.5 M conflicts (second seed 52 s / 3.3 M). Example for d = 2^31: state 3ffffe31 7ff49fa2 67ff13c8 ffe19d9b
83 +dffae8db ff73ee77 ff7dfdfe b91789e0, W = 88f6da84 dd518e9d 094ddd6f 3e9f5b2d ae8d60e9 ffeff1bf b652c097 b7f66631 80000000.
84 +Classification: **known_reproduced** (existence of conforming pairs for the GH local collision under conditions —
85 +Gilbert–Handschuh 2003, Mendel et al. 2006 — reproduced by a generic SAT solver with no hand-made conditions).
86 +
87 +## 5. Honest summary
88 +
89 +* Nothing here touches the security of full SHA-256: every solved instance is either <= 24 steps or has <= 16 free
90 + bits; 64-step preimage cells with >= 16 free bits all time out at 300 s, as expected.
91 +* The useful quantitative outputs for the paper are: 6 steps per injected word to full diffusion; statistics at the
92 + random-oracle floor from r = 5; the SAT frontier tables (section 3); UNSAT proofs for r <= 8 collisions; the
93 + 9-step local-collision pattern with its measured conformance probabilities and SAT-found conforming pairs.
94 +* Reductions actually applied to the plan: see README ("What was reduced").
added research_log/2026-11-01-signatures.md +131 −0
@@ -0,0 +1,131 @@
1 +# 2026-11-01 — Research Axis B: ECDSA / BIP340 Schnorr benchmarks (questions B2, B5)
2 +
3 +**Research question.** B2: can BIP340 verification be accelerated without changing consensus
4 +semantics (batch verification)? B5: which implementation choices produce measurable differences?
5 +**Null hypothesis (B2).** Batch verification gives no per-signature saving over single verification
6 +at any batch size. **Alternative.** The per-signature cost decreases with b and saturates.
7 +
8 +**Setup.** `experiments/signature_benchmarks/sigbench` (Rust 1.95/1.96, rust-secp256k1 0.33.1 →
9 +libsecp256k1 via secp256k1-sys 0.14.1, k256 0.14.0, elliptic-curve 0.14.3, ecdsa 0.17.0, wnaf 0.14.1),
10 +seed 34, 7 repetitions × ≥ 2000 operations per measurement, single thread, bootstrap CI with 2000
11 +resamples. Two hosts: laptop (Apple M5 Max, 18 cores, macOS 27.0) and cluster node M3U96a
12 +(Apple M3 Ultra, 28 cores, macOS 26.6). Results: `results/signature_benchmarks/bench_MacBookPro-de-simon-pierre-2.json`,
13 +`bench_MacStudnpierre2.json`, `summary.json`, logs `run_laptop.log`, `run_m3u96a.log`.
14 +Commands are stored in each JSON (`command`, `params.rust_command`). Synthetic keys only.
15 +
16 +## Correctness gates (both hosts, identical because the RNG is seeded)
17 +
18 +- BIP340 official vectors: 19/19 verification outcomes and 8/8 byte-identical signatures with
19 + **both** libsecp256k1 and k256 (vectors 5–14 with invalid keys / r ≥ p / s ≥ n / wrong parity all
20 + rejected as expected).
21 +- Cross-library: 1000/1000 k256-signed ECDSA signatures verified by libsecp256k1 and vice versa;
22 + same for Schnorr; **1000/1000 byte-identical** in both schemes (RFC 6979 nonces for ECDSA, the
23 + BIP340 aux-rand nonce derivation for Schnorr — deterministic by construction).
24 +- Python reference (`src/wp34`, exact integer arithmetic): 200/200 Rust signatures verified for all
25 + four (scheme × library) combinations, 200/200 re-signed identically in pure Python, 200/200 tampered
26 + signatures rejected. `tests/test_sig_crosscheck.py` passes (2 tests, also checks the Python
27 + reference against the 19 vectors independently of Rust).
28 +- Own MSM: Straus with one term agrees with k256 on 64 scalar multiplications; batch negative
29 + controls (s+1, flipped message bit, r+1 inside a batch of 64) rejected by all four MSM back-ends,
30 + while the unmodified batch is accepted.
31 +
32 +## Findings
33 +
34 +### F1 — Batch verification speed-up (classification: `known_reproduced`)
35 +
36 +Per-signature cost of Σ a_i s_i G = Σ a_i R_i + Σ a_i e_i P_i with a_1 = 1 and random 128-bit a_i, as a
37 +function of b (laptop; M3U96a shows the same shape at ×1.3 cost):
38 +
39 +| b | own Pippenger | own Straus (wNAF-5) | k256 lincomb_vartime | k256 lincomb (ct) |
40 +|---|---|---|---|---|
41 +| 1 | 91.9 µs (S 0.27) | 30.4 µs (S 0.81) | 25.1 µs (S 0.97) | 38.5 µs (S 0.64) |
42 +| 16 | 24.9 µs (0.99) | 12.9 µs (1.90) | 14.8 µs (1.65) | 22.5 µs (1.09) |
43 +| 128 | 12.8 µs (1.92) | 12.2 µs (2.01) | 15.3 µs (1.60) | 21.9 µs (1.12) |
44 +| 256 | 11.3 µs (2.16) | 12.4 µs (1.98) | 15.0 µs (1.63) | 21.8 µs (1.12) |
45 +| 1024 | 8.6 µs (2.84) | 12.7 µs (1.93) | 15.8 µs (1.55) | — |
46 +| 2048 | **7.9 µs (3.10 [3.05, 3.16])** | 13.4 µs (1.82) | 17.1 µs (1.43) | — |
47 +
48 +S = speed-up versus a single k256 `verify_raw` (24.5 µs); versus libsecp256k1's single verify
49 +(13.2 µs) the best point is **1.67 [1.65, 1.68]** (M3U96a: 3.03 [3.00, 3.04] and 1.58). The null
50 +hypothesis is rejected for every back-end at b ≥ 4 (CIs exclude 1); the gain saturates. This is the
51 +expected behaviour described in the BIP340 text ("batch verification ... up to a factor of 2") and
52 +measured for Ed25519 by Bernstein, Duif, Lange, Schwabe and Yang (2012); our numbers reproduce the
53 +known result in k256 arithmetic and do not constitute a new algorithm. Op counts (per signature,
54 +2b + 1 terms): Straus plateaus at ≈ 79 projective additions + 2 doublings per signature exactly as
55 +predicted by wNAF-5 density (≈ 256/6 + 7 table additions for the 256-bit P_i term, ≈ 128/6 + 7 for
56 +the 128-bit R_i term); Pippenger at b = 2048 (c = 8) uses 47.8 mixed additions + 8.0 additions +
57 +0.1 doublings per signature, i.e. about 0.7× the group operations of Straus, which is the whole
58 +source of its advantage.
59 +
60 +### F2 — The attainable speed-up is capped by the per-signature non-MSM work (`empirical_observation`)
61 +
62 +The MSM-only time falls to 4.9 µs/sig (laptop, b = 2048) but the total is 7.9 µs/sig: ≈ 3.0 µs per
63 +signature (3.6 µs on M3U96a) is spent outside the MSM — `lift_x(r)` (one square root = a 255-bit
64 +field exponentiation), the tagged SHA-256 challenge hash and the scalar parse. Single verification
65 +pays the same hash but no square root for R (it compares x-coordinates). Hence the asymptotic
66 +speed-up is bounded by (single verify) / (non-MSM floor + MSM/sig), roughly 4–5× in this arithmetic
67 +even with an ideal MSM — consistent with the saturation seen above. This is a well-understood cost
68 +model, reported here with measured constants.
69 +
70 +### F3 — Library choice matters more than batching (`empirical_observation`)
71 +
72 +- libsecp256k1 verifies ≈ 2× faster than k256 (ECDSA 12.6 vs 25.0 µs; Schnorr 13.2 vs 24.5 µs on the
73 + laptop), consistent with its 5×52-bit field representation, GLV + wNAF strategy with effective-affine
74 + precomputation and years of tuning. Conversely **k256 signs faster** than libsecp256k1 (ECDSA 15.3 vs
75 + 23.6 µs; Schnorr 11.6 vs 20.5 µs): rust-secp256k1's signing path goes through the blinded,
76 + constant-time `ecmult_gen` (19.8 µs for a generator multiplication through `PublicKey::from_secret_key`)
77 + whereas k256's `mul_by_generator` uses a 30 kB fixed table (8.3 µs; 6.6 µs in variable time). This
78 + was not anticipated and is worth stating in the paper as an implementation-choice effect, not a
79 + mathematical one.
80 +- Scalar multiplication (laptop): own wNAF-5 double-and-add without GLV 21.1 µs (257 doublings +
81 + 52 additions); k256 constant-time with GLV 16.4–17.2 µs; k256 variable-time GLV + wNAF 14.0 µs;
82 + libsecp256k1 `ecmult_const` 11.5 µs. The GLV endomorphism therefore buys ≈ 1.3–1.5× here, in line
83 + with Gallant–Lambert–Vanstone (2001). GLV is not switchable from either public API; this is stated
84 + in the JSON (`scalar_mult.glv_note`).
85 +- k256's own `lincomb_vartime` is slower than our naive Straus for b ≥ 8 and degrades for large b
86 + (17.1 µs/sig at b = 2048 vs 15.0 at b = 256); its per-term wNAF table precomputation (doubled by the
87 + GLV split) dominates when the terms are used once. Surprising at first sight, but it is a table-cost
88 + effect, not an arithmetic one.
89 +- Implementation spread for the same mathematics (Schnorr verify, laptop): pure Python `src/wp34`
90 + 7.4 ms → python-ecdsa (ECDSA) 419 µs → coincurve (CFFI) 12.5 µs → k256 24.5 µs → libsecp256k1 13.2 µs:
91 + a factor ≈ 560 between the pure-Python reference and C. coincurve adds ≈ 0–8 µs of FFI/object overhead
92 + (its `sign_schnorr` creates a keypair per call: 28.3 µs vs 20.5 µs in Rust).
93 +- Host effect: the M5 Max is 1.25–1.35× faster than the M3 Ultra on every single-threaded operation;
94 + the speed-up curves S(b) are the same within CI on both hosts, as expected for a ratio.
95 +
96 +### F4 — ECDSA vs Schnorr cost (`known_reproduced`)
97 +
98 +In libsecp256k1 the two verifications cost the same within 5 % (12.6 vs 13.2 µs): both are one
99 +double-scalar multiplication plus hashing; Schnorr's extra cost is the tagged hash and the x-only key
100 +lift, ECDSA's the modular inversion of s. Schnorr signing is cheaper than ECDSA signing in both
101 +libraries (no inversion). Nothing here contradicts the standard comparison (BIP340 motivation section).
102 +
103 +## Classification summary
104 +
105 +| finding | classification | note |
106 +|---|---|---|
107 +| batch verification speed-up (F1) | `known_reproduced` | BIP340 text; Bernstein et al. 2012 (Ed25519) |
108 +| speed-up cap from lift_x + hash floor (F2) | `empirical_observation` | measured constants, standard cost model |
109 +| library / implementation spread, k256 signs faster, libsecp256k1 verifies faster (F3) | `empirical_observation` | implementation, not mathematics |
110 +| ECDSA ≈ Schnorr verification cost (F4) | `known_reproduced` | |
111 +
112 +No algebraic shortcut was sought or found in this family; every measured gain is a constant factor
113 +from a known algorithm (Straus, Pippenger, GLV) and is **not** a cryptographic weakness.
114 +
115 +## Negative control and reproducibility
116 +
117 +- Negative controls: three corrupted batches (s+1, message bit flip, r+1) rejected by all back-ends;
118 + pure-Python `batch_verify` likewise rejects a flipped bit (`pure_python_batch_negative_control_rejected`).
119 +- Reproduce: `PYTHONPATH=src .venv/bin/python experiments/signature_benchmarks/run_bench.py --seed 34`
120 + (≈ 20 s Rust + 11 s Python cross-check + 35 s Python baselines on the laptop), then `summarize.py`,
121 + `figures/make_sig_figures.py`. The crosscheck triple files produced on both hosts are byte-identical.
122 +
123 +## Files produced today
124 +
125 +`experiments/signature_benchmarks/{sigbench/, run_bench.py, python_crosscheck.py, python_baselines.py, summarize.py, README.md}`,
126 +`results/signature_benchmarks/{bench_MacBookPro-de-simon-pierre-2.json, bench_MacStudnpierre2.json, summary.json, crosscheck_triples_*.json, run_*.log}`,
127 +`figures/{make_sig_figures.py, sig_batch_verification.pdf, sig_implementations.pdf}`, `tests/test_sig_crosscheck.py`.
128 +
129 +Open items for the paper: cite `bernstein2012highspeed`, `bip340`, `gallant2001glv`, `pippenger1976`,
130 +`straus1964` (verify keys exist in `paper/references.bib` before use); a hypothesis file for B2
131 +(`hypotheses/`) was not created here because this run was restricted to the directories above.
added research_log/discovery_log.jsonl +25 −0
@@ -0,0 +1,25 @@
1 +{"id": "H01", "title": "Standardised secp256k1 parameters and GLV constants are exactly consistent", "axis": "A", "kind": "algebraic", "status": "evaluated", "classification": "known_reproduced", "verdict": "All 14 facts agree between Python and PARI/GP; (\u03b2, \u03bb\u00b2) mismatch confirmed as expected.", "evidence": ["results/baselines/secp256k1_facts.json"], "logged_utc": "2026-11-01T07:04:09+00:00"}
2 +{"id": "H02", "title": "End(E) = Z[\u03c9] and the GLV basis is the Eisenstein ideal (\u03c0\u0304 \u2212 1): closed form of the libsecp256k1 constants", "axis": "A", "kind": "algebraic", "status": "evaluated", "classification": "known_reproduced", "verdict": "Closed form holds exactly; all 22 identities found by the search are explained by CM theory. Known in principle (ideal structure of the GLV lattice), the explicit identification with the published constants is a useful clarification, not new mathematics.", "evidence": ["results/automated_discovery/identity_search.json", "results/baselines/secp256k1_facts.json"], "logged_utc": "2026-11-01T07:04:09+00:00"}
3 +{"id": "H03", "title": "GLV decomposition halves doublings; sub-scalars never exceed 128 bits", "axis": "A", "kind": "algebraic", "status": "evaluated", "classification": "known_reproduced", "verdict": "Max sub-scalar size 128 bits; 184 vs 304 group ops (GLV+wNAF vs wNAF), 2.07\u00d7 vs double-and-add.", "evidence": ["results/baselines/scalar_mult_opcount.json"], "logged_utc": "2026-11-01T07:04:09+00:00"}
4 +{"id": "H04", "title": "Automorphism orbits give exactly \u221a6 on Pollard rho, with measurable fruitless-cycle overhead", "axis": "A", "kind": "algebraic", "status": "evaluated", "classification": "known_reproduced", "verdict": "180 paired instances at 44\u201352 bits: iteration speed-up 2.21 [2.00, 2.45] (r = 128) and 2.42 [2.19, 2.70] (r = 20) vs \u221a6 = 2.449; constant 0.499\u00b7\u221an vs \u221a(\u03c0/12) = 0.512; negation alone 1.29 vs \u221a2, \u03c6-orbits alone 1.52 vs \u221a3; fruitless cycles \u2264 0.11 per 1000 iterations. Wall-clock speed-up only 0.95 (order-6) and 0.73 (negation) because canonicalisation costs 56\u201383 ns vs 38 ns per plain iteration in this engine.", "evidence": ["results/synthetic_ecdlp/scaling_j0_M2U64.json", "results/synthetic_ecdlp/analysis.json"], "logged_utc": "2026-11-01T07:04:09+00:00"}
5 +{"id": "H05", "title": "The automorphism orbit set cannot be made a group of order n/6 (rejected conjecture)", "axis": "A", "kind": "algebraic", "status": "evaluated", "classification": "rejected", "verdict": "Compatibility fails for 1500/1500 random pairs; proof: a congruence would make the orbit of O a subgroup H with cosets the other orbits, but orbits have size 6 \u2260 |H| = 1.", "evidence": ["results/theory/orbit_quotient_counterexample.json"], "logged_utc": "2026-11-01T07:04:09+00:00"}
6 +{"id": "H06", "title": "No known algebraic attack applies to secp256k1; prime-field index calculus with m = 2 costs \u226b \u221an", "axis": "A", "kind": "algebraic", "status": "evaluated", "classification": "known_reproduced", "verdict": "All exclusion criteria verified; measured decomposition rates match the Poisson prediction within sampling error; even with a free decomposition oracle the relation stage costs \u2265 2.4\u221an and the linear algebra |F|\u00b2 \u226b \u221an.", "evidence": ["results/baselines/secp256k1_facts.json", "results/synthetic_ecdlp/index_calculus_probe.json"], "logged_utc": "2026-11-01T07:04:09+00:00"}
7 +{"id": "H07", "title": "An exhaustive small-coefficient identity search finds only CM-explained relations and no cross-modulus or generator-specific relation", "axis": "A", "kind": "algebraic", "status": "evaluated", "classification": "empirical_observation", "verdict": "22 identities found, 22 explained, 0 unexplained, 0 false positives (expected chance hits 10^-73).", "evidence": ["results/automated_discovery/identity_search.json"], "logged_utc": "2026-11-01T07:04:09+00:00"}
8 +{"id": "H08", "title": "Measured ECDLP cost scales as n^0.5 with constants matching random-mapping theory", "axis": "B", "kind": "algorithmic", "status": "evaluated", "classification": "known_reproduced", "verdict": "3 840 verified instances, 20\u201356 bits: fitted \u03b1 \u2208 [0.478, 0.500] (\u2265 32 bits) for all seven configurations with CIs touching 0.5; pooled rho constant 1.254\u00b7\u221an [1.21, 1.30] on 480 runs vs \u221a(\u03c0/2) = 1.2533; BSGS 1.50\u00b7\u221an (x-keyed \u00b1j table); kangaroo 1.94\u00b7\u221an; Floyd 2.67\u00b7\u221an, Brent 1.65\u00b7\u221an.", "evidence": ["results/synthetic_ecdlp/scaling_generic_M3U96a.json", "results/synthetic_ecdlp/scaling_j0_M2U64.json", "results/synthetic_ecdlp/analysis.json"], "logged_utc": "2026-11-01T07:04:09+00:00"}
9 +{"id": "H09", "title": "Automated search over iteration-function designs cannot beat the random-mapping constant for a given class size", "axis": "B", "kind": "algorithmic", "status": "evaluated", "classification": "rejected", "verdict": "25 configurations \u00d7 80 instances: r = 2 and r = 3 walks cost 3.77\u00b7\u221an and 2.26\u00b7\u221an (negative control), r \u2265 8 cluster at 1.1\u20131.5\u00b7\u221an with CIs overlapping \u221a(\u03c0/2); mixed walks indistinguishable; best cell r32 at 1.10 [0.92, 1.28] not significantly below the random-mapping constant. No generic iteration design beats the random-mapping floor.", "evidence": ["results/synthetic_ecdlp/sweep_r_M3U96a.json", "results/synthetic_ecdlp/analysis.json"], "logged_utc": "2026-11-01T07:04:09+00:00"}
10 +{"id": "H10", "title": "Parallel distinguished-point collision search has near-linear speed-up up to 28 threads", "axis": "B", "kind": "algorithmic", "status": "evaluated", "classification": "known_reproduced", "verdict": "56-bit instances on an Apple M3 Ultra: 14.8\u00d7 on 16 threads (efficiency 0.93), 22.1\u00d7 on 28 threads (0.79, P+E cores); total group operations within a factor 1.0 of single-thread.", "evidence": ["results/synthetic_ecdlp/parallel_M3U96a.json", "results/synthetic_ecdlp/analysis.json"], "logged_utc": "2026-11-01T07:04:09+00:00"}
11 +{"id": "H11", "title": "Time\u2013memory trade-off: BSGS with memory m costs m + n/m operations; rho with DP density \u03b8 stores \u2248 \u03b8\u00b7\u221a(\u03c0n/2) points", "axis": "B", "kind": "algorithmic", "status": "evaluated", "classification": "known_reproduced", "verdict": "BSGS cost matches m + n/(2m) within 7.8 % for m from 2^10 to 2^20 at 36 bits; DP-rho at 40 bits spans 1.17\u20134.95 M operations as the DP density goes from 2^-12 (optimum, \u2248 \u221a(\u03c0n/2)) to 2^-2 (restart-dominated) and 2^-14 (overshoot-dominated).", "evidence": ["results/synthetic_ecdlp/tmto_M2U64.json", "results/synthetic_ecdlp/analysis.json"], "logged_utc": "2026-11-01T07:04:09+00:00"}
12 +{"id": "H12", "title": "Discrete logarithms with preprocessing: online cost \u2248 1.5\u00b7\u221a(n/T) after storing T distinguished points", "axis": "B", "kind": "algorithmic", "status": "evaluated", "classification": "known_reproduced", "verdict": "Online/\u221a(n/T) ratio 1.48\u20131.67 across T = 2^4 \u2026 2^12 at 22 and 26 bits; per-instance speed-up 2.6\u00d7 \u2192 42\u00d7 as predicted by \u221aT scaling.", "evidence": ["results/synthetic_ecdlp/preprocessing_python.json"], "logged_utc": "2026-11-01T07:04:09+00:00"}
13 +{"id": "H13", "title": "BIP340 batch verification reduces per-signature cost by a factor that grows with batch size and saturates", "axis": "B", "kind": "algorithmic", "status": "evaluated", "classification": "known_reproduced", "verdict": "Per-signature cost falls from 24.5 \u00b5s (single, k256) to 7.9 \u00b5s at b = 2048 with Pippenger (S = 3.10 [3.05, 3.16]; 1.67\u00d7 vs libsecp256k1 single verify); Straus plateaus at \u2248 2\u00d7; \u2248 3 \u00b5s/sig of non-MSM work (lift_x, tagged hash) caps the gain; negative control rejected; same shape on M3 Ultra.", "evidence": ["results/signature_benchmarks/summary.json", "results/signature_benchmarks/bench_MacBookPro-de-simon-pierre-2.json", "results/signature_benchmarks/bench_MacStudnpierre2.json"], "logged_utc": "2026-11-01T07:04:09+00:00"}
14 +{"id": "H14", "title": "Implementation choice dominates signature performance: three orders of magnitude between pure Python and libsecp256k1", "axis": "B", "kind": "algorithmic", "status": "evaluated", "classification": "empirical_observation", "verdict": "Schnorr verify: 7.4 ms (pure Python) vs 24.5 \u00b5s (k256) vs 13.2 \u00b5s (libsecp256k1): 560\u00d7 spread; libsecp256k1 verifies 1.9\u00d7 faster than k256 while k256 signs 1.8\u00d7 faster (table-based generator multiplication 8.3 \u00b5s vs blinded ecmult_gen 19.8 \u00b5s); ECDSA and Schnorr verification within 5 % in libsecp256k1.", "evidence": ["results/signature_benchmarks/summary.json"], "logged_utc": "2026-11-01T07:04:09+00:00"}
15 +{"id": "H15", "title": "A single-bit message difference reaches full avalanche within a small number of steps", "axis": "C", "kind": "sha256", "status": "running", "classification": "pending", "verdict": "pending", "evidence": [], "logged_utc": "2026-11-01T07:04:09+00:00"}
16 +{"id": "H16", "title": "Statistically detectable input\u2013output correlations vanish after a small number of steps", "axis": "C", "kind": "sha256", "status": "running", "classification": "pending", "verdict": "pending", "evidence": [], "logged_utc": "2026-11-01T07:04:09+00:00"}
17 +{"id": "H17", "title": "SAT preimage search on step-reduced SHA-256 scales exponentially in the number of steps and in the number of free bits", "axis": "C", "kind": "sha256", "status": "running", "classification": "pending", "verdict": "pending", "evidence": [], "logged_utc": "2026-11-01T07:04:09+00:00"}
18 +{"id": "H18", "title": "Generic SAT encodings find step-reduced collisions only far below the published dedicated-tool records", "axis": "C", "kind": "sha256", "status": "running", "classification": "pending", "verdict": "pending", "evidence": [], "logged_utc": "2026-11-01T07:04:09+00:00"}
19 +{"id": "H19", "title": "The 9-step local collision holds with probability 1 in the XOR-linearised model and with low, position-dependent probability in the real step function", "axis": "C", "kind": "sha256", "status": "running", "classification": "pending", "verdict": "pending", "evidence": [], "logged_utc": "2026-11-01T07:04:09+00:00"}
20 +{"id": "H20", "title": "The SHA-256 step is a bijection on the state and the message schedule is invertible, so free-start and pseudo-preimage problems on the compression function are trivial", "axis": "C", "kind": "sha256", "status": "evaluated", "classification": "known_reproduced", "verdict": "Machine-checked: stepInv \u2218 step = id and step \u2218 stepInv = id (per-component SAT lemmas, AIG 2048/2420), steps_bijective for any number of rounds, scheduleStep_inv and schedule_backward; executable demonstration in tests/test_core.py. Consequence (pseudo-preimages of the feed-forward-free compression function are free) stated informally.", "evidence": ["formal/README.md", "formal/build.log", "tests/test_formal_lean.py", "tests/test_core.py"], "logged_utc": "2026-11-01T07:04:09+00:00"}
21 +{"id": "H21", "title": "Lean 4 bv_decide machine-checks the SHA-256 word-level identities used by optimised implementations", "axis": "D", "kind": "formal", "status": "evaluated", "classification": "known_reproduced", "verdict": "Lean 4.35 bv_decide (LRAT certificates re-checked in Lean): Ch_opt, Maj_opt_or, Maj_opt_xor, GF(2)-linearity of \u03a30, \u03a31, \u03c30, \u03c31, injectivity and explicit inverses (\u03a30\u207b\u00b9 = XOR of 17 rotations) hence bijectivity; clean build 32 s, no sorry, no user axiom, no Mathlib.", "evidence": ["formal/README.md", "formal/build.log", "tests/test_formal_lean.py"], "logged_utc": "2026-11-01T07:04:09+00:00"}
22 +{"id": "H22", "title": "\u03c30 and \u03c31 are not injective on 32-bit words (kernel of positive dimension), unlike \u03a30 and \u03a31", "axis": "D", "kind": "formal", "status": "evaluated", "classification": "rejected", "verdict": "Conjecture refuted: \u03c30 and \u03c31 are bijective (bv_decide UNSAT certificates for injectivity; GF(2) rank 32 by two independent eliminations; explicit 32\u00d732 inverse matrices verified in Lean). The shift makes each summand singular (x\u226b3 has rank 29; rotr7\u2295rotr18 kills 0xFFFFFFFF) but the sum is a unit. A wrong belief caught by the formal layer.", "evidence": ["formal/README.md", "formal/build.log", "tests/test_formal_lean.py", "formal/scripts/gf2_rank.py"], "logged_utc": "2026-11-01T07:04:09+00:00"}
23 +{"id": "H23", "title": "Cross-implementation agreement is a necessary gate: every arithmetic component is checked against at least one independent implementation", "axis": "D", "kind": "methodology", "status": "evaluated", "classification": "empirical_observation", "verdict": "11 reference tests pass; agreement with 5 independent implementations; two development bugs caught by the gate (kangaroo retry loop, PARI output parsing).", "evidence": ["tests/test_core.py", "tests/test_dlp_reference.py"], "logged_utc": "2026-11-01T07:04:09+00:00"}
24 +{"id": "H24", "title": "The six-stage pipeline rediscovers known structure at a high rate and reports zero validated novel shortcuts", "axis": "D", "kind": "methodology", "status": "running", "classification": "pending", "verdict": "pending", "evidence": [], "logged_utc": "2026-11-01T07:04:09+00:00"}
25 +{"id": "H25", "title": "ECDSA malleability and Schnorr correctness follow from the abstract group axioms alone (formalised)", "axis": "D", "kind": "formal", "status": "evaluated", "classification": "known_reproduced", "verdict": "Machine-checked in an axiomatised CyclicGroup structure (fields listed in formal/README.md): schnorr_correct (s = k + e\u00b7d mod n \u27f9 s\u2022G = R + e\u2022P), ecdsa_correct, ecdsa_malleable ((r,s) valid \u27f9 (r, n\u2212s) valid, modular inverse as witness). Only propext/Quot.sound used.", "evidence": ["formal/README.md", "formal/build.log", "tests/test_formal_lean.py"], "logged_utc": "2026-11-01T07:04:09+00:00"}
added results/automated_discovery/hypothesis_summary.json +19 −0
@@ -0,0 +1,19 @@
1 +{
2 + "n_hypotheses": 25,
3 + "by_kind": {
4 + "algebraic": 7,
5 + "algorithmic": 7,
6 + "sha256": 6,
7 + "formal": 3,
8 + "methodology": 2
9 + },
10 + "by_classification": {
11 + "new_verified": 0,
12 + "conditional": 0,
13 + "unresolved": 0,
14 + "empirical_observation": 3,
15 + "known_reproduced": 13,
16 + "rejected": 3,
17 + "pending": 6
18 + }
19 +}
\ No newline at end of file
added results/automated_discovery/identity_search.json +211 −0
@@ -0,0 +1,211 @@
1 +{
2 + "experiment": "identity_search_secp256k1_constants",
3 + "timestamp_utc": "2026-11-01T05:45:18+00:00",
4 + "command": "experiments/automated_discovery/identity_search.py",
5 + "cwd": "/Users/simon-pierreboucher/Desktop/UQO/Recherche/Recherche-UQO/mathematiques/beyond-square-root-barrier",
6 + "seed": null,
7 + "host": {
8 + "hostname": "MacBookPro-de-simon-pierre-2.local",
9 + "os": "Darwin 27.0.0 (27.0)",
10 + "machine": "arm64",
11 + "python": "3.12.13",
12 + "cpu": "Apple M5 Max",
13 + "cores": 18,
14 + "ram_gb": 48.0
15 + },
16 + "software": {
17 + "numpy": "2.5.4",
18 + "scipy": "1.18.1",
19 + "sympy": "1.14.0",
20 + "gmpy2": "2.3.2",
21 + "z3": "5.3.3",
22 + "pysat": "1.9.dev17",
23 + "ecdsa": "0.19.2",
24 + "coincurve": "21.0.0"
25 + },
26 + "params": {
27 + "spaces": [
28 + "Q1",
29 + "Q2",
30 + "Q3",
31 + "Q4",
32 + "Q5"
33 + ],
34 + "coeff_bound": 6
35 + },
36 + "measurements": {
37 + "candidates_tested": 58064,
38 + "identities_found": 22,
39 + "found": [
40 + {
41 + "space": "Q1",
42 + "id": "Q1:t^2+0*t*f+3*f^2=4p",
43 + "relation": "t^2 +0\u00b7t\u00b7f +3\u00b7f^2 = 4p",
44 + "classification": "known_reproduced",
45 + "explanation": "CM theory: 4p = t\u00b2 + 3f\u00b2 for j = 0 (Frobenius \u03c0 = (t + f\u221a\u22123)/2 \u2208 Z[\u03c9])"
46 + },
47 + {
48 + "space": "Q1",
49 + "id": "Q1:t^2+0*t*b1+3*b1^2=4p",
50 + "relation": "t^2 +0\u00b7t\u00b7b1 +3\u00b7b1^2 = 4p",
51 + "classification": "known_reproduced",
52 + "explanation": "b1 = \u2212f: the GLV vector's second coordinate is the CM conductor (4p = t\u00b2 + 3f\u00b2)"
53 + },
54 + {
55 + "space": "Q1",
56 + "id": "Q1:f^2+1*f*a1+1*a1^2=n",
57 + "relation": "f^2 +1\u00b7f\u00b7a1 +1\u00b7a1^2 = n",
58 + "classification": "known_reproduced",
59 + "explanation": "quadratic form u\u00b2 \u00b1 uv + v\u00b2 = n among {a1, b1, a2, b2, f}: all are Eisenstein norms of unit multiples / conjugates of \u03c0\u0304 \u2212 1 (CM theory, GLV lattice = ideal (\u03c0\u0304 \u2212 1))"
60 + },
61 + {
62 + "space": "Q1",
63 + "id": "Q1:f^2-1*f*a2+1*a2^2=n",
64 + "relation": "f^2 -1\u00b7f\u00b7a2 +1\u00b7a2^2 = n",
65 + "classification": "known_reproduced",
66 + "explanation": "quadratic form u\u00b2 \u00b1 uv + v\u00b2 = n among {a1, b1, a2, b2, f}: all are Eisenstein norms of unit multiples / conjugates of \u03c0\u0304 \u2212 1 (CM theory, GLV lattice = ideal (\u03c0\u0304 \u2212 1))"
67 + },
68 + {
69 + "space": "Q1",
70 + "id": "Q1:f^2+1*f*b2+1*b2^2=n",
71 + "relation": "f^2 +1\u00b7f\u00b7b2 +1\u00b7b2^2 = n",
72 + "classification": "known_reproduced",
73 + "explanation": "quadratic form u\u00b2 \u00b1 uv + v\u00b2 = n among {a1, b1, a2, b2, f}: all are Eisenstein norms of unit multiples / conjugates of \u03c0\u0304 \u2212 1 (CM theory, GLV lattice = ideal (\u03c0\u0304 \u2212 1))"
74 + },
75 + {
76 + "space": "Q1",
77 + "id": "Q1:a1^2+1*a1*f+1*f^2=n",
78 + "relation": "a1^2 +1\u00b7a1\u00b7f +1\u00b7f^2 = n",
79 + "classification": "known_reproduced",
80 + "explanation": "quadratic form u\u00b2 \u00b1 uv + v\u00b2 = n among {a1, b1, a2, b2, f}: all are Eisenstein norms of unit multiples / conjugates of \u03c0\u0304 \u2212 1 (CM theory, GLV lattice = ideal (\u03c0\u0304 \u2212 1))"
81 + },
82 + {
83 + "space": "Q1",
84 + "id": "Q1:a1^2-1*a1*b1+1*b1^2=n",
85 + "relation": "a1^2 -1\u00b7a1\u00b7b1 +1\u00b7b1^2 = n",
86 + "classification": "known_reproduced",
87 + "explanation": "n = N(a1 + b1\u00b7\u03c9): the GLV basis vector has Eisenstein norm n (GLV 2001 / Z[\u03c9] structure)"
88 + },
89 + {
90 + "space": "Q1",
91 + "id": "Q1:a1^2-1*a1*a2+1*a2^2=n",
92 + "relation": "a1^2 -1\u00b7a1\u00b7a2 +1\u00b7a2^2 = n",
93 + "classification": "known_reproduced",
94 + "explanation": "quadratic form u\u00b2 \u00b1 uv + v\u00b2 = n among {a1, b1, a2, b2, f}: all are Eisenstein norms of unit multiples / conjugates of \u03c0\u0304 \u2212 1 (CM theory, GLV lattice = ideal (\u03c0\u0304 \u2212 1))"
95 + },
96 + {
97 + "space": "Q1",
98 + "id": "Q1:b1^2-1*b1*a1+1*a1^2=n",
99 + "relation": "b1^2 -1\u00b7b1\u00b7a1 +1\u00b7a1^2 = n",
100 + "classification": "known_reproduced",
101 + "explanation": "quadratic form u\u00b2 \u00b1 uv + v\u00b2 = n among {a1, b1, a2, b2, f}: all are Eisenstein norms of unit multiples / conjugates of \u03c0\u0304 \u2212 1 (CM theory, GLV lattice = ideal (\u03c0\u0304 \u2212 1))"
102 + },
103 + {
104 + "space": "Q1",
105 + "id": "Q1:b1^2+1*b1*a2+1*a2^2=n",
106 + "relation": "b1^2 +1\u00b7b1\u00b7a2 +1\u00b7a2^2 = n",
107 + "classification": "known_reproduced",
108 + "explanation": "quadratic form u\u00b2 \u00b1 uv + v\u00b2 = n among {a1, b1, a2, b2, f}: all are Eisenstein norms of unit multiples / conjugates of \u03c0\u0304 \u2212 1 (CM theory, GLV lattice = ideal (\u03c0\u0304 \u2212 1))"
109 + },
110 + {
111 + "space": "Q1",
112 + "id": "Q1:b1^2-1*b1*b2+1*b2^2=n",
113 + "relation": "b1^2 -1\u00b7b1\u00b7b2 +1\u00b7b2^2 = n",
114 + "classification": "known_reproduced",
115 + "explanation": "quadratic form u\u00b2 \u00b1 uv + v\u00b2 = n among {a1, b1, a2, b2, f}: all are Eisenstein norms of unit multiples / conjugates of \u03c0\u0304 \u2212 1 (CM theory, GLV lattice = ideal (\u03c0\u0304 \u2212 1))"
116 + },
117 + {
118 + "space": "Q1",
119 + "id": "Q1:a2^2-1*a2*f+1*f^2=n",
120 + "relation": "a2^2 -1\u00b7a2\u00b7f +1\u00b7f^2 = n",
121 + "classification": "known_reproduced",
122 + "explanation": "quadratic form u\u00b2 \u00b1 uv + v\u00b2 = n among {a1, b1, a2, b2, f}: all are Eisenstein norms of unit multiples / conjugates of \u03c0\u0304 \u2212 1 (CM theory, GLV lattice = ideal (\u03c0\u0304 \u2212 1))"
123 + },
124 + {
125 + "space": "Q1",
126 + "id": "Q1:a2^2-1*a2*a1+1*a1^2=n",
127 + "relation": "a2^2 -1\u00b7a2\u00b7a1 +1\u00b7a1^2 = n",
128 + "classification": "known_reproduced",
129 + "explanation": "quadratic form u\u00b2 \u00b1 uv + v\u00b2 = n among {a1, b1, a2, b2, f}: all are Eisenstein norms of unit multiples / conjugates of \u03c0\u0304 \u2212 1 (CM theory, GLV lattice = ideal (\u03c0\u0304 \u2212 1))"
130 + },
131 + {
132 + "space": "Q1",
133 + "id": "Q1:a2^2+1*a2*b1+1*b1^2=n",
134 + "relation": "a2^2 +1\u00b7a2\u00b7b1 +1\u00b7b1^2 = n",
135 + "classification": "known_reproduced",
136 + "explanation": "quadratic form u\u00b2 \u00b1 uv + v\u00b2 = n among {a1, b1, a2, b2, f}: all are Eisenstein norms of unit multiples / conjugates of \u03c0\u0304 \u2212 1 (CM theory, GLV lattice = ideal (\u03c0\u0304 \u2212 1))"
137 + },
138 + {
139 + "space": "Q1",
140 + "id": "Q1:a2^2-1*a2*b2+1*b2^2=n",
141 + "relation": "a2^2 -1\u00b7a2\u00b7b2 +1\u00b7b2^2 = n",
142 + "classification": "known_reproduced",
143 + "explanation": "same for the second basis vector"
144 + },
145 + {
146 + "space": "Q1",
147 + "id": "Q1:b2^2+1*b2*f+1*f^2=n",
148 + "relation": "b2^2 +1\u00b7b2\u00b7f +1\u00b7f^2 = n",
149 + "classification": "known_reproduced",
150 + "explanation": "quadratic form u\u00b2 \u00b1 uv + v\u00b2 = n among {a1, b1, a2, b2, f}: all are Eisenstein norms of unit multiples / conjugates of \u03c0\u0304 \u2212 1 (CM theory, GLV lattice = ideal (\u03c0\u0304 \u2212 1))"
151 + },
152 + {
153 + "space": "Q1",
154 + "id": "Q1:b2^2-1*b2*b1+1*b1^2=n",
155 + "relation": "b2^2 -1\u00b7b2\u00b7b1 +1\u00b7b1^2 = n",
156 + "classification": "known_reproduced",
157 + "explanation": "quadratic form u\u00b2 \u00b1 uv + v\u00b2 = n among {a1, b1, a2, b2, f}: all are Eisenstein norms of unit multiples / conjugates of \u03c0\u0304 \u2212 1 (CM theory, GLV lattice = ideal (\u03c0\u0304 \u2212 1))"
158 + },
159 + {
160 + "space": "Q1",
161 + "id": "Q1:b2^2-1*b2*a2+1*a2^2=n",
162 + "relation": "b2^2 -1\u00b7b2\u00b7a2 +1\u00b7a2^2 = n",
163 + "classification": "known_reproduced",
164 + "explanation": "quadratic form u\u00b2 \u00b1 uv + v\u00b2 = n among {a1, b1, a2, b2, f}: all are Eisenstein norms of unit multiples / conjugates of \u03c0\u0304 \u2212 1 (CM theory, GLV lattice = ideal (\u03c0\u0304 \u2212 1))"
165 + },
166 + {
167 + "space": "Q2",
168 + "id": "Q2:beta^2+1*beta+1=0 mod p",
169 + "relation": "beta^2 +1\u00b7beta +1 \u2261 0 (mod p)",
170 + "classification": "known_reproduced",
171 + "explanation": "\u03b2 is a primitive cube root of unity (\u03b2\u00b2 + \u03b2 + 1 = 0)"
172 + },
173 + {
174 + "space": "Q2",
175 + "id": "Q2:lambda^2+1*lambda+1=0 mod n",
176 + "relation": "lambda^2 +1\u00b7lambda +1 \u2261 0 (mod n)",
177 + "classification": "known_reproduced",
178 + "explanation": "\u03bb is a primitive cube root of unity in Z/nZ"
179 + },
180 + {
181 + "space": "Q3",
182 + "id": "Q3:a2=+1*a1-1*b1",
183 + "relation": "a2 = 1\u00b7a1 -1\u00b7b1",
184 + "classification": "known_reproduced",
185 + "explanation": "second GLV vector = \u03c9\u00b7(first) up to sign: (a1,b1)\u00b7\u03c9 rotates the lattice (Eisenstein units)"
186 + },
187 + {
188 + "space": "Q3",
189 + "id": "Q3:b2=+1*a1+0*b1",
190 + "relation": "b2 = 1\u00b7a1 +0\u00b7b1",
191 + "classification": "known_reproduced",
192 + "explanation": "same"
193 + }
194 + ],
195 + "closed_form_glv_basis": {
196 + "a1": "64502973549206556628585045361533709077",
197 + "b1": "-303414439467246543595250775667605759171",
198 + "a2": "367917413016453100223835821029139468248",
199 + "b2": "64502973549206556628585045361533709077"
200 + },
201 + "closed_form_matches_libsecp256k1": true,
202 + "closed_form_statement": "a1 = (t\u2212f)/2 \u2212 1, b1 = \u2212f, a2 = (t+f)/2 \u2212 1, b2 = a1 with 4p = t\u00b2 + 3f\u00b2; lattice = ideal (\u03c0\u0304\u22121)Z[\u03c9]",
203 + "negative_controls": {
204 + "Q4_cross_modulus_hits": 0,
205 + "Q5_small_linear_hits": 0,
206 + "expected_chance_hits": 5.0145049098300383e-73
207 + },
208 + "unexplained": []
209 + },
210 + "elapsed_s": 0.156
211 +}
\ No newline at end of file
added results/baselines/scalar_mult_opcount.json +90 −0
@@ -0,0 +1,90 @@
1 +{
2 + "experiment": "scalar_mult_opcount_secp256k1",
3 + "timestamp_utc": "2026-11-01T05:41:19+00:00",
4 + "command": "experiments/baselines/scalar_mult_opcount.py",
5 + "cwd": "/Users/simon-pierreboucher/Desktop/UQO/Recherche/Recherche-UQO/mathematiques/beyond-square-root-barrier",
6 + "seed": 34,
7 + "host": {
8 + "hostname": "MacBookPro-de-simon-pierre-2.local",
9 + "os": "Darwin 27.0.0 (27.0)",
10 + "machine": "arm64",
11 + "python": "3.12.13",
12 + "cpu": "Apple M5 Max",
13 + "cores": 18,
14 + "ram_gb": 48.0
15 + },
16 + "software": {
17 + "numpy": "2.5.4",
18 + "scipy": "1.18.1",
19 + "sympy": "1.14.0",
20 + "gmpy2": "2.3.2",
21 + "z3": "5.3.3",
22 + "pysat": "1.9.dev17",
23 + "ecdsa": "0.19.2",
24 + "coincurve": "21.0.0"
25 + },
26 + "params": {
27 + "n_scalars": 200,
28 + "model": "affine, counts group add/dbl"
29 + },
30 + "measurements": {
31 + "double_and_add": {
32 + "add_mean": 127.24,
33 + "dbl_mean": 254.055,
34 + "total_mean": 381.295,
35 + "total_sd": 8.151562733611268,
36 + "total_min": 356,
37 + "total_max": 402,
38 + "speedup_vs_double_and_add_ops": 1.0,
39 + "time_mean_ms": 3.5227966523962095
40 + },
41 + "wnaf_w4": {
42 + "add_mean": 53.72,
43 + "dbl_mean": 254.82,
44 + "total_mean": 308.54,
45 + "total_sd": 2.8510348998214665,
46 + "total_min": 297,
47 + "total_max": 315,
48 + "speedup_vs_double_and_add_ops": 1.2358041096778376,
49 + "time_mean_ms": 2.8935274633113295
50 + },
51 + "wnaf_w5": {
52 + "add_mean": 49.125,
53 + "dbl_mean": 254.26,
54 + "total_mean": 303.385,
55 + "total_sd": 2.9860969508708184,
56 + "total_min": 288,
57 + "total_max": 310,
58 + "speedup_vs_double_and_add_ops": 1.256802412775846,
59 + "time_mean_ms": 2.8498097928240895
60 + },
61 + "glv_wnaf_w4": {
62 + "add_mean": 56.59,
63 + "dbl_mean": 127.775,
64 + "total_mean": 184.365,
65 + "total_sd": 2.6724099610651058,
66 + "total_min": 177,
67 + "total_max": 192,
68 + "speedup_vs_double_and_add_ops": 2.06815284896808,
69 + "time_mean_ms": 1.7463462293380871
70 + },
71 + "glv_wnaf_w5": {
72 + "add_mean": 56.235,
73 + "dbl_mean": 127.395,
74 + "total_mean": 183.63,
75 + "total_sd": 2.3522542379598343,
76 + "total_min": 177,
77 + "total_max": 190,
78 + "speedup_vs_double_and_add_ops": 2.076430866416163,
79 + "time_mean_ms": 1.7315525317098945
80 + },
81 + "glv_subscalar_bits": {
82 + "k1_max": 128,
83 + "k2_max": 128,
84 + "k1_mean": 126.1955,
85 + "k2_mean": 125.8085,
86 + "samples": 2000
87 + }
88 + },
89 + "elapsed_s": 3.385
90 +}
\ No newline at end of file
added results/baselines/secp256k1_facts.json +148 −0
@@ -0,0 +1,148 @@
1 +{
2 + "experiment": "secp256k1_parameter_verification",
3 + "timestamp_utc": "2026-11-01T05:40:39+00:00",
4 + "command": "experiments/baselines/secp256k1_facts.py",
5 + "cwd": "/Users/simon-pierreboucher/Desktop/UQO/Recherche/Recherche-UQO/mathematiques/beyond-square-root-barrier",
6 + "seed": null,
7 + "host": {
8 + "hostname": "MacBookPro-de-simon-pierre-2.local",
9 + "os": "Darwin 27.0.0 (27.0)",
10 + "machine": "arm64",
11 + "python": "3.12.13",
12 + "cpu": "Apple M5 Max",
13 + "cores": 18,
14 + "ram_gb": 48.0
15 + },
16 + "software": {
17 + "numpy": "2.5.4",
18 + "scipy": "1.18.1",
19 + "sympy": "1.14.0",
20 + "gmpy2": "2.3.2",
21 + "z3": "5.3.3",
22 + "pysat": "1.9.dev17",
23 + "ecdsa": "0.19.2",
24 + "coincurve": "21.0.0"
25 + },
26 + "params": {
27 + "gp_version": "2.19.0"
28 + },
29 + "measurements": {
30 + "python": {
31 + "p_is_prime": true,
32 + "p_equals_2^256-2^32-977": true,
33 + "p_mod_3": 1,
34 + "p_mod_4": 3,
35 + "n_is_prime": true,
36 + "G_on_curve": true,
37 + "nG_is_infinity": true,
38 + "cofactor": 1,
39 + "discriminant_nonzero": true,
40 + "j_invariant": 0,
41 + "trace_t": "432420386565659656852420866390673177327",
42 + "hasse_bound_ok": true,
43 + "hasse_exact": true,
44 + "anomalous": false,
45 + "frobenius_discriminant": "-276180966231670251052810340486134462307742726569900724701098994184861751821723",
46 + "disc_is_minus3_times_square": true,
47 + "conductor_f": "303414439467246543595250775667605759171",
48 + "beta_cubed_is_1_mod_p": true,
49 + "lambda_cubed_is_1_mod_n": true,
50 + "beta^2+beta+1=0 mod p": true,
51 + "lambda^2+lambda+1=0 mod n": true,
52 + "phi_G_on_curve": true,
53 + "phi_G_equals_lambda_G": true,
54 + "phi2_G_equals_lambda2_G": true,
55 + "beta_lambda2_mismatch": true,
56 + "glv_basis_v1": true,
57 + "glv_basis_v2": true,
58 + "glv_basis_det_is_n": true,
59 + "x0_points_exist": false,
60 + "embedding_degree_le_100": null,
61 + "twist_order": "115792089237316195423570985008687907853702405052206223696310004874299507848991",
62 + "twist_small_factors": {
63 + "3": 2,
64 + "13": 2,
65 + "3319": 1,
66 + "22639": 1
67 + },
68 + "twist_cofactor_bits": 220,
69 + "twist_cofactor_is_prime": true,
70 + "n_minus_1_small_factors": {
71 + "2": 6,
72 + "3": 1,
73 + "149": 1,
74 + "631": 1
75 + },
76 + "p_minus_1_small_factors": {
77 + "2": 1,
78 + "3": 1,
79 + "7": 1,
80 + "13441": 1
81 + },
82 + "glv_basis_rederived_from_euclid": true,
83 + "glv_euclid_candidates": [
84 + [
85 + "367917413016453100223835821029139468248",
86 + "64502973549206556628585045361533709077"
87 + ],
88 + [
89 + "64502973549206556628585045361533709077",
90 + "-303414439467246543595250775667605759171"
91 + ],
92 + [
93 + "45402545270420317080910594221470922863",
94 + "1581575170885439274604838923699562504932"
95 + ]
96 + ]
97 + },
98 + "pari_gp": {
99 + "p_is_prime": true,
100 + "n_is_prime": true,
101 + "ellcard": "115792089237316195423570985008687907852837564279074904382605163141518161494337",
102 + "G_on_curve": true,
103 + "nG_is_infinity": true,
104 + "trace_t": "432420386565659656852420866390673177327",
105 + "j_invariant": 0,
106 + "beta_cubed_is_1_mod_p": true,
107 + "lambda_cubed_is_1_mod_n": true,
108 + "phi_G_equals_lambda_G": true,
109 + "twist_small_primes": "[3, 13, 3319, 22639, 1013176677300131846900870239606035638738100997248092069256697437031]",
110 + "twist_small_exponents": "[2, 2, 1, 1, 1]",
111 + "twist_cofactor_is_prime": true,
112 + "twist_cofactor_bits": 220,
113 + "embedding_degree": "19298681539552699237261830834781317975472927379845817397100860523586360249056",
114 + "n_minus_1_small_primes": "[2, 3, 149, 631, 6414488540731361226607730496888035255996436684289152125202372832747357]",
115 + "p_minus_1_small_primes": "[2, 3, 7, 13441, 205115282021455665897114700593932402728804164701536103180137503955397371]",
116 + "frobenius_discriminant": "-276180966231670251052810340486134462307742726569900724701098994184861751821723",
117 + "disc_is_minus3_times_square": true,
118 + "cube_roots_unity_mod_p": [
119 + 55594575648329892869085402983802832744385952214688224221778511981742606582254,
120 + 60197513588986302554485582024885075108884032450952339817679072026166228089408
121 + ],
122 + "cube_roots_unity_mod_n": [
123 + 78074008874160198520644763525212887401909906723592317393988542598630163514318,
124 + 37718080363155996902926221483475020450927657555482586988616620542887997980018
125 + ],
126 + "kronecker_7_p": -1,
127 + "num_roots_x3_plus_7": 0
128 + },
129 + "agreement": {
130 + "p_prime": true,
131 + "n_prime": true,
132 + "order_equals_n": true,
133 + "trace": true,
134 + "j": true,
135 + "phi_lambda": true,
136 + "twist_cofactor_prime": true,
137 + "twist_cofactor_bits": true,
138 + "embedding_degree_gt_100": true,
139 + "beta_in_gp_roots": true,
140 + "lambda_in_gp_roots": true,
141 + "x0_points": true,
142 + "cm_discriminant": true,
143 + "frobenius_discriminant": true
144 + },
145 + "all_agree": true
146 + },
147 + "elapsed_s": 4.495
148 +}
\ No newline at end of file
added results/reduced_round_sha256/bias_seed1.json +1329 −0
@@ -0,0 +1,5254 @@
1 +{
2 + "experiment": "sha256_bias_test",
3 + "timestamp_utc": "2026-11-01T05:40:59+00:00",
4 + "command": "experiments/reduced_round_sha256/bias_test.py --seed 1 --n 20000 --out results/reduced_round_sha256/bias_seed1.json",
5 + "cwd": "/Users/simon-pierreboucher/Desktop/UQO/Recherche/Recherche-UQO/mathematiques/beyond-square-root-barrier",
6 + "seed": 1,
7 + "host": {
8 + "hostname": "MacBookPro-de-simon-pierre-2.local",
9 + "os": "Darwin 27.0.0 (27.0)",
10 + "machine": "arm64",
11 + "python": "3.12.13",
12 + "cpu": "Apple M5 Max",
13 + "cores": 18,
14 + "ram_gb": 48.0
15 + },
16 + "software": {
17 + "numpy": "2.5.4",
18 + "scipy": "1.18.1",
19 + "sympy": "1.14.0",
20 + "gmpy2": "2.3.2",
21 + "z3": "5.3.3",
22 + "pysat": "1.9.dev17",
23 + "ecdsa": "0.19.2",
24 + "coincurve": "21.0.0"
25 + },
26 + "params": {
27 + "n": 20000,
28 + "r_list": [
29 + 1,
30 + 2,
31 + 3,
32 + 4,
33 + 5,
34 + 6,
35 + 8,
36 + 10,
37 + 12,
38 + 16,
39 + 20,
40 + 24,
41 + 32,
42 + 64
43 + ],
44 + "n_input_bits": 64,
45 + "feedforward": true,
46 + "iv": "standard H0"
47 + },
48 + "measurements": {
49 + "input_bits_sampled": [
50 + 2,
51 + 9,
52 + 22,
53 + 26,
54 + 29,
55 + 30,
56 + 31,
57 + 35,
58 + 44,
59 + 64,
60 + 88,
61 + 96,
62 + 102,
63 + 104,
64 + 110,
65 + 120,
66 + 123,
67 + 137,
68 + 167,
69 + 177,
70 + 190,
71 + 194,
72 + 214,
73 + 221,
74 + 224,
75 + 227,
76 + 234,
77 + 236,
78 + 238,
79 + 248,
80 + 261,
81 + 272,
82 + 290,
83 + 296,
84 + 303,
85 + 310,
86 + 315,
87 + 325,
88 + 340,
89 + 353,
90 + 375,
91 + 379,
92 + 383,
93 + 388,
94 + 390,
95 + 399,
96 + 402,
97 + 413,
98 + 424,
99 + 426,
100 + 432,
101 + 443,
102 + 448,
103 + 449,
104 + 456,
105 + 460,
106 + 470,
107 + 480,
108 + 483,
109 + 491,
110 + 499,
111 + 501,
112 + 507,
113 + 511
114 + ],
115 + "by_r": {
116 + "1": {
117 + "max_bit_bias": 0.5,
118 + "mean_bit_bias": 0.375603515625,
119 + "bias_threshold_4sigma": 0.01414213562373095,
120 + "n_bits_bias_over_4sigma": 192,
121 + "bit_bias": [
122 + 0.0021,
123 + 0.00355,
124 + 5e-05,
125 + 0.0038,
126 + 0.00015,
127 + 0.00015,
128 + 0.001,
129 + 0.00265,
130 + 0.00445,
131 + 0.00165,
132 + 0.0047,
133 + 0.00415,
134 + 0.00185,
135 + 0.00015,
136 + 0.0016,
137 + 0.0044,
138 + 0.0031,
139 + 0.0028,
140 + 0.00015,
141 + 0.00055,
142 + 0.00165,
143 + 0.00155,
144 + 0.00185,
145 + 0.0013,
146 + 0.0033,
147 + 0.00315,
148 + 0.00355,
149 + 0.00195,
150 + 0.00235,
151 + 0.0076,
152 + 0.0042,
153 + 0.00145,
154 + 0.5,
155 + 0.5,
156 + 0.5,
157 + 0.5,
158 + 0.5,
159 + 0.5,
160 + 0.5,
161 + 0.5,
162 + 0.5,
163 + 0.5,
164 + 0.5,
165 + 0.5,
166 + 0.5,
167 + 0.5,
168 + 0.5,
169 + 0.5,
170 + 0.5,
171 + 0.5,
172 + 0.5,
173 + 0.5,
174 + 0.5,
175 + 0.5,
176 + 0.5,
177 + 0.5,
178 + 0.5,
179 + 0.5,
180 + 0.5,
181 + 0.5,
182 + 0.5,
183 + 0.5,
184 + 0.5,
185 + 0.5,
186 + 0.5,
187 + 0.5,
188 + 0.5,
189 + 0.5,
190 + 0.5,
191 + 0.5,
192 + 0.5,
193 + 0.5,
194 + 0.5,
195 + 0.5,
196 + 0.5,
197 + 0.5,
198 + 0.5,
199 + 0.5,
200 + 0.5,
201 + 0.5,
202 + 0.5,
203 + 0.5,
204 + 0.5,
205 + 0.5,
206 + 0.5,
207 + 0.5,
208 + 0.5,
209 + 0.5,
210 + 0.5,
211 + 0.5,
212 + 0.5,
213 + 0.5,
214 + 0.5,
215 + 0.5,
216 + 0.5,
217 + 0.5,
218 + 0.5,
219 + 0.5,
220 + 0.5,
221 + 0.5,
222 + 0.5,
223 + 0.5,
224 + 0.5,
225 + 0.5,
226 + 0.5,
227 + 0.5,
228 + 0.5,
229 + 0.5,
230 + 0.5,
231 + 0.5,
232 + 0.5,
233 + 0.5,
234 + 0.5,
235 + 0.5,
236 + 0.5,
237 + 0.5,
238 + 0.5,
239 + 0.5,
240 + 0.5,
241 + 0.5,
242 + 0.5,
243 + 0.5,
244 + 0.5,
245 + 0.5,
246 + 0.5,
247 + 0.5,
248 + 0.5,
249 + 0.5,
250 + 0.00315,
251 + 0.002,
252 + 0.0,
253 + 0.0005,
254 + 0.0048,
255 + 0.00055,
256 + 0.0002,
257 + 0.0021,
258 + 0.00125,
259 + 0.0007,
260 + 0.00585,
261 + 0.00205,
262 + 0.0026,
263 + 0.0018,
264 + 0.0057,
265 + 0.0053,
266 + 0.0003,
267 + 0.00215,
268 + 0.00505,
269 + 0.00095,
270 + 0.00505,
271 + 0.00075,
272 + 0.00205,
273 + 0.00185,
274 + 0.0053,
275 + 0.00075,
276 + 0.00135,
277 + 0.00095,
278 + 0.0059,
279 + 0.00335,
280 + 0.00185,
281 + 0.00145,
282 + 0.5,
283 + 0.5,
284 + 0.5,
285 + 0.5,
286 + 0.5,
287 + 0.5,
288 + 0.5,
289 + 0.5,
290 + 0.5,
291 + 0.5,
292 + 0.5,
293 + 0.5,
294 + 0.5,
295 + 0.5,
296 + 0.5,
297 + 0.5,
298 + 0.5,
299 + 0.5,
300 + 0.5,
301 + 0.5,
302 + 0.5,
303 + 0.5,
304 + 0.5,
305 + 0.5,
306 + 0.5,
307 + 0.5,
308 + 0.5,
309 + 0.5,
310 + 0.5,
311 + 0.5,
312 + 0.5,
313 + 0.5,
314 + 0.5,
315 + 0.5,
316 + 0.5,
317 + 0.5,
318 + 0.5,
319 + 0.5,
320 + 0.5,
321 + 0.5,
322 + 0.5,
323 + 0.5,
324 + 0.5,
325 + 0.5,
326 + 0.5,
327 + 0.5,
328 + 0.5,
329 + 0.5,
330 + 0.5,
331 + 0.5,
332 + 0.5,
333 + 0.5,
334 + 0.5,
335 + 0.5,
336 + 0.5,
337 + 0.5,
338 + 0.5,
339 + 0.5,
340 + 0.5,
341 + 0.5,
342 + 0.5,
343 + 0.5,
344 + 0.5,
345 + 0.5,
346 + 0.5,
347 + 0.5,
348 + 0.5,
349 + 0.5,
350 + 0.5,
351 + 0.5,
352 + 0.5,
353 + 0.5,
354 + 0.5,
355 + 0.5,
356 + 0.5,
357 + 0.5,
358 + 0.5,
359 + 0.5,
360 + 0.5,
361 + 0.5,
362 + 0.5,
363 + 0.5,
364 + 0.5,
365 + 0.5,
366 + 0.5,
367 + 0.5,
368 + 0.5,
369 + 0.5,
370 + 0.5,
371 + 0.5,
372 + 0.5,
373 + 0.5,
374 + 0.5,
375 + 0.5,
376 + 0.5,
377 + 0.5
378 + ],
379 + "max_abs_corr": 1.0,
380 + "corr_threshold_4sigma": 0.0282842712474619,
381 + "n_pairs_corr_over_4sigma": 13,
382 + "expected_pairs_under_null": 1.0378032523876717,
383 + "argmax_corr_pair": [
384 + 4,
385 + 29
386 + ],
387 + "chi2_min_p": 0.0,
388 + "chi2_n_bytes_rejected_bonferroni": 24,
389 + "chi2_p_by_byte": [
390 + 0.39041587181808785,
391 + 0.6245105588599271,
392 + 0.7711729729015471,
393 + 0.019627292239171076,
394 + 0.0,
395 + 0.0,
396 + 0.0,
397 + 0.0,
398 + 0.0,
399 + 0.0,
400 + 0.0,
401 + 0.0,
402 + 0.0,
403 + 0.0,
404 + 0.0,
405 + 0.0,
406 + 0.07579911843509278,
407 + 0.6389193952194389,
408 + 0.6910456371415133,
409 + 0.019627292239171076,
410 + 0.0,
411 + 0.0,
412 + 0.0,
413 + 0.0,
414 + 0.0,
415 + 0.0,
416 + 0.0,
417 + 0.0,
418 + 0.0,
419 + 0.0,
420 + 0.0,
421 + 0.0
422 + ],
423 + "chi2_stat_by_byte": [
424 + 260.6592,
425 + 247.2448,
426 + 237.952,
427 + 303.69280000000003,
428 + 5100000.0,
429 + 5100000.0,
430 + 5100000.0,
431 + 5100000.0,
432 + 5100000.0,
433 + 5100000.0,
434 + 5100000.0,
435 + 5100000.0,
436 + 5100000.0,
437 + 5100000.0,
438 + 5100000.0,
439 + 5100000.0,
440 + 288.0512,
441 + 246.4,
442 + 243.2512,
443 + 303.69280000000003,
444 + 5100000.0,
445 + 5100000.0,
446 + 5100000.0,
447 + 5100000.0,
448 + 5100000.0,
449 + 5100000.0,
450 + 5100000.0,
451 + 5100000.0,
452 + 5100000.0,
453 + 5100000.0,
454 + 5100000.0,
455 + 5100000.0
456 + ]
457 + },
458 + "2": {
459 + "max_bit_bias": 0.5,
460 + "mean_bit_bias": 0.2514517578125,
461 + "bias_threshold_4sigma": 0.01414213562373095,
462 + "n_bits_bias_over_4sigma": 128,
463 + "bit_bias": [
464 + 0.00265,
465 + 0.00215,
466 + 5e-05,
467 + 0.0011,
468 + 0.00015,
469 + 0.00635,
470 + 0.0005,
471 + 0.00035,
472 + 0.0015,
473 + 0.00215,
474 + 0.00105,
475 + 0.00135,
476 + 0.0003,
477 + 0.00625,
478 + 0.0071,
479 + 0.0041,
480 + 0.0001,
481 + 0.00015,
482 + 0.00075,
483 + 0.0027,
484 + 0.0,
485 + 0.00715,
486 + 0.0,
487 + 0.0016,
488 + 0.0023,
489 + 0.0048,
490 + 0.00255,
491 + 0.00535,
492 + 0.00085,
493 + 0.0006,
494 + 0.0029,
495 + 0.00355,
496 + 0.00635,
497 + 0.00525,
498 + 0.00235,
499 + 0.004,
500 + 0.00495,
501 + 0.0042,
502 + 0.00155,
503 + 0.0011,
504 + 0.00305,
505 + 0.0014,
506 + 0.0061,
507 + 0.0068,
508 + 0.0003,
509 + 0.0083,
510 + 0.0048,
511 + 0.00585,
512 + 0.0007,
513 + 0.00225,
514 + 0.00445,
515 + 0.0021,
516 + 0.0022,
517 + 0.002,
518 + 0.0005,
519 + 0.00155,
520 + 0.00285,
521 + 0.00575,
522 + 0.00035,
523 + 0.0012,
524 + 0.0065,
525 + 0.0099,
526 + 0.0042,
527 + 0.00145,
528 + 0.5,
529 + 0.5,
530 + 0.5,
531 + 0.5,
532 + 0.5,
533 + 0.5,
534 + 0.5,
535 + 0.5,
536 + 0.5,
537 + 0.5,
538 + 0.5,
539 + 0.5,
540 + 0.5,
541 + 0.5,
542 + 0.5,
543 + 0.5,
544 + 0.5,
545 + 0.5,
546 + 0.5,
547 + 0.5,
548 + 0.5,
549 + 0.5,
550 + 0.5,
551 + 0.5,
552 + 0.5,
553 + 0.5,
554 + 0.5,
555 + 0.5,
556 + 0.5,
557 + 0.5,
558 + 0.5,
559 + 0.5,
560 + 0.5,
561 + 0.5,
562 + 0.5,
563 + 0.5,
564 + 0.5,
565 + 0.5,
566 + 0.5,
567 + 0.5,
568 + 0.5,
569 + 0.5,
570 + 0.5,
571 + 0.5,
572 + 0.5,
573 + 0.5,
574 + 0.5,
575 + 0.5,
576 + 0.5,
577 + 0.5,
578 + 0.5,
579 + 0.5,
580 + 0.5,
581 + 0.5,
582 + 0.5,
583 + 0.5,
584 + 0.5,
585 + 0.5,
586 + 0.5,
587 + 0.5,
588 + 0.5,
589 + 0.5,
590 + 0.5,
591 + 0.5,
592 + 0.00075,
593 + 0.00455,
594 + 0.0012,
595 + 0.002,
596 + 0.00195,
597 + 0.00105,
598 + 0.00265,
599 + 0.00185,
600 + 0.0035,
601 + 0.00375,
602 + 0.00545,
603 + 0.0009,
604 + 0.0038,
605 + 0.00315,
606 + 0.0047,
607 + 0.0001,
608 + 0.00375,
609 + 0.00205,
610 + 0.0008,
611 + 0.0009,
612 + 0.00085,
613 + 0.00025,
614 + 0.00125,
615 + 0.0013,
616 + 0.00035,
617 + 0.0075,
618 + 0.006,
619 + 0.0011,
620 + 0.0007,
621 + 0.0058,
622 + 0.00305,
623 + 0.00085,
624 + 0.0059,
625 + 0.00285,
626 + 0.00155,
627 + 0.00235,
628 + 0.0069,
629 + 0.0062,
630 + 0.00015,
631 + 0.00355,
632 + 0.0017,
633 + 0.0026,
634 + 0.0046,
635 + 0.00595,
636 + 0.00275,
637 + 0.00875,
638 + 0.00315,
639 + 0.00565,
640 + 0.0002,
641 + 0.0017,
642 + 0.00095,
643 + 0.0007,
644 + 0.00255,
645 + 0.00115,
646 + 0.00155,
647 + 0.0019,
648 + 0.002,
649 + 0.0034,
650 + 0.0047,
651 + 0.0027,
652 + 0.0034,
653 + 0.0099,
654 + 0.0042,
655 + 0.00145,
656 + 0.5,
657 + 0.5,
658 + 0.5,
659 + 0.5,
660 + 0.5,
661 + 0.5,
662 + 0.5,
663 + 0.5,
664 + 0.5,
665 + 0.5,
666 + 0.5,
667 + 0.5,
668 + 0.5,
669 + 0.5,
670 + 0.5,
671 + 0.5,
672 + 0.5,
673 + 0.5,
674 + 0.5,
675 + 0.5,
676 + 0.5,
677 + 0.5,
678 + 0.5,
679 + 0.5,
680 + 0.5,
681 + 0.5,
682 + 0.5,
683 + 0.5,
684 + 0.5,
685 + 0.5,
686 + 0.5,
687 + 0.5,
688 + 0.5,
689 + 0.5,
690 + 0.5,
691 + 0.5,
692 + 0.5,
693 + 0.5,
694 + 0.5,
695 + 0.5,
696 + 0.5,
697 + 0.5,
698 + 0.5,
699 + 0.5,
700 + 0.5,
701 + 0.5,
702 + 0.5,
703 + 0.5,
704 + 0.5,
705 + 0.5,
706 + 0.5,
707 + 0.5,
708 + 0.5,
709 + 0.5,
710 + 0.5,
711 + 0.5,
712 + 0.5,
713 + 0.5,
714 + 0.5,
715 + 0.5,
716 + 0.5,
717 + 0.5,
718 + 0.5,
719 + 0.5
720 + ],
721 + "max_abs_corr": 1.0,
722 + "corr_threshold_4sigma": 0.0282842712474619,
723 + "n_pairs_corr_over_4sigma": 12,
724 + "expected_pairs_under_null": 1.0378032523876717,
725 + "argmax_corr_pair": [
726 + 5,
727 + 62
728 + ],
729 + "chi2_min_p": 0.0,
730 + "chi2_n_bytes_rejected_bonferroni": 16,
731 + "chi2_p_by_byte": [
732 + 0.8957298262111091,
733 + 0.8127758993385427,
734 + 0.836616398364919,
735 + 0.25379999700317507,
736 + 0.29656183733536473,
737 + 0.6621390393626484,
738 + 0.7554537935673091,
739 + 0.019627292239171076,
740 + 0.0,
741 + 0.0,
742 + 0.0,
743 + 0.0,
744 + 0.0,
745 + 0.0,
746 + 0.0,
747 + 0.0,
748 + 0.9633294234687942,
749 + 0.8422011103319755,
750 + 0.07043698857791295,
751 + 0.10240331175616556,
752 + 0.0332549564530106,
753 + 0.7232925717581397,
754 + 0.6531661325495348,
755 + 0.019627292239171076,
756 + 0.0,
757 + 0.0,
758 + 0.0,
759 + 0.0,
760 + 0.0,
761 + 0.0,
762 + 0.0,
763 + 0.0
764 + ],
765 + "chi2_stat_by_byte": [
766 + 227.0208,
767 + 234.8288,
768 + 232.8576,
769 + 269.568,
770 + 266.57280000000003,
771 + 245.0176,
772 + 239.0528,
773 + 303.69280000000003,
774 + 5100000.0,
775 + 5100000.0,
776 + 5100000.0,
777 + 5100000.0,
778 + 5100000.0,
779 + 5100000.0,
780 + 5100000.0,
781 + 5100000.0,
782 + 216.06400000000002,
783 + 232.3712,
784 + 288.9984,
785 + 284.0064,
786 + 297.984,
787 + 241.2032,
788 + 245.5552,
789 + 303.69280000000003,
790 + 5100000.0,
791 + 5100000.0,
792 + 5100000.0,
793 + 5100000.0,
794 + 5100000.0,
795 + 5100000.0,
796 + 5100000.0,
797 + 5100000.0
798 + ]
799 + },
800 + "3": {
801 + "max_bit_bias": 0.5,
802 + "mean_bit_bias": 0.12711640625,
803 + "bias_threshold_4sigma": 0.01414213562373095,
804 + "n_bits_bias_over_4sigma": 64,
805 + "bit_bias": [
806 + 0.00105,
807 + 0.00375,
808 + 0.0032,
809 + 0.0,
810 + 0.00205,
811 + 5e-05,
812 + 0.001,
813 + 0.00135,
814 + 0.00405,
815 + 0.0014,
816 + 0.00285,
817 + 0.00605,
818 + 0.0058,
819 + 0.0012,
820 + 0.00165,
821 + 0.00665,
822 + 0.00415,
823 + 0.00205,
824 + 0.00525,
825 + 0.00045,
826 + 0.00215,
827 + 0.00205,
828 + 0.003,
829 + 0.00695,
830 + 0.0003,
831 + 0.0068,
832 + 0.00035,
833 + 0.0025,
834 + 0.0014,
835 + 0.00265,
836 + 0.0035,
837 + 0.0018,
838 + 0.00105,
839 + 0.0,
840 + 0.0004,
841 + 0.0013,
842 + 0.0022,
843 + 0.00325,
844 + 0.0002,
845 + 0.00285,
846 + 0.00205,
847 + 0.00105,
848 + 0.0012,
849 + 0.00195,
850 + 0.0019,
851 + 0.0006,
852 + 0.0062,
853 + 0.0027,
854 + 0.0007,
855 + 0.00445,
856 + 0.0003,
857 + 0.004,
858 + 0.00045,
859 + 0.00685,
860 + 0.0022,
861 + 0.00225,
862 + 0.00425,
863 + 0.00105,
864 + 0.00265,
865 + 0.0025,
866 + 0.00255,
867 + 0.0025,
868 + 0.0029,
869 + 0.00355,
870 + 0.006,
871 + 0.00605,
872 + 0.0022,
873 + 0.00235,
874 + 0.00685,
875 + 0.00805,
876 + 0.00195,
877 + 0.00185,
878 + 0.00065,
879 + 0.0012,
880 + 0.00255,
881 + 0.0001,
882 + 0.0041,
883 + 0.0098,
884 + 0.00515,
885 + 0.0017,
886 + 0.00215,
887 + 0.00105,
888 + 0.00465,
889 + 0.00035,
890 + 0.0047,
891 + 0.00315,
892 + 0.0042,
893 + 0.0007,
894 + 0.0022,
895 + 0.00245,
896 + 0.0025,
897 + 0.0023,
898 + 0.00225,
899 + 0.0073,
900 + 0.00185,
901 + 0.00145,
902 + 0.5,
903 + 0.5,
904 + 0.5,
905 + 0.5,
906 + 0.5,
907 + 0.5,
908 + 0.5,
909 + 0.5,
910 + 0.5,
911 + 0.5,
912 + 0.5,
913 + 0.5,
914 + 0.5,
915 + 0.5,
916 + 0.5,
917 + 0.5,
918 + 0.5,
919 + 0.5,
920 + 0.5,
921 + 0.5,
922 + 0.5,
923 + 0.5,
924 + 0.5,
925 + 0.5,
926 + 0.5,
927 + 0.5,
928 + 0.5,
929 + 0.5,
930 + 0.5,
931 + 0.5,
932 + 0.5,
933 + 0.5,
934 + 0.00035,
935 + 0.0011,
936 + 0.00025,
937 + 0.00525,
938 + 0.0002,
939 + 0.0022,
940 + 0.00035,
941 + 0.00205,
942 + 0.0008,
943 + 0.00195,
944 + 0.00055,
945 + 0.0017,
946 + 0.00225,
947 + 0.0004,
948 + 0.0023,
949 + 0.00265,
950 + 0.00475,
951 + 0.00255,
952 + 0.0017,
953 + 0.0014,
954 + 0.00345,
955 + 0.00215,
956 + 0.00865,
957 + 0.00085,
958 + 0.00285,
959 + 0.0049,
960 + 0.0049,
961 + 0.0047,
962 + 0.002,
963 + 0.0033,
964 + 0.0046,
965 + 0.0011,
966 + 0.0021,
967 + 0.00085,
968 + 0.0084,
969 + 0.00055,
970 + 0.0026,
971 + 0.00195,
972 + 0.0016,
973 + 0.00415,
974 + 0.003,
975 + 0.0015,
976 + 0.00405,
977 + 5e-05,
978 + 0.00355,
979 + 0.0054,
980 + 0.00625,
981 + 0.0019,
982 + 0.0064,
983 + 0.0038,
984 + 0.0056,
985 + 0.0075,
986 + 0.00215,
987 + 0.0031,
988 + 0.0008,
989 + 0.00235,
990 + 0.00065,
991 + 0.0012,
992 + 0.001,
993 + 0.0032,
994 + 0.00305,
995 + 0.00495,
996 + 0.0024,
997 + 0.00085,
998 + 0.00585,
999 + 0.00635,
1000 + 0.0025,
1001 + 0.0025,
1002 + 0.00635,
1003 + 0.00495,
1004 + 0.0019,
1005 + 0.0009,
1006 + 0.0023,
1007 + 0.0025,
1008 + 0.004,
1009 + 0.0027,
1010 + 0.0031,
1011 + 0.00985,
1012 + 0.0046,
1013 + 0.00125,
1014 + 0.00025,
1015 + 0.00035,
1016 + 0.00325,
1017 + 0.0017,
1018 + 0.00465,
1019 + 0.00405,
1020 + 0.0008,
1021 + 0.0006,
1022 + 0.0014,
1023 + 0.006,
1024 + 0.0047,
1025 + 0.00215,
1026 + 0.00525,
1027 + 0.00335,
1028 + 0.00185,
1029 + 0.00145,
1030 + 0.5,
1031 + 0.5,
1032 + 0.5,
1033 + 0.5,
1034 + 0.5,
1035 + 0.5,
1036 + 0.5,
1037 + 0.5,
1038 + 0.5,
1039 + 0.5,
1040 + 0.5,
1041 + 0.5,
1042 + 0.5,
1043 + 0.5,
1044 + 0.5,
1045 + 0.5,
1046 + 0.5,
1047 + 0.5,
1048 + 0.5,
1049 + 0.5,
1050 + 0.5,
1051 + 0.5,
1052 + 0.5,
1053 + 0.5,
1054 + 0.5,
1055 + 0.5,
1056 + 0.5,
1057 + 0.5,
1058 + 0.5,
1059 + 0.5,
1060 + 0.5,
1061 + 0.5
1062 + ],
1063 + "max_abs_corr": 1.0,
1064 + "corr_threshold_4sigma": 0.0282842712474619,
1065 + "n_pairs_corr_over_4sigma": 13,
1066 + "expected_pairs_under_null": 1.0378032523876717,
1067 + "argmax_corr_pair": [
1068 + 6,
1069 + 95
1070 + ],
1071 + "chi2_min_p": 0.0,
1072 + "chi2_n_bytes_rejected_bonferroni": 8,
1073 + "chi2_p_by_byte": [
1074 + 0.06794012497381925,
1075 + 0.9422928509658142,
1076 + 0.46566598827449085,
1077 + 0.12194260660365808,
1078 + 0.7209260718249546,
1079 + 0.7565674283018644,
1080 + 0.38272660145823806,
1081 + 0.25379999700317507,
1082 + 0.24892903998464902,
1083 + 0.48367726623149077,
1084 + 0.9039191091028161,
1085 + 0.019627292239171076,
1086 + 0.0,
1087 + 0.0,
1088 + 0.0,
1089 + 0.0,
1090 + 0.9388997695183252,
1091 + 0.0332549564530106,
1092 + 0.5827553829212709,
1093 + 0.5208156204966536,
1094 + 0.95919136684172,
1095 + 0.6104021998003103,
1096 + 0.057092570655725185,
1097 + 0.10240331175616556,
1098 + 0.15099643583459638,
1099 + 0.20419590104856666,
1100 + 0.7295596263369478,
1101 + 0.019627292239171076,
1102 + 0.0,
1103 + 0.0,
1104 + 0.0,
1105 + 0.0
1106 + ],
1107 + "chi2_stat_by_byte": [
1108 + 289.4592,
1109 + 220.4672,
1110 + 256.2816,
1111 + 281.5232,
1112 + 241.35680000000002,
1113 + 238.976,
1114 + 261.12,
1115 + 269.568,
1116 + 269.92639999999994,
1117 + 255.25759999999997,
1118 + 226.048,
1119 + 303.69280000000003,
1120 + 5100000.0,
1121 + 5100000.0,
1122 + 5100000.0,
1123 + 5100000.0,
1124 + 221.056,
1125 + 297.984,
1126 + 249.65120000000002,
1127 + 253.15840000000003,
1128 + 217.0624,
1129 + 248.064,
1130 + 291.6352,
1131 + 284.0064,
1132 + 278.3232,
1133 + 273.4336,
1134 + 240.7936,
1135 + 303.69280000000003,
1136 + 5100000.0,
1137 + 5100000.0,
1138 + 5100000.0,
1139 + 5100000.0
1140 + ]
1141 + },
1142 + "4": {
1143 + "max_bit_bias": 0.011400000000000021,
1144 + "mean_bit_bias": 0.0028624999999999987,
1145 + "bias_threshold_4sigma": 0.01414213562373095,
1146 + "n_bits_bias_over_4sigma": 0,
1147 + "bit_bias": [
1148 + 0.00305,
1149 + 0.0068,
1150 + 0.0035,
1151 + 0.0032,
1152 + 0.0044,
1153 + 0.003,
1154 + 0.0056,
1155 + 0.0034,
1156 + 0.00035,
1157 + 0.0025,
1158 + 0.00085,
1159 + 0.00715,
1160 + 0.0008,
1161 + 0.0034,
1162 + 0.00495,
1163 + 0.00785,
1164 + 0.00155,
1165 + 0.0067,
1166 + 0.00185,
1167 + 0.00035,
1168 + 0.00135,
1169 + 0.0066,
1170 + 0.00155,
1171 + 0.00555,
1172 + 0.00155,
1173 + 0.0018,
1174 + 0.00065,
1175 + 0.00045,
1176 + 0.0042,
1177 + 0.00175,
1178 + 0.00315,
1179 + 0.01015,
1180 + 0.0021,
1181 + 0.0051,
1182 + 0.0048,
1183 + 0.0034,
1184 + 0.00675,
1185 + 0.0049,
1186 + 0.0026,
1187 + 0.00615,
1188 + 0.00335,
1189 + 0.0011,
1190 + 0.00195,
1191 + 0.0004,
1192 + 0.0026,
1193 + 0.00135,
1194 + 0.002,
1195 + 0.0038,
1196 + 0.0031,
1197 + 0.0025,
1198 + 0.003,
1199 + 0.0026,
1200 + 0.003,
1201 + 0.00125,
1202 + 0.0019,
1203 + 0.00285,
1204 + 0.00535,
1205 + 0.00145,
1206 + 0.0013,
1207 + 0.00365,
1208 + 0.0025,
1209 + 0.00255,
1210 + 0.0035,
1211 + 0.0018,
1212 + 0.00095,
1213 + 0.00035,
1214 + 0.00065,
1215 + 0.0005,
1216 + 0.0019,
1217 + 0.0026,
1218 + 0.0009,
1219 + 0.0018,
1220 + 0.00165,
1221 + 0.0004,
1222 + 0.005,
1223 + 0.0022,
1224 + 0.00555,
1225 + 0.00295,
1226 + 0.00185,
1227 + 0.00285,
1228 + 0.00175,
1229 + 0.0053,
1230 + 0.0029,
1231 + 0.00495,
1232 + 0.00355,
1233 + 0.0002,
1234 + 0.0013,
1235 + 0.00185,
1236 + 0.0006,
1237 + 0.0058,
1238 + 0.0091,
1239 + 0.00055,
1240 + 0.00205,
1241 + 0.0001,
1242 + 0.00225,
1243 + 0.00355,
1244 + 0.00675,
1245 + 0.00335,
1246 + 0.00385,
1247 + 0.00175,
1248 + 0.00575,
1249 + 0.0025,
1250 + 5e-05,
1251 + 0.00145,
1252 + 0.001,
1253 + 0.001,
1254 + 0.00145,
1255 + 0.00155,
1256 + 0.0021,
1257 + 0.0098,
1258 + 0.00065,
1259 + 0.00205,
1260 + 0.00245,
1261 + 0.0003,
1262 + 0.00265,
1263 + 0.0026,
1264 + 0.00385,
1265 + 0.0026,
1266 + 0.0009,
1267 + 0.00495,
1268 + 0.00175,
1269 + 0.00455,
1270 + 0.00025,
1271 + 0.00205,
1272 + 0.00225,
1273 + 0.0073,
1274 + 0.00185,
1275 + 0.00145,
1276 + 0.0043,
1277 + 0.00315,
1278 + 0.00085,
1279 + 0.0023,
1280 + 0.0054,
1281 + 0.0029,
1282 + 0.00345,
1283 + 0.00425,
1284 + 0.00275,
1285 + 0.00205,
1286 + 0.00145,
1287 + 0.0011,
1288 + 0.0048,
1289 + 0.0043,
1290 + 0.0024,
1291 + 0.0004,
1292 + 0.00285,
1293 + 0.00015,
1294 + 0.00565,
1295 + 0.0017,
1296 + 0.00305,
1297 + 0.00425,
1298 + 0.005,
1299 + 0.0114,
1300 + 0.0011,
1301 + 0.0027,
1302 + 0.00175,
1303 + 0.0013,
1304 + 0.0007,
1305 + 0.0045,
1306 + 0.005,
1307 + 0.0021,
1308 + 0.0018,
1309 + 0.0002,
1310 + 0.0021,
1311 + 0.00545,
1312 + 0.0017,
1313 + 0.0068,
1314 + 0.0014,
1315 + 0.00285,
1316 + 0.0006,
1317 + 0.0026,
1318 + 0.00125,
1319 + 0.0013,
1320 + 0.00135,
1321 + 0.002,
1322 + 0.00075,
1323 + 0.0028,
1324 + 0.0003,
1325 + 0.0018,
1326 + 0.001,
1327 + 0.0003,
1328 + 0.0024,
1329 + 0.0026,

Diff truncated — file too large.

Menu

Theme