adv-cache-3: attack plan for the chain break or skip (internal adversarial pass, not an independent review)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-07 18:49:24 +00:00
parent 5e41217735
commit fa61c67060

View file

@ -0,0 +1,88 @@
# Attack plan: the chained cache, chain break or skip (lane adv-cache-3)
Internal adversarial pass, not an independent review. Every sentence in this file and in the report that could be quoted publicly carries that label.
Lane `adv-cache-3`, branch `adv-cache-3` cut from `build/master` 04c4d9bc at 19:42 BST on 7 October 2026. The attacker here is an outsider with the public kit who has never worked on the hash code. The question class is the chain break or skip: can line `(s, j)` of the cache be had for fewer than `j + 1` block evaluations without holding an earlier line of its segment, and what structure do the XOR chaining and the feed-forward leave between lines.
## 0. The outsider rule, applied
| Check | Result |
|---|---|
| Frozen commit | `017e70376489251e18564c0abce7e466e606c8b3` (class v4 sub-version 3, object byte 7) |
| HEAD of this worktree | `04c4d9bc3942b3d312559212aa990fdf94e0c755` (`build/master` at 19:42 BST) |
| `git diff --quiet 017e7037 HEAD -- igneum-pow` | IDENTICAL (printed at 19:42 BST). The harness depends on the worktree's `igneum-pow/` by path `../../../igneum-pow`; the check is repeated before every build |
| Public kit zip | sha256 `4f2445c50c58d76a5544023492d8b858d0b07c5e372d31f9c90c4ce51f829154` as the brief states; the eight packs are read from `proto-cuda/packs-ca3-v4/` in the worktree |
| Devnet 3 epoch-0 pack | `/srv/artefacts/packs/v4-devnet3-epoch0.zip` on build box 1, sha256 `e025750f71175ed14d6e2a24e387ebbf1979b1cd0faee9139c41a7671165b334` (verified on the box, 19:43 BST); `program.json` id `0xfce15bf61030be57` at attempt 0, day bytes le64(20733); read in place, nothing copied into the repository |
| Worktree | `/Users/joshm/Projects/igneum-wt-adv-cache-3`, harness crate `tools/attack/adv-cache-3/` |
| Logs and results | `/srv/builds/_adv-cache-3/` on each box (outside the worktree mirror); copies under `docs/analysis/cryptanalysis/logs/adv-cache-3/` on this branch |
Files opened, the complete list:
| File | How much |
|---|---|
| `igneum-pow/src/memhard.rs` | in full (`chacha_block`, `qr`, `fill_segment_tagged`, `mixer`, `derive_items_mask`, `Shape`, the tests) |
| `igneum-pow/src/seed.rs`, `igneum-pow/src/lib.rs` | in full |
| `igneum-pow/src/bind.rs` | the public function list, `day_bytes`, `day_index`, the `day_bytes_layout` test |
| `igneum-pow/Cargo.toml`, `igneum-pow/Cargo.lock`, `igneum-pow/README.md` (first 60 lines), the `src/` and `tests/` file list | in full |
| `docs/spec/01-lottery-hash.md` at 017e7037 | the heading list; sections 1.8 (1.8.1 to 1.8.5) and 1.13.3 |
| `docs/analysis/chip-model-v3.md` at HEAD | the heading list; sections 1, 2, 5 (5.1 to 5.9), 6 |
| `proto-cuda/packs-ca3-v4/` | the directory and file listing (eight packs); `vectors.json` fields read when the fingerprint check runs |
| Devnet 3 pack `program.json` on build box 1 | the first 40 lines (id, attempt, seed, class, mixer text) |
| `tools/attack/f4-weakday/{Cargo.toml,src/main.rs}` from `build/attack-pass` | Cargo.toml in full; main.rs the header and first 80 lines (the crate layout, the day rule) |
| `tools/attack/f8-uniform/{Cargo.toml,src/main.rs}` from the attack-regate worktree | Cargo.toml in full; main.rs lines 1 to 260 (the crate layout, the log macro, the traced derivation, the histogram statistics) |
| `tools/build-remote.sh`, `infra/build-server/lib.sh`, `infra/build-server/remote-run.sh`, `tools/ci/export-exclude.txt` | in full (the box routing, the bounded class, the source overlay with `--delete` and its target-dir excludes, the slot files) |
| `/srv/builds/_adv/cache/queue/90-adv-cache-3-chain-skip-search.sh`, `91-adv-cache-3-feedforward-relations.sh`, `92-adv-cache-3-pebbling-curve.sh` on build box 2 | in full (the definitions); the queue and claims directory listings |
| Sibling lanes on the build mirror | `build/adv-cache:docs/plans/cryptanalysis/plan-chained-cache.md` and `docs/analysis/cryptanalysis/report-chained-cache.md` in full; `build/adv-cache-2:docs/plans/cryptanalysis/plan-chained-cache-2.md` in full; the file lists of the mixer and accept lanes' `cryptanalysis/` directories (not read) |
Not opened: anything else under `docs/`, `site/`, `proto-metal/`, `git log`, commit messages, other branches or worktrees. The `memhard.rs` doc comments point at `docs/plans/mixer-x4.md`, `hot-table.md`, `era-layout.md` and `MEMHARD.md`: not followed. The spec points at `MEMHARD.md` and several analyses: not followed. The repository's `CLAUDE.md` pointers were not followed. Sibling log directories on the boxes were listed by name while locating the queue; no sibling log was opened.
## 1. The target, restated from the spec and the code
Spec 1.8.3 and `memhard.rs` `fill_segment_tagged`: the cache is 2^26 words = 2^22 lines of 16 words in 2^16 segments of 64 lines. Segment `s`, line `j`:
```
c_j = (sigma[0..3] || K[0..7] || s || j || tag[0..1]) 16 words, all public once the day is known
x_0 = c_0 prev_0 = 0
x_j = line_{j-1} XOR c_j j >= 1
line_j = B(x_j) = C(x_j) + x_j C = the 12-round ChaCha permutation (6 double rounds), + word-wise mod 2^32
```
The block layout of `x_j` by word index: 0 to 3 sigma (constants), 4 to 11 `K[0..7]` (the day key, public), 12 `s` (16 significant bits), 13 `j` (6 significant bits), 14 and 15 the tag (constants); every word is XORed with `line_{j-1}`. For `j = 0` all 16 words are known. For `j >= 1` no word of `x_j` is known without `line_{j-1}`, since the XOR spreads the previous line over every word. The day key is `K = seed_words_from_bytes("igneum-day/" || d_le64)` (`bind::day_bytes`). Line `j` costs `j + 1` block evaluations from nothing; one block is 6 x 8 x 12 + 16 + 16 = 608 integer operations as written. The 2^16 segments share nothing but `K`.
Spec 1.8.5 under class v4 (`mixer_mult = 8`): an item is 72 mixer applications and 8 dependent line reads at `a = s[0] AND 0x3fffff`, about 9,360 operations (`chip-model-v3.md` 5.2). A recompute chip that does not hold line `(s, j)` pays `j + 1` blocks of 608 operations, 32.5 blocks on average: 19,760 operations, 2.1x the item. The chip model's SRAM column (128 mm^2, $46 for 256 MiB at N5) and the partial-store curve of 5.4 rest on that price. A chain break or skip is anything that lowers it.
## 2. The questions, in attack order
Harness crate `attack-adv-cache-3`, binary `adv-cache-3`, one subcommand per row, `igneum-pow` by path. The real `B` is the library's `chacha_block`; the chain is restated in the harness and checked against `Cache::fill_segment` word for word on every run. The vectors are the kit's day-20730 cache fingerprint and the Devnet 3 day-20733 fingerprint, read from the packs' `vectors.json`. A reduced block `B(w, r)` (16 words of `w` bits, `r` double rounds, the ChaCha rotations reduced mod `w` and floored at 1, the same chaining and the same word layout truncated to `w` bits) is the small-scale model. Every claim on the model is cross-checked on the real `B` on a few segments.
| # | Question (brief letter; queue file) | Method | Command | Known-failed shape (must fire before the real run counts) | Gate | Box-hours |
|---|---|---|---|---|---|---|
| Q1 | (a; 90) Line `(s, j)` in fewer than `j + 1` blocks without an earlier line | (1) Template skip search: for every `j` in 0..63 and 1,024 segments on 2 day keys, every earlier-line-free formula of a template set is evaluated and compared with the real line: `B(c_j)`, `B(c_j XOR c_i)` for `i < j`, `B(c_j) XOR B(c_i)`, `B(c_j) XOR c_i`, `B(B(c_j))`, `B(c_j) + c_i`, and the inverse direction `Cinv(line_j) XOR c_j = line_{j-1}` (a shortcut up the chain). A match is a derivation in 1 or 2 blocks. (2) The GF(2) affine-relation search: over 4,096 samples of `(x_j, line_j)` the 1,025-column bit matrix (512 input bits, 512 output bits, the constant) must have rank 1,025; a deficiency is an affine relation and a skip. The same on `(line_{j-1}, line_j)` across the real chain. (3) Dependence: for each of the 512 bits of `x_j` the flip probability of each bit of `line_j` (a 512 x 512 table over 2^12 lines) and the 16 x 16 word table; a zero cell means a line independent of part of its input. (4) Exhaustive at small scale: `B(4, 2)` with all 2^16 segments and a 16-line chain for (1) to (3); `B(2, r)` with a 32-bit state and every one of the 2^32 inputs enumerated: the image size of one block (how far `B` is from a permutation) and of the `k`-fold chain for `k` up to 64 (entropy lost down the chain), against the random-function expectation `2N / k` | `adv-cache-3 skip --day 20730 --segments 1024`, `--day 20733`; `adv-cache-3 skip --w 4 --rounds 2 --segments 65536 --lines 16`; `adv-cache-3 image --w 2 --rounds 6` | `--plant no-xor` (prev not XORed in): the template `B(c_j)` must match every line at 1 block and the dependence table must read 0. `--plant no-feedforward` (`B = C`): the inverse template must recover `line_{j-1}` from `line_j` on every line, and at w = 2 the image must be all 2^32 states (a permutation). `--rounds 1` on the rank test: a one-double-round `C` has affine output bits (bit 0 of an add is an XOR) and the rank must fall under 1,025 | 0 lines under `j + 1` blocks on the real chain at 64 lines and 1,024 segments, and on the model at 16 lines and 65,536 segments; rank 1,025 at every `j` tested; every flip cell within 6 sigma of 0.5; every plant fires | 0.3 (the w = 2 image census is 64 x 2^32 block evaluations on a 32-bit state, about 10 minutes on 88 cores; the rest seconds) |
| Q2 | (b, c; 91) Relations through `line_j = C(x_j) + x_j` and partial knowledge | Over 2^20 lines per day key (2^14 segments x 64 lines) on 4 day keys (20730 to 20733), `j >= 1`: (1) per-bit bias of `line_j XOR x_j`, `line_j - x_j` (that is `C(x_j)`), `line_j XOR line_{j-1}` and `line_j - line_{j-1}`, 512 bits each, in sigma against 1/2; (2) the 512 x 512 linear-correlation table between bits of `x_j` and bits of `line_j` (the bias of `x[a] XOR line[b]`), the largest cell in sigma; the same between `line_{j-1}` and `line_j`; (3) partial knowledge: the word-level dependence table of Q1 (3) read as "given `k` words of `line_{j-1}`, how many words of `line_j` are determined" (a word is computable from a subset of input words only if it is insensitive to the rest), and the reverse, what of `x_j` is determined by `line_j`: the correlation table read column-wise, plus a fixed-point inversion attempt `x <- Cinv(y - x)` from `x = 0`, 64 steps, on 2^16 lines, counting convergences | `adv-cache-3 relations --day0 20730 --days 4 --lines-log2 20 --threads 88`; `adv-cache-3 partial --day 20730 --lines-log2 16` | `--rounds 1` (one double round; the queue file's "2 rounds"): the correlation table must show cells far beyond 6 sigma where the 12-round table shows none; `--plant no-feedforward`: the inversion must converge on every line | every bias and every correlation cell within 6 sigma of uniform at 2^22 samples; no word of `line_j` computable from fewer than 16 words of `x_j`; 0 inversions converge; the plants fire | 0.5 (2^22 lines x 512 x 512 bit correlations by popcount, about 2 x 10^12 bit operations, minutes on 88 cores) |
| Q3 | (d; 92) The 64-line chain and the 2^16 independent segments under a parallel amortising adversary: the pebbling curve | Exact arithmetic and simulation on the chain as a graph. (1) The static pebbling optimum: for `k` held lines of 64 (`f = k / 64`), the expected blocks per uniform read for the best placement, by dynamic programming on the path (exact), against the honest hold-every-`(64 / k)`-th-line placement, for `f = 1/64, 2/64, 4/64, 8/64, 16/64, 32/64, 1`; exhaustive over every subset of held lines at 10 to 16 lines to check the DP. (2) The amortising adversary: `m` requests per segment in flight (the 2^16 segments are independent, so amortisation is only within a segment), one walk per segment from the nearest held line to the deepest request; blocks per read by exact combinatorics over uniform requests and by Monte Carlo, for `m = 1, 2, 4, ..., 64` at each `f`; the cross-check is sibling adv-cache's real-address batch rows (23.84 at m = 1, 7.06 at m = 8, 0.99 at m = 64 with nothing held). (3) Ops per item at each point: `72 x 128 + 144 + 8 x blocks x 608`, against 9,360 at `f = 1`, with the SRAM held (`f` x 128 mm^2) beside it | `adv-cache-3 pebble --lines 64 --exhaustive-upto 16 --mc 1000000` | `--plant skip-edge 8` (every line `j` also derivable from line `j - 8` in one block): the optimal cost at `f = 1/64` must fall under the path's | the curve is monotone in `f` and never below the honest hold-every-`k`-th curve at the same `f`; `f = 1` reads 9,360 ops per item; the plant fires | 0.1 |
| Q4 | (e) Anything else | (1) Cross-segment relation: the 512 x 512 correlation table between `line_j(s)` and `line_j(s XOR 2^b)` at `j` in {0, 1, 63}, the one-word-difference multi-target at `j = 0` included. (2) Cross-day relation: the same table between `line_j` of day `d` and of day `d + 1` for the same `(s, j)`. (3) The known-constant words: the count of input words of `x_j` with a known value for `j = 0` (16) and `j >= 1` (0), stated from the code | `adv-cache-3 cross --day 20730 --segments 4096` | `--plant no-xor`: at `j >= 1` lines of adjacent segments differ in word 12 only before the block, so a reduced-round `C` must show cross-segment correlation | every cell within 6 sigma; the plant fires | 0.1 |
Total estimate: about 1.0 box-hours of the 8 budgeted (the ask line is 16). Memory per run: under 1 GiB (the w = 2 image census holds a 2^32-bit bitmap, 512 MiB; the correlation tables are 512 x 512 u64, 2 MiB each).
## 3. Running rules followed
- No cargo on the Mac. Builds through `tools/build-remote.sh --box 1|2 -- build --release` from `tools/attack/adv-cache-3/`; the binary is copied to `/srv/builds/_adv-cache-3/bin/` on the box that runs it and its sha256 recorded in the report.
- Runs over 10 minutes start on the box from `tools/attack/adv-cache-3/run-box.sh`: `nohup nice -n 10 taskset -c 8-95 <bin> <args> > <log> 2>&1 &` with a pid file beside the log under `/srv/builds/_adv-cache-3/`. No SIGSTOP yield. Kill by pid file only. Cores 0 to 7 never used. No GPU row exists in this lane.
- The queue: files 90, 91, 92 on build box 2 are claimed with `mkdir /srv/builds/_adv/cache/claims/<file>` before they run; every further sweep is written as `/srv/builds/_adv/cache/queue/NN-adv-cache-3-<name>.sh` (self-contained) and claimed the same way. When this lane's queue is empty, the next unclaimed sibling file in name order is claimed and run, the owner named in the report.
- Box-hours: 8 is a reading, 16 the ask line.
- Pushes only to `build adv-cache-3`; `origin` never touched; every other branch read-only. Commits as igneum-labs.
- No secrets, keys or host lines in any document. Every time quoted is current UK time (BST), checked with `TZ=Europe/London date`.
## 4. Order of work and clock (BST, 7 to 8 October 2026)
| Step | What | When |
|---|---|---|
| 1 | This plan committed and pushed | by 20:10 |
| 2 | Harness crate written, built on a box; the fingerprint checks; every plant fired and logged | 20:10 to 21:30 |
| 3 | Q1 skip, rank, dependence; Q3 pebble (seconds each); the w = 2 image census and Q2 relations queued back to back | 21:30 to 23:00 |
| 4 | Q4 cross; the report with first rows pushed | by 00:00 |
| 5 | Sibling queue files, if any are unclaimed; the report updated as rows land | from 00:00 |
## 5. What a result is
A BREAK is a method with a counted gain (a line at fewer than `j + 1` blocks, or a pebbling point under the honest curve at the same `f`), reproducible from the command and the seed in the report. A BOUND is what was searched, with which tool, how far (segments, lines, day keys, samples) and the margin (sigma, or the rank margin). "Nothing found" counts only with its effort stated.