165 lines
13 KiB
Markdown
165 lines
13 KiB
Markdown
# F7: the era-draw bias harness and the era-seed census
|
|
|
|
Attack-pass row F7 (`docs/plans/cryptanalysis.md` 4.2; the record `docs/analysis/attack-pass-2026-10.md`), both halves:
|
|
the fast-time re-roll harness (node lane) and the 2^20 era-seed census plus the 64-bit day-key check (hash lane).
|
|
Written 7 October 2026. Every number cites its log path on igneum-build-1.
|
|
|
|
## Target
|
|
|
|
| Item | Value |
|
|
|---|---|
|
|
| Commit | `924288d1` on branch `attack-pass` (`igneum-pow` is byte-identical at worktree HEAD `11b375a0`: `git diff --stat 924288d1..HEAD -- igneum-pow docs/spec infra/fast-time` is empty) |
|
|
| Spec | `docs/spec/01-lottery-hash.md` 1.13.1 (era seed and draw), 1.8.4 (the day-key mixer stream); `docs/spec/04-seeds-and-vdf.md` 4.4 (era seed pipeline) and 4.6 (T from a reference core); `docs/plans/era-layout.md` sections 1 and 8 (branch `ca2-era`); `docs/analysis/horizon/algorithm.md` 5.4 |
|
|
| Draw code | `igneum_pow::generator::era_draw` over `V3_ALLOWED = [1]` (the chain's path): the width draw (consumed, pinned at 4 bytes), the odd stride multiplier `M`, the rotation `R` in 1..31, the four interleave positions `pos` by partial Fisher-Yates |
|
|
| Day-key code | `igneum_pow::memhard::MixParams::with_shape`: `SplitMix64::new(K[0] \| (K[1] << 32))` draws ROT[0..7], MUL[0..15], RC[0..15]; `K = seed_words_from_bytes("igneum-day/" \|\| day_le64)` (node fork `consensus/pow/src/igneum.rs`, `bind::day_bytes`) |
|
|
| Node draw input (today) | `consensus/src/consensus/mod.rs` `seed_below`: `E_n` is the hash of the last selected-chain block below `15,552,000 n - 7,200` (era 0: genesis). The 1-hour VDF of spec 4.4 and the certified checkpoint it reads do NOT exist in the node (era-layout.md section 8, `proto-vdf` is a prototype) |
|
|
|
|
## Sub-row verdicts
|
|
|
|
| Sub-row | Verdict | Gate (plan 4.2 F7) |
|
|
|---|---|---|
|
|
| (a) re-roll harness | INCOMPLETE, with the written argument | no re-roll inside the publish window |
|
|
| (b) 2^20 era-seed census | PASS | no era class with gain over 1.1x at a fraction over 2^-20 |
|
|
| (c) 64-bit day-key seeding | PASS, within spec intent (one observation recorded) | the draw's input set as the spec states it |
|
|
|
|
## (a) The re-roll harness (node lane)
|
|
|
|
`tools/attack/f7-era/reroll.mjs`: a 3-node fast-time network (`infra/fast-time/override-60x.json` with
|
|
`skip_proof_of_work`, the `class-v4-signal.mjs` shape), own ports 29800 and up, own devnet suffix 980, own data dir
|
|
`/tmp/igneum-fast-time-attack-f7`. The node binary is the ladder fork `vendor/igneum-node-ladder` at `1591ee1d`
|
|
(`igneumd 2.1.0`, already built on the box; read-only). Two honest virtual miners share 1 block/s on nodes 0 and 1; the
|
|
adversary on node 2 holds a block `A` built on the tip at DAA score `S - 1` (the seed block sits there), optionally waits
|
|
a stub VDF of `--vdf-ms`, then publishes `A` to try to make its own block the epoch's seed block (the last selected-chain
|
|
block below the cut `S`). A re-roll succeeds when the epoch's reported seed becomes `hash(A)`.
|
|
|
|
The era cut `15,552,000 n - 7,200` is 180 days of DAA score away on every profile (`POW_ERA_BLOCKS` is a chain constant,
|
|
not an override field), so the harness attacks the EPOCH cut (`60 e - 10` at 60x), which runs the identical `seed_below`
|
|
derivation at a reachable score, one cut per minute. The harness's own era draw (JS) is checked byte-for-byte against the
|
|
Rust census at start: seed `b62532bc...` draws `M 558c0543 R 4 pos [0,1,2,3]` on both (log line "draw self-check ... OK").
|
|
|
|
Firings (both runs 6 cuts, box cores 36-37,84-85 under the shared measure lock):
|
|
|
|
| Run | `--vdf-ms` | Re-rolls to A | Gate | Harness | Log |
|
|
|---|---|---|---|---|---|
|
|
| known-pass | 0 (no delay, the stand-in) | 1 of 6 (epoch 11, seed = A) | FAIL | SOUND (fires) | `/srv/builds/igneum-wt-attack/attack-f7/reroll-knownpass.log` |
|
|
| known-fail | 5,000 (a delay past one block interval) | 0 of 6 | PASS | SOUND (silent) | `/srv/builds/igneum-wt-attack/attack-f7/reroll-knownfail.log` |
|
|
|
|
Both runs: 6 of 6 adversary blocks accepted, all three sinks agree, no reorg of the honest chain. The harness fires on the
|
|
known-pass and is silent on the known-fail, so it is trusted.
|
|
|
|
Written argument (the plan allows one for the VDF's assumptions; the VDF's own delay soundness belongs to the finality
|
|
review row of `funding.md`). The re-roll is possible ONLY when the adversary can evaluate the draw of a candidate input
|
|
inside the block publish window. Today the node has no VDF: `E_n` is a plain block hash, so the input of any candidate
|
|
block is known the instant the block is built, and the harness shows the last-block-before-the-cut is grindable with one
|
|
block of hash (1 of 6 cuts steered in fast time, `--vdf-ms 0`). With any delay past one honest block interval the
|
|
re-roll is gone (`--vdf-ms 5000`: 0 of 6). The design closes this with the 1-hour class-group VDF of spec 4.4: re-rolling
|
|
by withholding needs the 3,600 s VDF evaluated inside the 2 s window, a 1,800x evaluator, and spec 4.6's margin table
|
|
gives 300x as the horizon (`algorithm.md` 5.4; `sim/horizon/algorithm/model.py --section era`). The forge route needs
|
|
2/3 of the 30-day weight, 20 days of 100 percent hash (CLAUDE.md headline). The sub-row is INCOMPLETE because the harness
|
|
cannot demonstrate the real gate: the VDF and the certified checkpoint it reads are not in the node yet (era-layout.md
|
|
section 8 states this). What the harness DOES establish: the C_era cut rule with no delay is grindable, so the era draw's
|
|
soundness rests entirely on the VDF landing before the draw procedure is frozen, and the delay-soundness measurement is
|
|
owed to the finality lane.
|
|
|
|
## (b) The 2^20 era-seed census (hash lane)
|
|
|
|
`tools/attack/f7-era/` (a cargo crate with `igneum-pow` as a path dependency and an empty `[workspace]`; ELF built on the
|
|
box, sha256 `a87818d8...`). `attack-f7 census` runs `era_draw` over `V3_ALLOWED` on `2^n` seeds and classifies each draw;
|
|
`attack-f7 all` runs the plant known-fail case, the census, the spec-stream op-weight census and the day-key check.
|
|
|
|
Known-fail / known-pass of the classifier (planted parameters through a test hook in this crate; log
|
|
`/srv/builds/igneum-wt-attack/attack-f7/census-2p20.log`): every planted weak draw fires its flag (M = 1, M = 2^32-1,
|
|
M = 2^16+1, a naf-2 multiplier, an even M, R = 0, R = 32, pos linear, pos contiguous, pos not ascending) and a sound draw
|
|
(igneum-era-test/0) raises nothing. "Plant verdict: every planted case fired and the sound draw did not."
|
|
|
|
Census results (2^24 = 16,777,216 draws, the stronger run; `census-2p24.log`; the 2^20 run agrees, `census-2p20.log`):
|
|
|
|
| Class | Count (2^24) | Fraction | Expected (uniform) | Chip gain |
|
|
|---|---|---|---|---|
|
|
| M even (bijection failure) | 0 | 0 | 0 | finding if present: none |
|
|
| R out of 1..31 | 0 | 0 | 0 | finding if present: none |
|
|
| pos invalid (not 4 ascending) | 0 | 0 | 0 | finding if present: none |
|
|
| M = 1 (identity stride) | 0 | 0 | 4.66e-10 | 1.0034x |
|
|
| M = 2^32 - 1 | 0 | 0 | 4.66e-10 | 1.0030x |
|
|
| popcount(M) <= 2 | 1 | 5.96e-8 (2^-24) | 1.49e-8 | 1.0030x |
|
|
| popcount(M) <= 4 | 43 | 2.56e-6 (2^-18.6) | 2.33e-6 | 1.0022x |
|
|
| popcount(M) <= 6 | 1,626 | 9.69e-5 | 9.61e-5 | 1.0014x |
|
|
| popcount(M) <= 8 | 27,749 | 1.65e-3 | 1.66e-3 | 1.0007x |
|
|
| naf(M) <= 2 | 1 | 5.96e-8 | - | 1.0030x |
|
|
| naf(M) <= 3 | 18 | 1.07e-6 | - | 1.0026x |
|
|
| M = 2^k + 1 | 1 | 5.96e-8 | 1.44e-8 | 1.0030x |
|
|
| pos linear [0,1,2,3] | 9,257 | 5.52e-4 | 5.50e-4 | 1.0000x |
|
|
| pos contiguous | 120,054 | 7.16e-3 | 7.14e-3 | 1.0000x |
|
|
| pos in the low byte | 645,856 | 3.85e-2 | 3.85e-2 | 1.0000x |
|
|
|
|
The gain metric is the datapath energy a chip saves per hash against the base weights, over the hash's datapath energy
|
|
(19.5 nJ at 100,000 ops x 0.195 pJ, the N5 floor of `algorithm.md` 5.4 / `model.py --section era`). The stride multiply is
|
|
one of three address operations, run 128 times per hash (16 loads x 8 iterations); a low-weight `M` replaces the multiplier
|
|
with a few shift-adds, worth at most 128 x 0.52 pJ = 67 pJ, so M = 1 is the richest corner at 1.0034x. The rotation is a
|
|
wire mux and the interleave an address-line permute, 0 pJ on the modelled chip. No drawn parameter touches the memory
|
|
bound, the item derivation, the load count or N.
|
|
|
|
Gate: no class with gain over 1.1x at a fraction over 2^-20. The richest gain in the whole classifier is 1.0034x (M = 1),
|
|
and M = 1 did not occur in 2^24 draws (expected 4.66e-10). Every class at a fraction over 2^-20 has gain 1.0000x to
|
|
1.0007x. PASS on both counts.
|
|
|
|
Uniformity of the draw (2^24): stride rotation R over 1..31 chi-square 38.5 on 30 dof (max bucket deviation 2.07 sigma,
|
|
R = 0 or 32 seen 0 times); interleave pos 1,820 of 1,820 four-subsets seen, chi-square 1,775.7 on 1,819 dof (max deviation
|
|
3.63 sigma, 0 draws with a non-4-subset); M bit 0 always set (odd by construction), bits 1..31 each set in 0.500 of draws
|
|
(worst bit 1.81 sigma); the stride bijection never failed (0 even M). The era stream's own 64-bit seed (words 0 and 1) was
|
|
distinct on all 2^24 draws.
|
|
|
|
Op-weight corners (spec 1.13.1 first stream, implemented in `attack-f7 spec` from the spec text because `igneum-pow` does
|
|
not draw the op-weight perturbation at this commit; 2^20 draws, `census-2p20.log`): the ten non-load weights each
|
|
perturbed by -2..+2 and renormalised to 75 move the multiply share (mul+mad+mulhi, base 22 of 75) between 15 and 31. The
|
|
richest corner for a chip is 15/75 (0.152 pJ per op, -22 percent of the base datapath), seen once in 2^20; 16/75 at
|
|
3.22e-3. The GPU's energy moves the same way (its IMAD is the chain's own op), so the chip-against-GPU gain of every
|
|
weight corner is 1.0x, with 0 memory effect. Renormalised sums were 75 on every draw (0 failures). Fold rotations: a triple
|
|
all equal 2.13e-3, both triples all equal 1.91e-6, all six equal 0; uniform over 1..31, rotation 0 never drawn; a wire
|
|
mux, 1.0x.
|
|
|
|
## (c) The 64-bit seeding of the day-key stream (hash lane)
|
|
|
|
`attack-f7 days` over days 0..131,072 (`census-2p20.log`). The day key `K` is `seed_words_from_bytes("igneum-day/" ||
|
|
day_le64)`: a calendar function, no chain state. All 256 bits of `K` enter the cache fill (spec 1.8.3, `K[0..7]` in every
|
|
block input), so the dataset depends on the full key; the mixer-constant stream (ROT, MUL, RC) is seeded from `K[0] |
|
|
(K[1] << 32)`, 64 bits, which is the spec's stated intent (spec 1.8.4).
|
|
|
|
| Quantity | Value |
|
|
|---|---|
|
|
| Days the chain can have | about 65,745 in 180 years at 1 block/s (2^16.0) |
|
|
| Distinct 256-bit keys K over 2^17 days | 131,072 (all) |
|
|
| Distinct 64-bit stream seeds over 2^17 days | 131,072 (0 duplicates) |
|
|
| Distinct (ROT, MUL, RC) tuples over 2^17 days | 131,072 |
|
|
| Birthday bound on a 64-bit collision among 2^16 days | 2^(32 - 65) = 2^-33 |
|
|
|
|
The spec intends 64 bits for the mixer-constant draw, and the truncation is not a reduction of the draw space the public report
|
|
would flag: at most 2^16 days are ever drawn, each a distinct calendar day with a distinct 64-bit seed (0 collisions in
|
|
2^17), so no two days share a mixer. One observation, within spec intent and recorded for the written argument of
|
|
`funding.md` B5 rank 6: the mixer-constant stream has 64 bits of seed entropy, so at most 2^64 distinct daily mixers are
|
|
reachable (not the ~2^1,047 nominal); this is not exploitable (the days used are 2^16, all distinct) and whether any
|
|
reachable tuple is weak is the separate weak-day census of row F4.
|
|
|
|
## Consequences per tier
|
|
|
|
The era draw and the day-key seeding are protocol-wide and do not differ by card tier: the load width and load count are
|
|
pinned, so every era is equally memory-bound and no 8, 12, 16 or 24/32 GB card is advantaged or disadvantaged by any draw
|
|
(the measured six-era hash-rate spread is 1.3 percent on the RTX 5090, 3.2 on the RX 9070 XT, 0.8 on the M5 Max,
|
|
`algorithm.md` 5.4). No drawn era parameter or day key makes a chip cheaper against a GPU: the richest datapath corner is
|
|
1.0034x and is shared with the GPU. The one operational consequence is for the protocol, not a miner tier: the era draw's
|
|
grinding resistance is not yet demonstrable because the 1-hour VDF and its certified checkpoint are not in the node, so
|
|
the freeze of the draw procedure and the C_era cut rule must wait on the VDF landing and the finality lane's delay-
|
|
soundness measurement.
|
|
|
|
## Gate line
|
|
|
|
- (a) harness: INCOMPLETE. No re-roll with a one-block delay (known-fail 0 of 6); a re-roll with no delay (known-pass 1 of
|
|
6). The real gate (no re-roll inside the 2 s window) rests on the 1-hour VDF, which is not in the node; written argument
|
|
above.
|
|
- (b) census: PASS. No era class with gain over 1.1x at any fraction (richest 1.0034x, M = 1, absent in 2^24); the draw is
|
|
a bijection on every sample and uniform in R, pos and the M bits.
|
|
- (c) 64-bit seeding: PASS within spec intent. The spec intends 64 bits for the mixer stream; 2^16 days are all distinct;
|
|
the one observation (2^64 reachable mixers) is recorded, not a flaw.
|
|
|
|
What a failure moves (plan 4.2 F7): the draw procedure or the C_era cut rule; a redraw rule for the era stream. Nothing in
|
|
(b) or (c) moves them. (a) moves nothing in shipped code but gates the freeze of the draw procedure on the VDF.
|