igneum/docs/plans/cryptanalysis/plan-mixer-3.md
igneum-labs b97539c0d1 adv-mixer-3: times in UTC (identity grep)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 18:52:43 +00:00

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, 18:20 to 19:20 UTC. 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 18:25 UTC, 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 18:25 UTC.

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.