2026-11-01 — Native ECDLP engine rho_engine (Research Axis A, synthetic instances)
Deliverable. experiments/synthetic_ecdlp/rho_engine/ (Rust 1.95 crate, library + binary), fixture
tests/fixtures/ecdlp_instances.json, pytest tests/test_rho_engine.py. Documentation of the iteration
functions, canonical representatives and cycle handling with references: rho_engine/README.md.
Design decisions
- Field arithmetic. p < 2^62 in Montgomery form (R = 2^64, u128 products, one conditional subtraction);
extended-Euclid inversion on i64; Montgomery's simultaneous inversion for every batched phase. Rejected
alternative:
u128 % pper multiplication (compiles to__umodti3, ≈ 10× slower). Exact integer arithmetic everywhere; floats appear only in theexpected_iterationsfigure and timers. - Affine points, per-thread counters
{add, dbl, inv, mul}with the same semantics aswp34.ec.OpCounter(1 inversion per add/dbl; 3 resp. 4 multiplications), plus the batch-inversion multiplications and the Montgomery conversions used by the canonical representatives, so the Rustmulis an upper bound. - Rho iteration. Teske r-adding walk (r = 20 default), partition from a splitmix64 mix of the
Montgomery x residue (multiply-high, no division); distinguished points from the low bits of the same mix.
Walk starts ‖uG + vQ‖ via two 16×16 comb tables (≤ 31 additions, counted). vOW parallelism:
--walkswalks per thread advanced with one simultaneous inversion, sharedMutex<HashMap>of distinguished points, restart after each distinguished point, abandon after 64·2^dp steps. Floyd and Brent single-walk versions use one inversion per step (no batching possible on a sequential chain). - Negation map. Literal rule
y = min(y, p − y)on the standard form (one Montgomery conversion per step; a residue-ordering rule would save it but was not chosen, to keep the representative as specified). 2-cycles: Bos–Kleinjung–Lenstra "next partition" look-ahead (f stays a function of P). Observation: this rule leaves residual 2-cycles at rate ≈ 1/(2r³) per step (a second rejection from R re-uses the partition that produced R); measured 96 two-cycles / 26 649 steps at r = 4 and 1 / 6 569 at r = 20, consistent. They are caught by the periodic check (one remembered point every K = 32 steps, one comparison per step), as are the 4-, 6-, …-cycles (rate ≈ 1/(4r²)) and the 3-cycles of the order-3 automorphism. Escape by doubling the minimum point of the cycle (Bernstein–Lange–Schwabe 2011), restart if the same cycle is met twice. The expected overhead with r = 20 is visible: at 48 bits--negwith r = 20 produced 2 743 fruitless cycles and 181 k look-ahead rejections for 3.3 M steps, versus 162 cycles / 86 k rejections for 10.9 M steps with r = 128 — use--r 128or more with--neg, as BLS recommend. - Order-3 automorphism.
--aut3requires β, λ from the caller (Python computes them) and verifies β³ = 1 (p), λ³ = 1 (n) and (β·gx, gy) = λ·G at start-up (wrong pairs abort with exit 2). Canonical x = min{x, βx, β²x} (standard form), scalars × λ^i; composed with the negation rule for the order-6 orbit. - Kangaroo. vOW tame/wild herds with distinguished points, power-of-two jumps with mean ≈ N√w/4 (Pollard 2000), positions kept mod n, same-herd merges and runaway kangaroos (travel > 3w + 4·m·2^dp) restart at a fresh random position. The runaway guard was added after a test on a 199-element group hung: the jump map's functional graph had disjoint cycles, so the herds never met.
- Determinism. Every RNG is ChaCha20 seeded from (
--seed, thread index, purpose); 1-thread runs are bit-for-bit reproducible (tested). Multi-thread runs are reproducible only in distribution. - Memory / time. Per-thread CPU time by
CLOCK_THREAD_CPUTIME_ID(summed intocpu_s), wall-clock,getrusagepeak RSS, hash-table capacities × entry size. The binary links onlylibSystem/libiconv(arm64 Mach-O, 880 kB), no OpenSSL, so it can be copied to the Mac Studio nodes. - No
rayon.std::thread::scopesuffices (one worker per thread, shared tables behind a mutex); it avoids a dependency with no benefit for this access pattern.
Correctness tests (all passing, 2026-11-01)
cargo test --release: 8 tests — Montgomery arithmetic, batch inversion, Tonelli–Shanks, group law vs repeated addition; small curves (primes 101…32771, prime-order curves found by Legendre point counting, 1 052 (curve, d) pairs brute-forced, each solved by bsgs ×3 settings, rho ×8 settings (Floyd, Brent, dp 1/2 threads, neg, neg+Brent r = 3, neg+mixed, mixed) and kangaroo ×3 interval/thread settings); fixtures (60 instances, 20–32 bits, 10 of them j = 0 per size, generated byscripts/gen_fixtures.pyfromsrc/wp34/ec.py+ PARI/GP, seed 34): bsgs (default and--memory 1000 --threads 2), rho floyd / brent / dp 1 thread / dp 4 threads / neg / neg+brent / mixed 4, aut3 / neg+aut3 / neg+aut3 floyd / neg+aut3 4 threads on j = 0 curves, kangaroo on [0, n), 4 threads, and on a tight interval; 1-thread determinism; wrong β / wrong λ rejected. Runtime 2.2 s..venv/bin/python -m pytest tests/test_rho_engine.py: 553 passed in 3.5 s — the release binary on every fixture × {bsgs, rho floyd, rho brent, rho dp 1 thread, rho dp 4 threads, rho neg, rho mixed, kangaroo [0, n)} and × {aut3, neg+aut3, neg+aut3 brent} on the 20 j = 0 instances; each answer re-verified in pure Python (Curve.mul(d, G) == Q); determinism; batch mode; interval / memory overrides; wrong β abort.- Two bugs found and fixed by the small-curve tests: (i) Brent/Floyd meeting an earlier point of the
augmented trajectory (after an escape) is not an f-cycle —
cycle_lengthis now bounded and the walk restarts; (ii) the kangaroo runaway guard above.
Measured throughput (laptop, Apple M5 Max, 18 cores, macOS 27.0, rustc 1.95.0; single runs, seed 1 unless noted)
Instances: rho_engine/bench/instances.json (40/48/56/62-bit prime-order curves, seed 3434); raw JSON of
every run with host provenance: rho_engine/bench/results-laptop-2026-11-01.json. These are engine
validation numbers (one run each), not the repeated-measurement scaling experiment, which belongs to the
scaling driver.
| instance | algorithm / options | threads | group ops | ops / CPU-s per thread | ops / wall-s | iterations / expected |
|---|---|---|---|---|---|---|
| 48b | rho dp (r 20, 128 walks), seeds 1–3 | 1 | 15.5–31.5 M | 45.8 M | 45.6 M | 0.97 / 1.58 / 1.97 |
| 48b | rho dp --neg --r 128 |
1 | 11.2 M | 30.7 M | 30.6 M | 0.99 |
| 48b | rho dp --neg --r 512 |
1 | 8.1 M | 30.6 M | 30.3 M | 0.72 |
| 48b | kangaroo [0, n) | 1 | 28.4 M | 48.9 M | 48.3 M | 1.14 |
| 48b | bsgs (m = 2^24, 288 MB RSS) | 1 | 24.8 M | 41.1 M | 39.2 M | — |
| 48b | rho Brent (single walk, 1 inversion/step) | 1 | 33.9 M | 9.0 M | 8.9 M | 2.17 |
| 56b | rho dp | 1 | 126 M | 48.5 M | 48.4 M | 0.44 |
| 56b | rho dp --neg --r 128 |
1 | 189 M | 30.5 M | 30.1 M | 0.91 |
| 56b | kangaroo [0, n) | 1 | 757 M | 48.4 M | 47.8 M | 1.65 |
| 56b | rho dp | 2 / 4 / 8 / 12 / 16 | 253 / 503 / 438 / 466 / 620 M | 45.9 / 45.5 / 41.8 / 41.0 / 40.5 M | 89 / 178 / 327 / 483 / 613 M | 0.87–2.14 |
| 62b | rho dp | 16 | 1.82 G | 40.3 M | 608 M | 0.86 (wall 3.0 s) |
| 62b | rho dp --neg --r 128 |
16 | 1.45 G | 26.8 M | 397 M | 0.96 (wall 3.6 s) |
| 62b-j0 | rho dp --neg --aut3 --r 128 |
16 | 0.70 G | 18.2 M | 275 M | 0.76 (wall 2.6 s) |
| 62b | kangaroo [0, n) | 16 | 4.61 G | 39.1 M | 580 M | 1.37 (wall 8.0 s) |
40-bit (runs of 25–60 ms, dominated by start-up): rho 36 M, --neg 25.5 M, --neg --aut3 19.4 M,
kangaroo 46.6 M, bsgs 38.8 M ops / CPU-s per thread; 16 threads 330 M ops / wall-s.
- Target (≥ 20 M group additions / s / thread on a 40-bit curve): met for every mode except the
order-6 class (
--neg --aut3, 18–19 M/s), whose literal canonical rule costs 2 extra multiplications and 4 extra Montgomery conversions per step; it still solves faster per unit time than--negalone because of the √3 reduction in iterations (62b-j0: 2.6 s vs 3.6 s wall). - Per-thread rate falls from 48 M (1 thread) to 40 M (16 threads): the M5 Max has performance and efficiency cores and the DP table mutex is taken once per batch; wall-clock scaling 1 → 16 threads is 12.6× on the 56-bit instance.
- The iterations/expected ratios (0.3–2.2 on single runs) are what the √(πn/2) heavy-tailed distribution predicts; the scaling study must use ≥ 10 seeds per point (CONVENTIONS.md §4).
- Kangaroo on the full interval costs ≈ 2√n vs ≈ 1.25√n for rho, as expected; its per-step cost is lower (no canonicalisation, no restarts).
Usage for the scaling driver
BIN=experiments/synthetic_ecdlp/rho_engine/target/release/rho_engine
$BIN solve --p P --a A --b B --n N --gx GX --gy GY --qx QX --qy QY --alg rho --neg --r 128 --dp 10 \
--threads 16 --seed 7 --expected-d D --json
$BIN batch --file jobs.json --out results.json --verbose # jobs: JSON array with the same keysEach result carries d, ok, ops{add,dbl,inv,mul}, iterations, dp_count, dp_bytes, fruitless_cycles, threads, wall_s, cpu_s, table_entries, peak_rss_bytes, params (big integers as decimal strings).
Classification
Engine construction and validation — known_reproduced (BSGS, Pollard rho with r-adding walks, vOW
distinguished points, negation and order-3/6 classes with fruitless-cycle handling, Pollard kangaroo); no
hypothesis evaluated yet. Open items for later sessions: a residue-ordering variant of the canonical
representatives (speed), Monte-Carlo estimate of the constant in front of √n for each class size with ≥ 10
seeds per point, and the negative control (random mapping) for the rho family.