WP34: foundations, literature, ECDLP/signature studies, Lean formal layer, draft manuscript
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
182 changed files +20,001 −0
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/main.pdf) | |
| 7 | +[](requirements.txt) | |
| 8 | +[](experiments/synthetic_ecdlp/rho_engine) | |
| 9 | +[](formal/) | |
| 10 | +[](results/) | |
| 11 | +[](https://uqo.ca) | |
| 12 | +[](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.