From 111b6b9bf7841c3f334ee5404b0411147586d961 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:29:55 +0000 Subject: [PATCH] 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 --- docs/plans/cryptanalysis/plan-mixer-3.md | 194 +++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 docs/plans/cryptanalysis/plan-mixer-3.md diff --git a/docs/plans/cryptanalysis/plan-mixer-3.md b/docs/plans/cryptanalysis/plan-mixer-3.md new file mode 100644 index 00000000..0bff37e8 --- /dev/null +++ b/docs/plans/cryptanalysis/plan-mixer-3.md @@ -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- 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 ` (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.