# 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.