adv-mixer-3: attack plan for the statistical distinguisher and the round margin of M_r

Internal adversarial pass, not an independent review. Target c3d32437 (class v4 sub-version 3). Seven questions:
exhaustive round-0 line-index census, high-N avalanche, differential, linear, rotational-XOR, SAT on the round-0
input, per-day runs. Known-failed shape per method, box-hours, the complete read set.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-07 18:29:55 +00:00
parent d21ca54a24
commit 111b6b9bf7

View file

@ -0,0 +1,194 @@
# 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 | 017e70376489251e18564c0abce7e466e606c8b3 (class v4 sub-version 3, object byte 7) |
| `git diff --stat 017e7037 HEAD -- igneum-pow` at 7a7caa34 | prints nothing. 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 | build-1 and build-2, nice 10, yield to every build-<k> 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-<name>.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.