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
Large diff (85 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/reduced_round_sha256/diffusion.py
+113 −0
Large diff (113 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/reduced_round_sha256/local_collision.py
+232 −0
Large diff (232 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/reduced_round_sha256/make_summary.py
+141 −0
Large diff (141 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/reduced_round_sha256/preimage_sat.py
+176 −0
Large diff (176 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/reduced_round_sha256/run_cluster.sh
+47 −0
Large diff (47 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/reduced_round_sha256/solvers.py
+177 −0
Large diff (177 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/signature_benchmarks/README.md
+79 −0
Large diff (79 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/signature_benchmarks/python_baselines.py
+119 −0
Large diff (119 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/signature_benchmarks/python_crosscheck.py
+72 −0
Large diff (72 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/signature_benchmarks/run_bench.py
+134 −0
Large diff (134 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/signature_benchmarks/sigbench/Cargo.lock
+784 −0
Large diff (784 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/signature_benchmarks/sigbench/Cargo.toml
+22 −0
Large diff (22 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/signature_benchmarks/sigbench/src/main.rs
+584 −0
Large diff (584 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/signature_benchmarks/sigbench/src/msm.rs
+362 −0
Large diff (362 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/signature_benchmarks/sigbench/src/stats.rs
+105 −0
Large diff (105 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/signature_benchmarks/sigbench/src/vectors.rs
+151 −0
Large diff (151 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/signature_benchmarks/summarize.py
+80 −0
Large diff (80 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/synthetic_ecdlp/analyze_ecdlp.py
+381 −0
Large diff (381 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/synthetic_ecdlp/ecdlp_study.py
+128 −0
Large diff (128 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/synthetic_ecdlp/index_calculus_probe.py
+72 −0
Large diff (72 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/synthetic_ecdlp/preprocessing_python.py
+81 −0
Large diff (81 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/synthetic_ecdlp/rho_engine/.gitignore
+1 −0
Large diff (1 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/synthetic_ecdlp/rho_engine/Cargo.lock
+407 −0
Large diff (407 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/synthetic_ecdlp/rho_engine/Cargo.toml
+33 −0
Large diff (33 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/synthetic_ecdlp/rho_engine/README.md
+184 −0
Large diff (184 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/synthetic_ecdlp/rho_engine/bench/instances.json
+93 −0
Large diff (93 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/synthetic_ecdlp/rho_engine/bench/results-laptop-2026-11-01.json
+786 −0
Large diff (787 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/synthetic_ecdlp/rho_engine/scripts/gen_bench_instances.py
+27 −0
Large diff (27 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/synthetic_ecdlp/rho_engine/scripts/gen_fixtures.py
+58 −0
Large diff (58 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/synthetic_ecdlp/rho_engine/src/bsgs.rs
+233 −0
Large diff (233 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/synthetic_ecdlp/rho_engine/src/curve.rs
+243 −0
Large diff (243 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/synthetic_ecdlp/rho_engine/src/field.rs
+353 −0
Large diff (353 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/synthetic_ecdlp/rho_engine/src/instance.rs
+128 −0
Large diff (128 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/synthetic_ecdlp/rho_engine/src/kangaroo.rs
+288 −0
Large diff (288 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/synthetic_ecdlp/rho_engine/src/lib.rs
+254 −0
Large diff (254 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/synthetic_ecdlp/rho_engine/src/main.rs
+206 −0
Large diff (206 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/synthetic_ecdlp/rho_engine/src/rho.rs
+792 −0
Large diff (792 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/synthetic_ecdlp/rho_engine/src/stats.rs
+62 −0
Large diff (62 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/synthetic_ecdlp/rho_engine/tests/fixtures.rs
+180 −0
Large diff (180 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/synthetic_ecdlp/rho_engine/tests/small_curves.rs
+169 −0
Large diff (169 lines) is not loaded by default to keep the page fast.Load diff
added
experiments/synthetic_ecdlp/run_batch_remote.py
+45 −0
Large diff (45 lines) is not loaded by default to keep the page fast.Load diff
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
Large diff (189 lines) is not loaded by default to keep the page fast.Load diff
added
figures/make_sig_figures.py
+186 −0
Large diff (186 lines) is not loaded by default to keep the page fast.Load diff
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
Large diff (1 lines) is not loaded by default to keep the page fast.Load diff
added
formal/README.md
+213 −0
Large diff (213 lines) is not loaded by default to keep the page fast.Load diff
added
formal/Wp34.lean
+7 −0
Large diff (7 lines) is not loaded by default to keep the page fast.Load diff
added
formal/Wp34/Audit.lean
+46 −0
Large diff (46 lines) is not loaded by default to keep the page fast.Load diff
added
formal/Wp34/Group.lean
+252 −0
Large diff (252 lines) is not loaded by default to keep the page fast.Load diff
added
formal/Wp34/LocalCollision.lean
+262 −0
Large diff (262 lines) is not loaded by default to keep the page fast.Load diff
added
formal/Wp34/Schedule.lean
+55 −0
Large diff (55 lines) is not loaded by default to keep the page fast.Load diff
added
formal/Wp34/Sha256Step.lean
+122 −0
Large diff (122 lines) is not loaded by default to keep the page fast.Load diff
added
formal/Wp34/Sha256Words.lean
+370 −0
Large diff (370 lines) is not loaded by default to keep the page fast.Load diff
added
formal/build.log
+133 −0
Large diff (133 lines) is not loaded by default to keep the page fast.Load diff
added
formal/lake-manifest.json
+6 −0
Large diff (6 lines) is not loaded by default to keep the page fast.Load diff
added
formal/lakefile.toml
+6 −0
Large diff (6 lines) is not loaded by default to keep the page fast.Load diff
added
formal/lean-toolchain
+1 −0
Large diff (1 lines) is not loaded by default to keep the page fast.Load diff
added
formal/scripts/aig_sizes.py
+50 −0
Large diff (50 lines) is not loaded by default to keep the page fast.Load diff
added
formal/scripts/aig_sizes.tsv
+64 −0
Large diff (64 lines) is not loaded by default to keep the page fast.Load diff
added
formal/scripts/gf2_rank.py
+103 −0
Large diff (103 lines) is not loaded by default to keep the page fast.Load diff
added
formal/scripts/local_collision.py
+77 −0
Large diff (77 lines) is not loaded by default to keep the page fast.Load diff
added
hypotheses/H01-standardised-secp256k1-parameters-and-glv-consta.md
+29 −0
Large diff (29 lines) is not loaded by default to keep the page fast.Load diff
added
hypotheses/H02-end-e----z-\317\211--and-the-glv-basis-is-the-eisenstei.md
+35 −0
Large diff (35 lines) is not loaded by default to keep the page fast.Load diff
added
hypotheses/H03-glv-decomposition-halves-doublings.md
+30 −0
Large diff (30 lines) is not loaded by default to keep the page fast.Load diff
added
hypotheses/H04-automorphism-orbits-give-exactly--6-on-pollard-r.md
+33 −0
Large diff (33 lines) is not loaded by default to keep the page fast.Load diff
added
hypotheses/H05-the-automorphism-orbit-set-cannot-be-made-a-grou.md
+29 −0
Large diff (29 lines) is not loaded by default to keep the page fast.Load diff
added
hypotheses/H06-no-known-algebraic-attack-applies-to-secp256k1.md
+38 −0
Large diff (38 lines) is not loaded by default to keep the page fast.Load diff
added
hypotheses/H07-an-exhaustive-small-coefficient-identity-search.md
+31 −0
Large diff (31 lines) is not loaded by default to keep the page fast.Load diff
added
hypotheses/H08-measured-ecdlp-cost-scales-as-n-0-5-with-constan.md
+36 −0
Large diff (36 lines) is not loaded by default to keep the page fast.Load diff
added
hypotheses/H09-automated-search-over-iteration-function-designs.md
+32 −0
Large diff (32 lines) is not loaded by default to keep the page fast.Load diff
added
hypotheses/H10-parallel-distinguished-point-collision-search-ha.md
+27 −0
Large diff (27 lines) is not loaded by default to keep the page fast.Load diff
added
hypotheses/H11-time-memory-trade-off.md
+29 −0
Large diff (29 lines) is not loaded by default to keep the page fast.Load diff
added
hypotheses/H12-discrete-logarithms-with-preprocessing.md
+30 −0
Large diff (30 lines) is not loaded by default to keep the page fast.Load diff
added
hypotheses/H13-bip340-batch-verification-reduces-per-signature.md
+33 −0
Large diff (33 lines) is not loaded by default to keep the page fast.Load diff
added
hypotheses/H14-implementation-choice-dominates-signature-perfor.md
+28 −0
Large diff (28 lines) is not loaded by default to keep the page fast.Load diff
added
hypotheses/H15-a-single-bit-message-difference-reaches-full-ava.md
+25 −0
Large diff (25 lines) is not loaded by default to keep the page fast.Load diff
added
hypotheses/H16-statistically-detectable-input-output-correlatio.md
+25 −0
Large diff (25 lines) is not loaded by default to keep the page fast.Load diff
added
hypotheses/H17-sat-preimage-search-on-step-reduced-sha-256-scal.md
+28 −0
Large diff (28 lines) is not loaded by default to keep the page fast.Load diff
added
hypotheses/H18-generic-sat-encodings-find-step-reduced-collisio.md
+33 −0
Large diff (33 lines) is not loaded by default to keep the page fast.Load diff
added
hypotheses/H19-the-9-step-local-collision-holds-with-probabilit.md
+28 −0
Large diff (28 lines) is not loaded by default to keep the page fast.Load diff
added
hypotheses/H20-the-sha-256-step-is-a-bijection-on-the-state-and.md
+35 −0
Large diff (35 lines) is not loaded by default to keep the page fast.Load diff
added
hypotheses/H21-lean-4-bv-decide-machine-checks-the-sha-256-word.md
+30 −0
Large diff (30 lines) is not loaded by default to keep the page fast.Load diff
added
hypotheses/H22-\317\2030-and-\317\2031-are-not-injective-on-32-bit-words--ker.md
+30 −0
Large diff (30 lines) is not loaded by default to keep the page fast.Load diff
added
hypotheses/H23-cross-implementation-agreement-is-a-necessary-ga.md
+33 −0
Large diff (33 lines) is not loaded by default to keep the page fast.Load diff
added
hypotheses/H24-the-six-stage-pipeline-rediscovers-known-structu.md
+27 −0
Large diff (27 lines) is not loaded by default to keep the page fast.Load diff
added
hypotheses/H25-ecdsa-malleability-and-schnorr-correctness-follo.md
+31 −0
Large diff (31 lines) is not loaded by default to keep the page fast.Load diff
added
literature/db.yaml
+662 −0
Large diff (662 lines) is not loaded by default to keep the page fast.Load diff
added
literature/records.md
+68 −0
Large diff (68 lines) is not loaded by default to keep the page fast.Load diff
added
literature/research_gaps.md
+139 −0
Large diff (139 lines) is not loaded by default to keep the page fast.Load diff
added
literature/unverified.md
+25 −0
Large diff (25 lines) is not loaded by default to keep the page fast.Load diff
added
paper/.latexmkrc
+5 −0
Large diff (5 lines) is not loaded by default to keep the page fast.Load diff
added
paper/Makefile
+11 −0
Large diff (11 lines) is not loaded by default to keep the page fast.Load diff
added
paper/appendix/app_results.tex
+1 −0
Large diff (1 lines) is not loaded by default to keep the page fast.Load diff
added
paper/appendix/appendix.tex
+80 −0
Large diff (80 lines) is not loaded by default to keep the page fast.Load diff
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
Large diff (31 lines) is not loaded by default to keep the page fast.Load diff
added
paper/figures/fig_pipeline.tex
+21 −0
Large diff (21 lines) is not loaded by default to keep the page fast.Load diff
added
paper/figures/fig_secp256k1_structure.tex
+13 −0
Large diff (13 lines) is not loaded by default to keep the page fast.Load diff
added
paper/figures/fig_sha256_architecture.tex
+27 −0
Large diff (27 lines) is not loaded by default to keep the page fast.Load diff
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
Large diff (140 lines) is not loaded by default to keep the page fast.Load diff
added
paper/references.bib
+1438 −0
Large diff (1,438 lines) is not loaded by default to keep the page fast.Load diff
added
paper/sections/01_introduction.tex
+75 −0
Large diff (75 lines) is not loaded by default to keep the page fast.Load diff
added
paper/sections/02_foundations.tex
+151 −0
Large diff (151 lines) is not loaded by default to keep the page fast.Load diff
added
paper/sections/03_literature.tex
+141 −0
Large diff (141 lines) is not loaded by default to keep the page fast.Load diff
added
paper/sections/04_secp256k1.tex
+150 −0
Large diff (150 lines) is not loaded by default to keep the page fast.Load diff
added
paper/sections/05_dlog_algorithms.tex
+95 −0
Large diff (95 lines) is not loaded by default to keep the page fast.Load diff
added
paper/sections/06_signatures.tex
+93 −0
Large diff (93 lines) is not loaded by default to keep the page fast.Load diff
added
paper/sections/07_sha256.tex
+72 −0
Large diff (72 lines) is not loaded by default to keep the page fast.Load diff
added
paper/sections/08_discovery_framework.tex
+71 −0
Large diff (71 lines) is not loaded by default to keep the page fast.Load diff
added
paper/sections/09_experimental_design.tex
+82 −0
Large diff (82 lines) is not loaded by default to keep the page fast.Load diff
added
paper/sections/10_results.tex
+244 −0
Large diff (244 lines) is not loaded by default to keep the page fast.Load diff
added
paper/sections/10b_results_sha.tex
+1 −0
Large diff (1 lines) is not loaded by default to keep the page fast.Load diff
added
paper/sections/11_formal_negative.tex
+38 −0
Large diff (38 lines) is not loaded by default to keep the page fast.Load diff
added
paper/sections/11a_formal.tex
+48 −0
Large diff (48 lines) is not loaded by default to keep the page fast.Load diff
added
paper/sections/12_implications.tex
+52 −0
Large diff (52 lines) is not loaded by default to keep the page fast.Load diff
added
paper/sections/13_limitations.tex
+27 −0
Large diff (27 lines) is not loaded by default to keep the page fast.Load diff
added
paper/sections/14_conclusion.tex
+32 −0
Large diff (32 lines) is not loaded by default to keep the page fast.Load diff
added
paper/sections/abstract.tex
+1 −0
Large diff (2 lines) is not loaded by default to keep the page fast.Load diff
added
paper/sections/titlepage.tex
+77 −0
Large diff (77 lines) is not loaded by default to keep the page fast.Load diff
added
paper/tables/ecdlp_fits.tex
+12 −0
Large diff (12 lines) is not loaded by default to keep the page fast.Load diff
added
paper/tables/ecdlp_j0.tex
+10 −0
Large diff (10 lines) is not loaded by default to keep the page fast.Load diff
added
paper/tables/ecdlp_parallel.tex
+11 −0
Large diff (11 lines) is not loaded by default to keep the page fast.Load diff
added
paper/tables/ecdlp_scaling.tex
+48 −0
Large diff (48 lines) is not loaded by default to keep the page fast.Load diff
added
paper/tables/ecdlp_sweep_r.tex
+17 −0
Large diff (17 lines) is not loaded by default to keep the page fast.Load diff
added
paper/tables/ecdlp_tmto.tex
+19 −0
Large diff (19 lines) is not loaded by default to keep the page fast.Load diff
added
paper/tables/hypotheses.tex
+32 −0
Large diff (32 lines) is not loaded by default to keep the page fast.Load diff
added
paper/tables/index_calculus_probe.tex
+12 −0
Large diff (12 lines) is not loaded by default to keep the page fast.Load diff
added
paper/tables/numbers.tex
+68 −0
Large diff (68 lines) is not loaded by default to keep the page fast.Load diff
added
paper/tables/numbers_ecdlp.tex
+142 −0
Large diff (142 lines) is not loaded by default to keep the page fast.Load diff
added
paper/tables/preprocessing.tex
+15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added
paper/tables/scalar_mult_opcount.tex
+10 −0
Large diff (10 lines) is not loaded by default to keep the page fast.Load diff
added
paper/tables/sig_implementations.tex
+13 −0
Large diff (13 lines) is not loaded by default to keep the page fast.Load diff
added
paper/uq_logo.jpg
+0 −0
Binary file not shown.
added
requirements.txt
+15 −0
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
added
research_log/2026-11-01-ecdlp-studies.md
+8 −0
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
added
research_log/2026-11-01-formal.md
+71 −0
Large diff (71 lines) is not loaded by default to keep the page fast.Load diff
added
research_log/2026-11-01-literature.md
+48 −0
Large diff (48 lines) is not loaded by default to keep the page fast.Load diff
added
research_log/2026-11-01-rho-engine.md
+123 −0
Large diff (123 lines) is not loaded by default to keep the page fast.Load diff
added
research_log/2026-11-01-sha256.md
+94 −0
Large diff (94 lines) is not loaded by default to keep the page fast.Load diff
added
research_log/2026-11-01-signatures.md
+131 −0
Large diff (131 lines) is not loaded by default to keep the page fast.Load diff
added
research_log/discovery_log.jsonl
+25 −0
Large diff (25 lines) is not loaded by default to keep the page fast.Load diff
added
results/automated_discovery/hypothesis_summary.json
+19 −0
Large diff (20 lines) is not loaded by default to keep the page fast.Load diff
added
results/automated_discovery/identity_search.json
+211 −0
Large diff (212 lines) is not loaded by default to keep the page fast.Load diff
added
results/baselines/scalar_mult_opcount.json
+90 −0
Large diff (91 lines) is not loaded by default to keep the page fast.Load diff
added
results/baselines/secp256k1_facts.json
+148 −0
Large diff (149 lines) is not loaded by default to keep the page fast.Load diff
added
results/reduced_round_sha256/bias_seed1.json
+1329 −0
Large diff (1,329 lines) is not loaded by default to keep the page fast.Load diff
Diff truncated — file too large.