13 KiB
Attack plan: the statistical distinguisher and the round margin of M_r
Internal adversarial pass, not an independent review. Lane adv-mixer-3, branch adv-mixer-3 from build/master
(7a7caa34). Written 7 October 2026, 19:20 to 20:20 BST. Every sentence here that could be quoted in public
carries the label: internal adversarial pass, not an independent review.
0. The outsider rule, applied
| Check | Result |
|---|---|
| Frozen commit | 017e703764 (class v4 sub-version 3, object byte 7) |
| Base commit | build/master 7a7caa34 (fetched 19:25 BST, the current mirror; the earlier master 3f0afcd5 differed from the frozen crate in 6 files and was never used here) |
git diff --quiet 017e7037 HEAD -- igneum-pow && echo IDENTICAL |
prints IDENTICAL. The crate is byte-identical to the frozen commit. The harness depends on the worktree's igneum-pow by path |
| Attacker | an outsider with the public kit. I have never worked on the hash code. No defender number is read; every figure below is derived from the crate, the spec or the chip model, or marked unknown |
| Worktree | /Users/joshm/Projects/igneum-wt-adv-mixer-3, harness under tools/attack/adv-mixer-3/ |
| Boxes | the CPU-bound sweeps on build-1 explicitly (--box 1, logs under /srv/builds/igneum-wt-adv-mixer-3/adv/ on build-1), the rest on build-2, both kept busy until the queue is empty, nice 10, yield to every build- and measure lock (SIGSTOP/SIGCONT, 5 s poll) |
1. The target, restated from the spec and the code
The mixer M (spec 1.8.4, memhard::mixer) acts on a 16-word state of 32-bit words with a 32-bit round key rk.
layer 1, per word i in 0..15: s[i] = (s[i] XOR (RC[i] + rk)) * MUL[i] MUL[i] odd
layer 2, one double round: QR(0,4,8,12) QR(1,5,9,13) QR(2,6,10,14) QR(3,7,11,15) rotations ROT[0..3]
QR(0,5,10,15) QR(1,6,11,12) QR(2,7,8,13) QR(3,4,9,14) rotations ROT[4..7]
QR is the ChaCha quarter round: add, xor, rotate, four times. ROT (8 values in 1..31), MUL (16 odd words) and RC
(16 words) are drawn once per day from one SplitMix64 stream seeded with K[0] | K[1] << 32, K the day key
seed_words_from_bytes("igneum-day/" || le64(day)) (bind::day_bytes). The round key of application k is
round_key(k) = (k + 1) * 0x9E3779B9, k in 0..71. The keys are fixed. Only rk changes between applications.
Item t under class v4 (V4_CLASS = MX8: mixer_mult 8, growth on, no derivation program; derive_items_mask):
s[0..7] = K
s[8 + i] = t * MUL[i] + RC[i] i in 0..7
for r in 0..7:
8 applications, keys round_key(8 r + j), j in 0..7
a = s[0] AND 0x3fffff the line index (2^22 lines at genesis)
s ^= cache[line a]
8 applications, keys round_key(64 + j)
item(t) = s
72 applications per item, 8 between dependent cache reads. The mixer is a bijection on 512 bits for every key, so inverting one or many applications is free. A shortcut must therefore come from structure, not from inversion.
Two input regimes matter and I treat them apart.
| Regime | Input to the 8 applications | Why it matters |
|---|---|---|
| Round 0 | 8 fixed words K and 8 words affine in one 32-bit t | the whole block is a function of 32 bits. The first line index is a map of 32 bits to 22 bits. It can be censused exhaustively |
| Rounds 1 to 8 | the previous state XORed with a cache line | the input is spread over 512 bits. Statistical tests on random states apply |
Cost model: chip-model-v3.md prices the honest item at 9,360 ops (72 x 130). A shortcut of k applications out of 72 is worth at most k / 72 of that, so k = 8 is 11 percent of the mixer cost, and the chip's cost is also the 8 cache reads, which no mixer result removes. A distinguisher is a soundness result (the day's dataset is not what the design assumes), priced separately from a shortcut.
2. The questions, in attack order
| Rank | Q | Question | Result shape |
|---|---|---|---|
| 1 | Q1 | The line-index census of round 0: over all 2^32 t, how is s[0] AND 0x3fffff distributed after k applications, k = 1..8? At what k is it uniform (chi-square, empty bins, max load, per-bit balance)? |
largest k with a measurable non-uniformity; k = 8 is the real read |
| 2 | Q2 | Single-bit avalanche across k applications at high sample count: largest k at which any (in, out) cell has bias above the census band, and the decay curve of the worst bias with k | the round margin from bias |
| 3 | Q3 | Differential: for low-weight input differences, the most frequent full output difference and its probability after k applications; the truncated differential (unchanged output words) | largest k with a differential above 2^-16 |
| 4 | Q4 | Linear: single-bit mask correlations c(u, v) after k applications at 2^24 samples | largest k with a correlation above the band |
| 5 | Q5 | Rotational-XOR: Pr[M^k(x <<< r) = M^k(x) <<< r XOR delta] for the best delta per word, under the odd multiply | largest k where any RX property survives |
| 6 | Q6 | SAT: a bit-level CNF of k applications with the real day constants on the round-0 input (t unknown): find t whose line index after k applications equals a target. Time to solve for k = 1, 2, 3, 4 | largest k the solver reaches in one hour |
| 7 | Q7 | Days: every test above on the genesis day 20729, the Devnet 3 day 20733, and random day indices; the weak-ROT days a sibling named if I reach them | per-day margins |
3. Method and tool per question, with the known-failed shape
One Rust crate, tools/attack/adv-mixer-3 (binary adv-mixer-3), igneum-pow by path, no other dependency. Every
command takes --day <index> (chain day index through bind::day_bytes and MixParams::with_shape) and
--plant none|one|nomul|weak: one runs one application in place of k; nomul removes the multiply layer (MUL all
1, RC kept); weak is MUL all 1, RC all 0, ROT all 16. A tool is trusted only once it fires on its plant.
Q1 index census (adv-mixer-3 index --day D --apps k --threads T)
Exhaustive over t in 0..2^32: init as the code does, k applications with keys round_key(0..k-1), tally
s[0] AND 0x3fffff into 2^22 counters. Report: chi-square against uniform (expected 1024 per bin), its sigma,
empty bins, min and max load, the per-bit balance of all 32 bits of s[0] and the 512 state bits (counted on a
2^-8 sample). Uniform reads chi-square within a few sigma of 2^22 and no empty bin.
Known-failed shape: --apps 0 (the init state: s[0] = K[0], one bin holds everything) and --plant nomul --apps 1
(the multiply layer gone: one application of the double round on an input that varies in 8 words only). Both must
read as non-uniform by orders of magnitude.
Q2 avalanche at high N (adv-mixer-3 sac --day D --apps k --states N --threads T)
Random 512-bit states, each of 512 input bits flipped, k applications, the 512 x 512 flip-probability matrix. Report holes (p = 0 or 1), the worst |p - 0.5| in sigma at N, the count of cells beyond 6 sigma, and the mean flip. N = 2^24 puts 6 sigma at 0.0015. The decay of the worst bias from k = 1 to the first k where no cell clears 6 sigma is the margin.
Known-failed shape: --plant weak at every k must show holes; --plant one at k = 8 must read as k = 1.
Q3 differential multiplicity (adv-mixer-3 diff --day D --apps k --samples N)
For each of the 512 single-bit input differences and 64 random two-bit differences: N random pairs, the full 512-bit output difference hashed to 64 bits, sorted, the largest multiplicity; also the count of output words with zero difference. Multiplicity m at N gives a differential of probability about m / N. With N = 2^16 the band is 2^-15.
Known-failed shape: --plant nomul at k = 1 (the double round alone has deterministic bits) and --plant one.
Q4 linear correlations (adv-mixer-3 lin --day D --apps k --samples N --threads T)
N random states, y = M^k(x); the joint counts of (x_i = 1, y_j = 1) for all 512 x 512 pairs; the correlation c(i, j) = 2 Pr[x_i = y_j] - 1. Report the worst |c| in sigma (sigma = 1 / sqrt(N)) and the count beyond 6 sigma. At N = 2^24 the band is 0.0015.
Known-failed shape: --plant weak at k = 1 must show correlations near 1.
Q5 rotational-XOR (adv-mixer-3 rx --day D --apps k --samples N)
For rotations r in {1, 8, 16}: N random x; d = M^k(x <<< r) XOR (M^k(x) <<< r) per word; the most frequent d per word and its frequency; the count of d = 0. Anything above the 2^-32 floor at N = 2^20 is a property.
Known-failed shape: --plant weak with RC all 0 and rk set to 0 must show d = 0 often (a pure ARX round is
rotation-invariant when every constant is zero).
Q6 SAT (adv-mixer-3 cnf --day D --apps k --target A --out file.cnf, then a solver)
A Tseitin CNF of k applications on the round-0 input: t is 32 free variables, the 16 init words are affine in t (the multiply by MUL[i] is a shift-add chain of 32-bit adders), each application is 16 XORs with constants, 16 constant multiplies, 8 quarter rounds (adders, XORs, wire rotations). The constraint is the 22 low bits of s[0] after k applications equal A. Solve with a CDCL solver installed in my box user directory (CaDiCaL from source under ~/adv-mixer-3-tools on the box, never system-wide; if the clone is refused, a pure-Rust solver crate, and I state which). Wall time per k with a one-hour cap. The honest cost of finding such a t is 2^10 trials of k applications. The result is the largest k the solver finishes inside the cap, and the time ratio against the honest 2^10 x k x 130 ops.
Known-failed shape: k = 1 must solve in seconds and the returned t must verify through the real code.
Q7 days
Q1, Q2, Q4 on days 20729 and 20733 and on 8 random day indices. Q3, Q5, Q6 on 20729 and 20733.
4. Box-hours per step
Wall hours on one box at 32 threads, nice 10, before yield pauses.
| Step | Where | Estimate |
|---|---|---|
| Build the harness (release) | box 2 | 0.05 |
| Q1 index census, k = 0..8, 3 days | box 2 | 0.3 |
| Q2 sac, k = 1..8 at 2^24 states, 2 days | box 2 | 0.5 |
| Q3 diff, k = 1..6, 2 days | box 2 | 0.1 |
| Q4 lin, k = 1..6 at 2^24, 2 days | box 1 | 0.5 |
| Q5 rx, k = 1..4, 2 days | box 1 | 0.05 |
| Q6 SAT, k = 1..4, one-hour cap each, 2 days | box 1 | up to 4 (capped) |
| Q7 the 8 random days on Q1, Q2, Q4 | box 2 | 1.5 |
| Total planned | about 7, inside the 8-hour reading line |
The report carries the hours actually spent. Every sweep is a queue file on build-2 under /srv/builds/_adv/mixer/queue/NN-adv-mixer-3-.sh, claimed by mkdir before it runs.
5. Files opened (the complete read set)
| File | How |
|---|---|
| igneum-pow/src/memhard.rs | worktree HEAD (identical to 017e7037), read in full |
| igneum-pow/src/seed.rs | read in full |
| igneum-pow/src/bind.rs | grep for the day rule, day_bytes and day_index |
| igneum-pow/src/generator.rs | grep and the lines 205 to 240, 410 to 470, 795 to 832: LoadClass, MX4, MX8, V3_CLASS, V4_CLASS |
| igneum-pow/tests/mixer.rs | the head (lines 1 to 150) |
| igneum-pow/Cargo.toml | read |
| igneum-pow/ file list | ls |
docs/spec/01-lottery-hash.md at 017e7037 |
sections 1.3, 1.8.1 to 1.8.5, via git show |
| docs/analysis/chip-model-v3.md at HEAD | headings; sections 1, 2, 5.2, 6 |
| proto-cuda/packs-ca3-v4/ | the directory listing of the eight packs |
| Devnet 3 pack on build-1 /srv/artefacts/packs/v4-devnet3-epoch0 | sha256 of the zip verified e025750f...65b334; program.json (program id 0xfce15bf61030be57, attempt 0, seed_words); vectors.json keys, day bytes 69676e65756d2d6461792ffd50000000000000 = "igneum-day/" le64(20733), cache_fnv1a64 0x7334fa46e5d972eb |
| build/attack-pass tools/attack/f4-weakday | file list, Cargo.toml, src/main.rs head (the day rule and the draw) |
| tools/attack/f8-uniform (attack-regate worktree) | file list and Cargo.toml |
| tools/build-remote.sh | header and the slot and box rules (grep) |
| infra/build-server/lib.sh | the worktree naming line |
| infra/build-server/remote-run.sh | the slot and lock rules (grep) |
| infra/build-server/capacity/run.sh, lib.sh | the yield pattern in full (run_slice, cap_build_active) |
| build/adv-mixer docs/plans/cryptanalysis/plan-mixer.md and docs/analysis/cryptanalysis/report-mixer.md | read in full |
| build/adv-cache docs/plans/cryptanalysis/plan-chained-cache.md | the head (sections 0 and 1) |
| build/adv-accept docs/plans/cryptanalysis/plan-acceptance-rule.md | the head (sections 0 and 1) |
Not opened: anything else under docs/, site/, proto-metal/, git log, commit messages, any other branch or worktree. The memhard.rs comments point at docs/plans/mixer-x4.md, hot-table.md, era-layout.md and counter-asic-3-derivation.md; not followed. The spec points at MEMHARD.md and several analyses; not followed. build/adv-mixer-2 was not on the build mirror at 19:25 BST.
6. How this lane differs from adv-mixer
The sibling adv-mixer runs the avalanche census at 2e6 states (8 sigma at 0.57 percent), a fold probe and the weak-day census. This lane goes under that band (2^24 states), adds the exhaustive round-0 index census, the differential, linear and rotational-XOR measurements and the SAT model, and reports the margin per method.