26 KiB
F4. The weak-day census: 2^24 day keys through MixParams::with_shape
Attack pass row F4 (docs/plans/cryptanalysis.md section 4.2; the gate is section 1.4 (3) and funding.md B5
rank 3; the threat is funding.md B2 rank 3). Run 7 October 2026, 09:10 to 09:55 UK, on igneum-build-1 by the
attack-f4 agent (the verifier timing row of 6.6 queued behind other lanes' holds). Every number below cites its log.
Verdict
PASS on the gate read against M2, the DSP-bound per-day datapath (0 days over 1.1x in 2^28), and on every named weak class; the generous bound M1 (every multiply in LUT adders) exceeds the gate at 3.26e-4 of days as the tail of a sum, not a class, and is routed to main as a bound finding with a rejection-and-redraw rule for the next class. Class v4 is not changed.
Which metric the 1.1x gate reads against, and why: M2. The gate (plan 1.4 (3)) asks for the fraction of days in a weak class, and M1's excess has no class behind it (section 6.2: the exact 16-fold convolution of one random NAF weight predicts the census to 0.6 percent). A per-day FPGA attacker who builds the 16 multiplies in LUT shift-add trees is building the slower design: those trees are 72 percent of M1's cost (167 of 231 adders), and DSP blocks take that cost off the fabric, so the design that wins is DSP-bound, where the day's constants move nothing unless a word has NAF weight at most 3, which happens on no day in 2^28 for two words. M1 is still reported in full because the brief asks for the generous bound, and because a two-line rule closes it for nothing.
| Metric | Days over 1.1x in 2^24 | Fraction | Days over 1.1x in 2^28 | Fraction | Gate 2^-20 = 9.54e-7 | Log |
|---|---|---|---|---|---|---|
| M1: per-day LUT datapath, adders per mixer application, against the census median | 5,476 | 3.264e-4 | 87,426 | 3.257e-4 | OVER, by 342x | census-2p24.md, census-2p28.md gate table |
| M1 exact expectation (16-fold convolution of the NAF-weight table over all 2^31 odd constants) | 5,441 | 3.243e-4 | the census is the tail of a smooth sum, not a class | expect-231.log last line |
||
| M2: DSP-bound datapath, 16/(16 - k), k = words of NAF weight at most 3 | 0 | 0 | 0 | 0 | under | census-2p24.md, census-2p28.md M2 table |
| ROT value and RC value on a per-day datapath | 0 | 0 | 0 | 0 | under (exact 0 ops moved, section 3) | section 3 |
The gate as written fails under M1 only. What M1 finds is not a weak class: the per-day cost of the 16 constant multipliers is a sum of 16 NAF weights (mean 231.1 adder-equivalents per application, sd 6.19), and 1 day in 3,070 sits 3.4 sigma below the median, where a bitstream synthesised for that day pays 10 to 19 percent fewer adders. The worst day in 2^28 reads 1.19x (day 27,952,752, cost 194). The exact expectation predicts the census to 0.6 percent. Section 7 prices the consequence (0.004 percent more hashes a year for an all-LUT FPGA that re-synthesises every day, nothing for a chip or a GPU) and section 8 gives the rejection-and-redraw rule that closes it.
1. Target
| Item | Value |
|---|---|
| Commit | 924288d1 (the brief); the worktree HEAD moved to 11b375a0 during the pass (F5 and F6 records); git diff 924288d1 11b375a0 --stat -- igneum-pow/src is empty, so the target code is the same |
| Code | igneum-pow/src/memhard.rs MixParams::with_shape (lines 189 to 215): SplitMix64::new(key[0] as u64 | (key[1] as u64) << 32), then ROT[0..7] = 1 + below(31), MUL[0..15] = next() as u32 | 1, RC[0..15] = next() as u32; no rejection rule |
| Day key | bind::day_bytes(d) = "igneum-day/" || d_le64, key = seed_words_from_bytes(day_bytes) (the interim day rule, bind.rs lines 30 to 68); the genesis day index is 20,729 (bind.rs test day_bytes_layout) |
| Shape | Shape::for_class(&V4_CLASS): mixer x8, cache 2^26 words, no derivation program (asserted by the harness) |
| Mixer | memhard::mixer: per word (s ^ (RC + rk)) * MUL, then one ChaCha double round with ROT[0..3] on the columns and ROT[4..7] on the diagonals; 72 applications per item under x8 |
| Census set | 2^24 consecutive chain days from 20,729 (the gate run), and 2^28 (the extended run); the first 36,525 of them are the chain's public calendar for the next 100 years under the interim rule |
The 64-bit seeding fact (F7 covers the spec's intent): the 40 draws depend on key[0] | key[1] << 32 alone, so the
stream can produce at most 2^64 distinct parameter sets whatever the key's other 192 bits hold. Over the 2^24 census
days the 64-bit seeds were all distinct (0 collisions, expected 7.6e-6; census-2p24.md "64-bit seeding" line).
below(31) is next() % 31 without rejection: the bias per rotation value is 2^-64 and is ignored.
2. Known-failed shape
A day key whose drawn ROT, MUL or RC gives a fixed datapath a gain over 1.1x: all-equal ROT (31^-7 per day,
MEMHARD.md section 3 item 3, untested until now), MUL = 1 (2^-31 per word), pairs summing to 32, small rotation
amounts, low-weight multipliers, RC + rk = 0.
3. The gain metrics (exact, structural)
The verifier and every GPU run the same instructions on every day (rotate_left by a register amount, wrapping_mul,
no branch on a drawn value), so wall time cannot move with the draw; the only attacker a weak day helps is one who
builds the day's constants into logic. That is an FPGA bitstream synthesised per day (hours of compile against a
public calendar), never a taped-out chip. Costs are in 32-bit adder-equivalents per mixer application:
| Element of one application | Generic datapath | Per-day datapath |
|---|---|---|
16 x s ^ (RC + rk) |
16 | 0 (constant XOR: inverters, absorbed into the next LUT) |
16 x * MUL |
16 multipliers (value-independent) | M1: NAF(MUL_i) - 1 adders each (canonical signed-digit shift-add); M2: a DSP block each, value-independent, except a word of NAF weight at most 3 moves to 2 LUT adders and frees its DSP |
| 8 quarter rounds: 32 adds, 32 XORs | 64 | 64 |
| 32 rotations | 32 barrel shifters | 0 (wiring) |
- M1
cost = 64 + sum_i (NAF(MUL_i) - 1); gain of a day = census median cost / the day's cost. The generous bound: optimal single-constant multiplication is below NAF for every constant and the ratio between days is what is measured. - M2 gain =
16 / (16 - k)on a DSP-bound design, k the words of NAF weight at most 3. - ROT and RC hand a per-day datapath exactly 0 ops at any value (wiring and inverters); on a generic
datapath a rotation costs the same at every amount and
RC + rk = 0removes one XOR of 10,368 ops per item (1.0001x). They are censused as structure, and the worst members are measured for diffusion (section 6), the only other thing a rotation draw could move; a bit-exact verifier never lets a chip skip an application, so diffusion is reported and is not a gain.
4. Harness
| Item | Path or line |
|---|---|
| Crate | tools/attack/f4-weakday/ (Cargo.toml with igneum-pow = { path = "../../../igneum-pow" } and an empty [workspace]; src/main.rs); igneum-pow untouched |
| Build | cd tools/attack/f4-weakday && IGNEUM_AGENT=attack-f4 bash /Users/joshm/Projects/igneum/tools/build-remote.sh --artefacts "target/release/attack-f4" --out <scratch> -- build --release; box binary /srv/builds/igneum-wt-attack/tools/attack/f4-weakday/target/release/attack-f4: sha256 fda006d7...835f52 ran every census and firing (build-1.log); the rebuild 5eb081cf...7f0355 (build-2.log) removes one unused import and nothing else |
| Unit tests | build-remote.sh --no-fetch -- test --release on the box (test-1.log): 2 passed, 0 failed (naf_weights: 0, 1, 3, 7, 2^32 - 1, the alternating maximum 17, and the planted weight-3 constant; genesis_day_draw_matches_memhard_md: the string day 2026-10-03 draws ROT 20 20 19 4 26 3 3 27, MEMHARD.md section 1.1, through the same with_shape path the census uses) |
| Census (gate) | flock -s /srv/builds/_locks/measure -c 'nice -n 10 taskset -c 16-21,64-69 attack-f4 census --from 20729 --count 16777216 --threads 12 --dedupe --out census-2p24.md'; 4.2 s |
| Census (extended) | the same with --count 268435456 --out census-2p28.md; 68.6 s |
| Expectation tables | attack-f4 expect --threads 12 --median 231 (every odd 32-bit constant: NAF weight and popcount, then the 16-fold convolution); 14.9 s |
| One day | attack-f4 day --index <d> --median 231 |
| Firings | attack-f4 plant alleq|mul1|mul1all|mulnaf|rc0|rcrk0 --median 231 (the day 20,729 draw with one field forced through the crate's own hook) |
| Diffusion | attack-f4 avalanche --index <d> --states 2048 [--plant-alleq r] |
| Timing (exclusive hold) | timing.sh on the box under nohup: flock -x -w 7200 /srv/builds/_locks/measure -c 'nice -n 19 taskset -c 16,64 igneum-pow bench --seed x --epoch-hex edc4fa84...fb07 --day-hex <day bytes> --program-class v4 --warps 100' for day 20,729 and the worst day, A B A B. Process note: withdrawing the first attempt, one ad hoc ssh line used pkill -f "<literal>", the banned shape, and killed its own shell (self-match); the relaunch used the bracket form. Nothing else was touched |
| Calendar | attack-f4 census --from 20729 --count 36525 --threads 12 --out census-100y.md (the chain's first 100 years) |
| Box logs | /srv/builds/igneum-wt-attack/attack-f4/{run2.log, census-2p24.md, census-2p28.md, census-100y.md, expect-231.log, firings.log, avalanche.log, timing.log} |
| Mac copies | /private/tmp/claude-501/-Users-joshm/cd75457f-4858-4f86-9634-7481ee056b7b/scratchpad/attack-f4/box/ (the box directory was deleted once from under the pass at about 09:14 UK by another agent's worktree sync; everything was re-run and copied to the Mac the moment it ended; the re-run reproduced the first run line for line) |
5. The two firings (firings.log)
| Case | Classifier | Gain | Result |
|---|---|---|---|
Known-pass: day 20,729 (the genesis day), ROT [6, 25, 5, 25, 29, 11, 9, 21], NAF sum 178 |
no weak class (only "pair sums to 32", 12 and 60 percent of all days) | M1 0.978x, M2 1.000x | passes, as it must |
Known-fail: plant mul1all (all 16 MUL = 1) |
MUL any = 1 FIRED |
M1 3.453x, M2 unbounded | FIRED over 1.1x |
Known-fail: plant mulnaf (four words at NAF weight 3) |
MUL any NAF weight <= 3 FIRED |
M1 1.145x, M2 1.333x | FIRED over 1.1x |
plant mul1 (one word MUL = 1) |
MUL any = 1 FIRED |
M1 1.023x, M2 1.067x | flagged, under the gate: one word of 16 |
plant alleq (ROT all 7) |
ROT all equal FIRED |
M1 0.978x (0 ops moved) | flagged; diffusion in section 6 |
plant rc0, plant rcrk0 |
RC any = 0, RC + rk = 0 FIRED |
M1 0.978x (0 ops moved) | flagged |
6. Numbers
6.1 Classes over 2^24 days (census-2p24.md), with the 2^28 count (census-2p28.md)
Expected per day is analytic (independent draws); the NAF rows come from the exact table of expect-231.log.
| Class | Count 2^24 | Fraction | Expected per day | Expected count 2^24 | Count 2^28 | Worst member (day, M1 cost, M1 gain, M2 gain) |
|---|---|---|---|---|---|---|
| ROT all equal | 0 | 0 | 3.64e-11 (31^-7) | 0.001 | 0 | none |
| ROT distinct <= 3 | 534 | 3.18e-5 | 3.07e-5 | 515 | 8,229 | 2^28: day 49,986,853, 206, 1.121x, 1.000x |
| ROT distinct <= 4 | 26,010 | 1.55e-3 | 1.54e-3 | 25,783 | 412,698 | 2^28: day 208,103,482, 197, 1.173x, 1.000x |
| ROT max multiplicity >= 4 | 35,631 | 2.12e-3 | 2.35e-3 (first order) | 39,421 | 568,423 | 2^28: day 115,569,197, 200, 1.155x, 1.000x |
| ROT same-word pair sums to 32 | 2,062,481 | 0.1229 | 0.1229 | 2,062,288 | 32,997,484 | 2^28: day 97,502,921, 196, 1.179x, 1.000x |
| ROT any pair sums to 32 | 10,022,037 | 0.5974 | 0.6007 (approx., pairs not independent) | 10,078,561 | 160,353,891 | 2^28: day 27,952,752, 194, 1.191x, 1.000x |
| ROT all 8 in {1, 2, 30, 31} | 2 | 1.19e-7 | 7.68e-8 | 1.29 | 19 | day 14,330,190, 217, 1.064x, 1.000x |
| ROT >= 6 in {1, 2, 30, 31} | 1,761 | 1.05e-4 | 1.02e-4 | 1,716 | 27,651 | 2^28: day 181,528,254, 204, 1.132x, 1.000x |
| ROT >= 4 in {8, 16, 24} | 74,541 | 4.44e-3 | 4.46e-3 | 74,756 | 1,196,376 | day 5,517,722, 198, 1.167x, 1.000x |
| MUL any = 1 | 0 | 0 | 7.45e-9 | 0.125 | 4 | 2^28: day 196,441,106, 221, 1.045x, 1.067x |
| MUL any = 2^32 - 1 | 0 | 0 | 7.45e-9 | 0.125 | 1 | 2^28: day 39,988,645, 215, 1.074x, 1.067x |
| MUL any popcount <= 2 | 0 | 0 | 2.38e-7 | 4.0 | 57 | 2^28: day 218,029,468, 209, 1.105x, 1.067x |
| MUL any popcount <= 4 | 612 | 3.65e-5 | 3.72e-5 | 624 | 10,132 | day 7,275,755, 200, 1.155x, 1.000x |
| MUL any NAF weight <= 2 | 4 | 2.38e-7 | 4.62e-7 | 7.75 | 125 | 2^28: day 63,704,833, 205, 1.127x, 1.067x |
| MUL any NAF weight <= 3 | 216 | 1.29e-5 | 1.30e-5 | 218 | 3,515 | 2^28: day 247,161,685, 200, 1.155x, 1.067x |
| MUL any NAF weight <= 4 | 3,637 | 2.17e-4 | 2.20e-4 | 3,683 | 58,667 | 2^28: day 81,133,010, 198, 1.167x, 1.000x |
| MUL any < 256 | 22 | 1.31e-6 | 9.54e-7 | 16 | 262 | 2^28: day 241,187,962, 203, 1.138x, 1.067x |
| MUL two equal | 0 | 0 | 5.59e-8 | 0.94 | 18 | 2^28: day 223,900,428, 226, 1.022x, 1.000x |
| MUL M2 k >= 2 (gain >= 1.143x) | 0 | 0 | 7.9e-11 (C(16,2) x (8.12e-7)^2, approx.) | 0.0013 | 0 | none |
| RC any = 0 | 0 | 0 | 3.73e-9 | 0.062 | 1 | 2^28: day 109,542,046, 243, 0.951x, 1.000x |
| RC any popcount <= 4 or >= 28 | 5,186 | 3.09e-4 | 3.09e-4 | 5,180 | 82,804 | 2^28: day 53,303,116, 206, 1.121x, 1.000x |
| RC + rk = 0 for any of the 72 keys | 10 | 5.96e-7 | 2.68e-7 | 4.5 | 87 | day 3,194,363, 218, 1.060x, 1.000x |
| RC two equal | 1 | 5.96e-8 | 2.79e-8 | 0.47 | 8 | 2^28: day 182,857,055, 222, 1.040x, 1.000x |
Every class sits at its expectation (the largest deviation, "ROT max multiplicity >= 4", is against a first-order bound). The worst member of every class owes its gain to its MUL draw (M1 is a MUL-only quantity); the class itself moves nothing. No day in 2^28 has two words of NAF weight at most 3, so M2 never exceeds 1.067x.
6.2 The M1 tail: census against the exact expectation (census-2p24.md, expect-231.log)
| M1 cost per application | Gain vs median 231 | Days in 2^24 | Cumulative fraction, census | Cumulative fraction, exact |
|---|---|---|---|---|
| 197 (the 2^24 minimum, day 4,819,563) | 1.173x | 1 | 5.96e-8 | 8.18e-8 |
| 200 | 1.155x | 10 | 8.34e-7 | 8.62e-7 |
| 205 | 1.127x | 250 | 2.94e-5 | 2.87e-5 |
| 208 | 1.111x | 1,382 | 1.84e-4 | 1.83e-4 |
| 209 | 1.105x | 2,387 | 3.26e-4 | 3.24e-4 |
| 210 | 1.100x | 3,887 | 5.58e-4 | 5.64e-4 |
| 231 (median) | 1.000x | 1,079,174 | 0.522 | 0.522 |
Mean cost 231.113 (exact 231.111), sd 6.190 (exact 6.190). The 2^28 minimum is 194 (1.191x, day 27,952,752). A
single NAF weight has mean 11.44 and sd 1.55 over the 2^31 odd constants (expect-231.log).
6.3 ROT structure (census-2p24.md histograms)
| Distinct rotation amounts a chip must wire | Days in 2^24 | Fraction | Expected S(8,d) 31_d / 31^8 |
|---|---|---|---|
| 1 | 0 | 0 | 3.63e-11 |
| 2 | 4 | 2.4e-7 | 1.4e-7 |
| 3 | 530 | 3.16e-5 | 3.05e-5 |
| 4 | 25,476 | 1.52e-3 | 1.51e-3 |
| 5 | 421,405 | 0.0251 | 0.0251 |
| 6 | 2,773,843 | 0.1653 | 0.1653 |
| 7 | 7,302,781 | 0.4353 | 0.4351 |
| 8 | 6,253,177 | 0.3727 | 0.3729 |
Small amounts {1, 2, 30, 31} and byte-aligned amounts {8, 16, 24} follow Binomial(8, 4/31) and Binomial(8, 3/31) to within 3 percent in every bin.
6.4 Diffusion of the worst members (avalanche.log: 2,048 states x 512 input bits, mean and minimum per-output-bit flip probability)
| Day | Why | ROT | After 1 application, mean / min | After 2, mean / min |
|---|---|---|---|---|
| 20,729 | genesis, same-word pair 11 + 21 = 32 | 6 25 5 25 29 11 9 21 | 0.461 / 0.383 | 0.500 / 0.498 |
| 4,819,563 | M1 worst in 2^24 | 26 18 8 30 24 24 6 9 | 0.467 / 0.426 | 0.500 / 0.499 |
| 27,952,752 | M1 worst in 2^28 | 11 26 11 7 6 20 20 3 | 0.460 / 0.392 | 0.500 / 0.498 |
| 11,482,247 | 3 distinct amounts, multiplicity 5 | 12 19 19 4 19 12 19 19 | 0.453 / 0.374 | 0.500 / 0.499 |
| 14,330,190 | all 8 amounts in {1, 2, 30, 31} | 1 1 2 31 1 31 1 31 | 0.331 / 0.196 | 0.4995 / 0.497 |
| 332,924 | NAF weight 3 word, three amounts of 1 | 1 23 1 1 12 11 16 18 | 0.458 / 0.370 | 0.500 / 0.498 |
| 196,441,106 | MUL = 1 word (2^28) |
30 24 23 5 11 28 12 9 | 0.461 / 0.364 | 0.500 / 0.499 |
| 109,542,046 | RC = 0 word (2^28) |
28 5 4 31 25 28 12 4 | 0.459 / 0.348 | 0.500 / 0.499 |
| planted all 1 | the worst all-equal draw | 1 x 8 | 0.345 / 0.216 | 0.500 / 0.499 |
| planted all 16 | half-word swaps | 16 x 8 | 0.387 / 0.312 | 0.500 / 0.499 |
| planted all 7 | 7 x 8 | 0.464 / 0.400 | 0.500 / 0.498 |
The slowest draw that can exist (all rotations by 1, probability 31^-8 per day) reaches full avalanche after 2 of the 8 applications between cache reads; the worst real day in 2^28 (all amounts in {1, 2, 30, 31}) the same. No draw gives an attacker a shorter dependency between reads than the round margin F2 measures.
6.5 The chain's first 100 years (census-100y.md: days 20,729 to 57,253 under the interim day rule)
| Item | Value |
|---|---|
| Days over 1.1x under M1 | 6 of 36,525 (1.64e-4; the 2^24 rate predicts 12) |
| First such day | 22,633 (genesis + 1,904 days, about 5.2 years in), cost 208, 1.111x |
| Worst day | 29,337 (genesis + 8,608 days, about 23.6 years in), cost 206, 1.121x |
| Days at exactly 1.100x (cost 210) | 6 more: 25,605; 28,102; 31,573; 33,710; 42,573; 54,884 |
| M2 k >= 2 | 0 |
| Genesis day 20,729 | cost 226, 0.978x; the next four devnet days (20,730 to 20,733) read 0.987x, 1.036x, 0.947x, 0.979x |
| Rotation structure | 1 day with 2 distinct amounts (57,146, genesis + 36,417, cost 225, 1.027x), 46 with 4, none with 3 or fewer otherwise; no day with a MUL of NAF weight under 4 |
6.6 Verifier time (exclusive hold, timing.log)
A confirmation row only: the verifier's code path is value-independent, so the exact metric is the op count above
and a wall-time difference between days can only be noise. Queued on the box at 09:47 UK (timing.sh, nohup, an
exclusive flock -x -w 7200 behind the shared holds of F1, F2, F8, F9, F10 and F7 and the queued exclusive hold of
F6; the first attempt, queued 09:14 UK, was attached to a Mac ssh session and was withdrawn in favour of the nohup
job). Cores 16 and 64, nice 19, --warps 100, day 20,729 against day 4,819,563 (the 2^24 M1 worst), A B A B.
| Day | Cold warp 0 (ms) | Average per warp, 100 warps (ms) |
|---|---|---|
| 20,729 (genesis) | pending (timing.log) |
pending |
| 4,819,563 (M1 worst, 1.173x) | pending (timing.log) |
pending |
The verdict does not rest on this row.
6.7 The worst days in full (worst-days.log, export-29337.log)
The worst day in adders per application against the census median, in each set. M1 is the sum of the 16 NAF weights less 16 plus 64. Every one is an ordinary draw whose 16 weights happen to sum low; none has a word under NAF weight 7.
| Set | Chain day | Years after genesis | ROT | MUL words (hex) | NAF weights | M1 cost | Gain vs median 231 | M2 |
|---|---|---|---|---|---|---|---|---|
| The public calendar, first 36,525 days (what an auditor runs) | 29,337 | 23.6 | 24 12 18 11 14 26 29 21 | 3fe4d03b 227c2043 06011627 40c10137 00234d99 063071d9 91e5abb7 035240b1 f40bfe47 809251b9 1ce999ef 940b381d da13a021 f75f8ba7 3f59bca7 01310e05 | 9 8 9 8 10 10 12 10 9 10 12 11 10 11 11 8 (sum 158) | 206 | 1.121x | 1.000x |
| 2^24 (the gate census) | 4,819,563 | 13,139 | 26 18 8 30 24 24 6 9 | a0653c83 a09de525 810085fb 6a00eba1 bf8205ff bba82079 f27da4c3 2cb80223 6001efcf 1c2814f7 ae9d09d7 ffedd7b7 943dde01 39ff47e1 0513a83f c028eef9 | 11 11 7 10 7 10 12 10 7 9 13 8 8 8 9 9 (sum 149) | 197 | 1.173x | 1.000x |
| 2^28 (extended) | 27,952,752 | 76,481 | 11 26 11 7 6 20 20 3 | f15eb273 227a08f1 20f822e1 6d477779 8d9b3aff 03040503 27fff521 bfd9ce7d 7708000d 5d60ba11 2d40005b f07e10d7 1deefdb1 4881e821 01e1fc71 3ee7c39b | 13 9 8 11 11 7 7 11 7 11 9 9 8 8 7 10 (sum 146) | 194 | 1.191x | 1.000x |
Reproduction, through the harness: attack-f4 day --index 29337 --median 231 (and 4819563, 27952752). Through
igneum-pow itself, with the day bytes "igneum-day/" || d_le64 as hex (day 29,337 = 0x7299):
igneum-pow export --seed x --epoch-hex edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07 --day-hex 69676e65756d2d6461792f9972000000000000 --program-class v4 --out <dir>
writes the day's constants into the pack's memhard.h as IGNEUM_MIX_ROT_INIT and IGNEUM_MIX_MUL_INIT; run on the
box at 09:52 UK (export-29337.log, OVERALL PASS, cache FNV-1a 64 1979492fb76b52ce), the pack's 8 rotations and
16 multipliers equal the harness's word for word. The day-hex strings of the other two days are in section 6.2's
source list (census-2p24.md and census-2p28.md, "The 16 lowest-cost days"): ...2f6b8a490000000000 and
...2f7086aa0100000000.
7. Gate line and consequences
Gate (plan 1.4 (3)): the fraction of days with any gain over 1.1x under 2^-20.
| Model | Fraction over 1.1x | Gate | What the number means per tier |
|---|---|---|---|
| M1 (per-day LUT bitstream) | 3.26e-4 (1 day in 3,070; 2^24 and 2^28 agree; exact expectation 3.24e-4) | FAIL by 342x | An FPGA farm that re-synthesises its bitstream every day gains 10 to 19 percent on those days: 3.26e-4 x about 0.12 = 4e-5 of a year's hashes, 0.004 percent. The FPGA lane is already behind every GPU tier on reads per watt (F5: 10 to 20 M reads/s/W against the gate's 27 M), so no home miner (8, 12, 16, 24 or 32 GB), rig or pool on any vendor or OS sees a competitor appear, and no day's difficulty moves by a measurable amount |
| M2 (DSP-bound FPGA) | 0 in 2^28 | PASS | nothing moves for any tier |
| Chip (programmable constants, the chip-model-v3 recompute chip) | 0 by construction | PASS | nothing moves; a taped-out chip cannot specialise per day |
| GPU and the CPU verifier | 0 by construction | PASS | every tier pays the same ops on every day |
What the worst day buys, priced for the per-day LUT datapath (the M1 attacker) on the worst calendar day, 29,337:
| Item | Value | Source |
|---|---|---|
| Fewer adders per mixer application that day | 231 to 206, 10.8 percent fewer | section 6.7 |
| Item derivations per unit of fabric that day | 1.121x (M1 gain) | section 6.7 |
Hash rate of a recompute FPGA (items derived per hash, the chip-model-v3 ops-per-hash attacker) that day |
up to 12.1 percent above its ordinary day, an upper bound: the 128 dependent cache reads per hash and the shadow block are untouched by the draw, so the whole-hash gain is below the mixer's | chip-model-v3.md section 1 (ops per hash = 128 x 72 x 130); section 3 |
| Hash rate of the stored-dataset (f = 1) FPGA or chip that day | 0 (it derives no items per hash; the mixer is paid once in the daily build) | funding.md B2 rank 2 |
| Days a century at or over 1.1x | 12 (6 over, 6 at exactly 1.100x) | section 6.5 |
| Share of a century's hashes the M1 attacker gains | 12 / 36,525 x about 0.11 = 3.6e-5, 0.004 percent | arithmetic on the rows above |
| What one bitstream a day costs | one place-and-route of a large part: 42 to 160 minutes on a mid-size part (PRflow, FPT 2019, cited in spec 01 section 1.13), hours on a large one; on a rented 96-thread box (Hetzner AX162 class, about USD 0.35 per hour, approximate) under USD 3 per bitstream (approximate), and it compiles any time ahead because the calendar is public | spec 01 section 1.13; price approximate |
So the bitstream is cheap and the gain is 0.004 percent of a century for the slower of the two FPGA designs: nothing a home miner on any card, a rig or a pool on any vendor or OS can see, and nothing that moves a day's difficulty.
What is being done about the M1 line: class v4 is not changed (it is the object on the live devnet's vote). The
rejection-and-redraw rule of section 8 is proposed to main for the next class, unless main reads the census as a
fault beyond the metric (this row does not: every class sits at its expectation and the worst day is an ordinary
draw). The rule costs one redraw on 5.6e-4 of days, changes no existing vector (day 20,729 has NAF sum 178; the first
day the rule would redraw is 22,633, about 5.2 years after genesis, section 6.5), and makes the M1 gate pass by
construction. igneum-pow is untouched by this row.
8. Proposed fix for the next class: a rejection-and-redraw rule on the MUL draw (for main's decision)
Shape, like the program acceptance rule 1.4.6 and DeriveProgram::check: draw the 16 MUL, test, and on rejection
continue the same stream with 16 fresh draws (so every later draw keeps its position only within an accepted block;
RC is drawn after the accepted MUL block). Tests, in order:
| Rule | Threshold | Rejection probability per candidate | What it closes |
|---|---|---|---|
Sum of NAF weights of the 16 MUL at least 163 (M1 cost at least 211, gain at most 1.095x against the median 231) |
sum_i NAF(MUL_i) >= 163 |
5.64e-4 (expect-231.log cumulative at cost 210) |
the M1 tail: no day over 1.1x by construction |
Every MUL of NAF weight at least 4 |
NAF(MUL_i) >= 4 |
1.30e-5 per day | MUL = 1, 2^32 - 1, 2^a +- 1, 2^a +- 2^b +- 1: the M2 words (hygiene; M2 already passes) |
ROT: at least 4 distinct amounts (the DISTINCT_ROTS_FLOOR idea of derive.rs) |
distinct >= 4 |
3.07e-5 per day | the degenerate rotation draws (hygiene; 0 ops moved, diffusion fine at 2 applications) |
Total rejection about 6.1e-4 per day: one redraw every 4.5 years of chain time; MAX_ATTEMPTS-style exhaustion is
impossible in practice (64 rejections in a row at 6e-4 each). Class check to land with it: a unit test in memhard.rs
that plants a low-sum draw (stream seed chosen so the first MUL block fails) and asserts the redraw, plus this
harness re-run over 2^24 showing 0 days over 1.1x under M1 after the rule. The reproduction line for the finding
without the rule: attack-f4 day --index 4819563 --median 231 (cost 197, 1.173x) and
attack-f4 day --index 27952752 --median 231 (cost 194, 1.191x).
Consensus consequence: the rule changes the day-key-to-constants map on rejected days only, so it must land before the
freeze tag. In the chain's first 100 years the sum rule redraws 12 days (6 under 1.1x and 6 at exactly 1.100x,
section 6.5), the first of them 22,633, about 5.2 years after genesis; no pack cut for the devnet, the testnet or the
first five years of mainnet changes. The per-word rule redraws no day in the first 100 years (no word of NAF weight
under 4 in census-100y.md); the ROT rule redraws one, day 57,146 (2 distinct amounts, 99.7 years in).
9. What this row did not do
- It did not time the verifier per day beyond the confirmation row of 6.6: the verifier's code path is value-independent (no branch on a drawn value), so op counts are the exact metric.
- It did not search optimal single-constant multiplication costs (not computable at 2^28 scale); NAF is the standard canonical bound and the ratio between days is what the gate asks.
- It did not census the era draw (F7) or the spec's intent for the 64-bit seeding (F7); the fact is stated in section 1.