Base build/master 5e412177; igneum-pow byte-identical to c3d32437 (git diff --quiet prints IDENTICAL).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
98 lines
20 KiB
Markdown
98 lines
20 KiB
Markdown
# Attack plan: the chained cache, partial-state and hot-set attacks (lane adv-cache-2)
|
|
|
|
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-2`, branch `adv-cache-2` from `build/master` (cut at 7a7caa34 at 19:23 BST, after the mirror had moved past the stale 3f0afcd5; re-merged with `build/master` 04c4d9bc at 19:4x BST before any build). Written 7 October 2026, 19:23 to 20:1x BST. The attacker here is an outsider with the public kit who has never worked on the hash code. Nothing below is a measurement of a chip.
|
|
|
|
## 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 HEAD 7a7caa34, and `git diff --quiet 017e7037 HEAD -- igneum-pow` at HEAD 04c4d9bc (after the re-merge) | Printed nothing; printed IDENTICAL. The crate at `build/master` is byte-identical to the frozen commit over every file of `igneum-pow/`. The harness depends on the worktree's `igneum-pow/` by path. Base commit of every build in this lane: 04c4d9bc or a later `build/master` that keeps this identity (re-checked before each build). |
|
|
| Public kit zip `packs-ca3-v4-sub3-20261007.zip` on build box 1 | sha256 `4f2445c50c58d76a5544023492d8b858d0b07c5e372d31f9c90c4ce51f829154`, as the brief states (verified on the box, 19:29 BST) |
|
|
| Devnet 3 epoch-0 pack `v4-devnet3-epoch0.zip` | copied read-only to the scratchpad; sha256 `e025750f71175ed14d6e2a24e387ebbf1979b1cd0faee9139c41a7671165b334`, as the brief states; `program.json`: id `0xfce15bf61030be57`, attempt 0, epoch seed and era seed both `4020cb43...68b925`, day bytes `igneum-day/` + le64(20733); `vectors.json`: cache FNV-1a 64 `0x7334fa46e5d972eb` |
|
|
| Shared devnet epoch-0 pack `v4-devnet-epoch0` (in the kit) | id `0xa785001687d8688a`, attempt 1, epoch seed and era seed both `edc4fa84...fb07` (the genesis hash), day bytes le64(20730); cache FNV `0x448274a57f508cbc` |
|
|
| Worktree | `/Users/joshm/Projects/igneum-wt-adv-cache-2`, harness crate `tools/attack/adv-cache-2/` (`igneum-pow` by path `../../../igneum-pow`) |
|
|
|
|
Files opened, the complete list:
|
|
|
|
| File | How much |
|
|
|---|---|
|
|
| `igneum-pow/src/memhard.rs` | in full (the cache fill, the mixer, `derive_items_mask`, the shape) |
|
|
| `igneum-pow/src/seed.rs`, `src/lib.rs` | in full |
|
|
| `igneum-pow/src/bind.rs` | header comment, `day_bytes`, `day_index` |
|
|
| `igneum-pow/src/verify.rs` | `load_index`, `window`, `step` (the load path), `Epoch::chain_dataset_day`, `chain_program`, the public function list |
|
|
| `igneum-pow/src/generator.rs` | `LoadClass`, `EraParams`, `era_draw`, the class constants (`V3_CLASS`, `V4_CLASS`, shadow 256 x 27), the public function list |
|
|
| `igneum-pow/src/main.rs` | the CLI argument list only |
|
|
| `igneum-pow/Cargo.toml`; the `src/` and `tests/` file lists | in full |
|
|
| `docs/spec/01-lottery-hash.md` at 017e7037 | sections 1.5, 1.6, 1.8 (1.8.1 to 1.8.5), 1.9, 1.13.1 to 1.13.3 |
|
|
| `docs/analysis/chip-model-v3.md` at HEAD | sections 1, 2, 5 (5.1 to 5.9), 6 |
|
|
| `proto-cuda/packs-ca3-v4/*/program.json` | the header fields (id, attempt, seeds, class, era draw, op semantics) of `v4-devnet-epoch0`; id, attempt and class of the other seven |
|
|
| `proto-cuda/packs-ca3-v4/v4-devnet-epoch0/{vectors.json,seeds.txt}` | field names and values |
|
|
| Devnet 3 pack `program.json`, `vectors.json` (scratchpad copy) | the same fields |
|
|
| `tools/attack/f8-uniform/{Cargo.toml,src/main.rs}` from the attack-regate worktree | in full (the `lines` and `warps` censuses, the mirror, the plant hooks, the stats) |
|
|
| `tools/attack/f4-weakday/{Cargo.toml,src/main.rs}` from `build/attack-pass` | the header and the first 250 lines (the day rule and the draw classification) |
|
|
| `tools/build-remote.sh`, `infra/build-server/lib.sh`, `infra/build-server/remote-run.sh` | the usage header, the `--box` routing, the box 2 bounded class |
|
|
| `infra/build-server/capacity/lib.sh`, `capacity/run.sh` | `cap_build_active` (the flock probe) and `run_slice` (the SIGSTOP/SIGCONT loop): the yield pattern |
|
|
| Sibling plans on the build mirror: `build/adv-cache:docs/plans/cryptanalysis/plan-chained-cache.md`, `build/adv-mixer:docs/plans/cryptanalysis/plan-mixer.md` (head), `build/adv-accept:docs/plans/cryptanalysis/plan-acceptance-rule.md` (head) | read as other outsiders' plans; `adv-cache-3` has no branch on the mirror yet |
|
|
|
|
Not opened: anything else under `docs/` (no `docs/plans/` beyond the siblings' `cryptanalysis/plan-*.md`, no `docs/analysis/` beyond `chip-model-v3.md`, no fud ledger, no `attack-pass/`), no `site/`, no `proto-metal/`, no `git log`, no commit messages, no other branches or worktrees. The repository's `CLAUDE.md` was displayed to me by the tooling; none of its pointers into other documents were followed. The `memhard.rs` doc comments point at `docs/plans/mixer-x4.md`, `hot-table.md`, `era-layout.md`, `counter-asic-3-derivation.md`: not followed. A sibling lane's log directory on box 2 was listed by name while locating the queue directory; no log was opened.
|
|
|
|
## 1. The target, restated from the spec and the code
|
|
|
|
The cache (spec 1.8.3, `memhard.rs` `fill_segment_tagged`): 2^26 words = 2^22 lines of 16 words = 2^16 segments of 64 chained lines. Segment `s`, line `j`: `in = prev XOR (sigma || K || s || j || tag)`, `line = ChaCha12(in) + in`, `prev = line`, `prev_0 = 0`. Line `j` costs `j + 1` block evaluations from nothing; one block is 6 double rounds x 8 quarter rounds x 12 ops + 32 = 608 integer ops (counted from `chacha_block`). The day key `K = seed_words_from_bytes("igneum-day/" || d_le64)` (`bind::day_bytes`); nobody chooses it.
|
|
|
|
The item (spec 1.8.5 class v3 text with `m = 8`, `memhard.rs` `derive_items_mask`, `Shape::for_class(V4_CLASS)` = `mixer_mult 8, cache_log2_words 26, derive_len 0`):
|
|
|
|
```
|
|
s[0..7] = K; s[8+i] = t * MUL[i] + RC[i] i in 0..7
|
|
for r in 0..7:
|
|
8 mixer applications, keys round_key(8 r + j)
|
|
a = s[0] AND 0x3fffff bits 0..5 = line j (the recompute depth), bits 6..21 = segment s
|
|
s[i] ^= cache[line a][i]
|
|
8 mixer applications, keys round_key(64 + j)
|
|
```
|
|
|
|
72 mixer applications of about 128 ops (hoisted) plus the folds: 9,360 ops and 8 dependent 64-byte reads per item (`chip-model-v3.md` 5.2 counts the same). The item index `t` is not the attacker's: the program's load address picks it (`verify.rs` `load_index`: `y = rotl(x * M, R)` then the site's window `k = min(win, D - 26)`, `idx = ((y AND (MASK >> k)) OR (off << (D - k))) AND MASK`; `t = idx` with the four interleave bits removed, `Layout::split`). A hash is 128 loads (16 load sites x 8 iterations), one item each, 4 bytes read.
|
|
|
|
What the chained cache is supposed to buy, in the attacker's words: a chip that holds less than the whole 256 MiB cache pays `j + 1` block evaluations (608 ops each) for a line it does not hold, against 9,360 for the whole item; a chip that holds less than the whole 1 GiB dataset recomputes the items it does not hold at 9,360 ops each (`chip-model-v3.md` section 5: the partial-store curve, priced at a uniform hit rate `f`). Both prices assume the reads are uniform. My question class is whether they are: whether a hot set of lines or of items exists, per round, per load site, per program, per day, and whether anyone can steer reads toward one.
|
|
|
|
The chip model rows this lane moves (`chip-model-v3.md` sections 2 and 5): the SRAM column (256 MiB = 128 mm^2, $46 at the N5 headline density; a partial cache store of fraction `f` is `f` of that) and the partial-store curve of 5.4 (the `f = 0.25`, `0.5`, `0.75` rows, whose "items recomputed" column is `128 (1 - f)` only if a stored fraction `f` of items serves a fraction `f` of reads).
|
|
|
|
## 2. The questions, in attack order, with method, tool, known-failed shape, gate and box-hours
|
|
|
|
The order is cheapest and most decisive first. Every census runs on the real derivation and the real interpreter through the library (nothing re-implemented except the traced mirrors, which are checked word for word against `derive_items` and `Epoch::hash_warp` on every run). Harness crate `attack-adv-cache-2`, binary `adv-cache-2`, one command per row.
|
|
|
|
| # | Question | Method | Command | Known-failed shape (must fire before the real run counts) | Gate | Box-hours (box 2, 32-core band, nice 10) |
|
|
|---|---|---|---|---|---|---|
|
|
| Q1 (brief a) | The line-index distribution over 2^22 lines: largest bucket in sigma against uniform, per read round and pooled; the 2^16 segment (64-line bucket) histogram; the 64-bucket depth (`j`) histogram per round | The f8 `lines` pattern rewritten in my crate: for each of `days` consecutive chain days, every item `t < 2^24` derived with its 8 line indices recorded (traced mirror, batched 64), 8 per-round 2^22-bin histograms, pooled over rounds and over days, the 2^16 segment histogram, the 64-bin depth histogram per round, chi-square per dof, largest and smallest bucket in sigma, top 0.1 / 0.5 / 1 percent share against a SplitMix64 control of the same size. 16 days x 2^24 items = 2^28 derivations = 2^31 line reads (the brief's floor); extended to 64 days = 2^30 derivations if the hours allow | `adv-cache-2 lines --day0 20730 --days 16 --items-log2 24 --threads 32` | `--plant quarter-lines` (the index masked to 2^20 lines: the 6-sigma and hot-set tests must flag) and `--plant half-lines` (2^21); both at `--items-log2 20 --days 1` before the real run; the log says FLAGGED | Largest line bucket and largest segment bucket within 6 sigma of uniform, per round and pooled; top 1 percent of lines carry under 1 percent plus the control's chance share; the depth histogram flat within 6 sigma (a skew toward low `j` would make a prefix store cheap) | 0.4 for 2^28 (fill 0.2 s per day; about 2^24 items in 10 to 20 s on 32 cores, estimate; the controls and the snapshots the rest); 1.5 for 2^30 |
|
|
| Q2 (brief b) | The hot set of items and of lines across the hashes of an epoch, for the two real programs (shared devnet `a785001687d8688a` on day 20730, Devnet 3 `fce15bf61030be57` on day 20733) and for drawn programs: the share of reads taken by the top 0.1 and 1 percent of items against 0.1 and 1 percent (uniform) and against the window-model control; per load site and overall; the same for lines (an item read is 8 line reads) | The f8 `warps` pattern in my crate: the day's 2^24 items derived once into a table with their 8 lines (1.5 GiB); the warp interpreter mirrored with every load's item index recorded (the shadow block run, since it writes registers between iterations); per program: the 2^24 item histogram, 16 per-site 2^24 item histograms, the 2^22 line histogram (weighted by item reads) and 16 per-site line histograms, the top-f shares against a flat control and a window-model control, the largest bucket in sigma, the hottest item traced to its site. Real programs at 2^24 nonces (2^31 reads, 128 per item on average); drawn programs: 16 epochs under the devnet era (genesis era bytes, epoch seeds `seed_words_from_bytes("igneum-adv-cache-2/epoch/k")`) and 16 with drawn era bytes, 2^22 nonces each | `adv-cache-2 warps --program devnet` / `--program devnet3` / `--programs era-fixed:1..16` / `--programs era-drawn:1..16` with `--nonces 16777216` or `4194304` | `--plant const-item` (the first load site fed one constant item: the hot-set test and the per-site table must flag site 0) at `--nonces 65536` before the real runs | Overall: top 0.1 percent of items under 1.2x the window-model control's share and X_f < f (the f8 definitions, restated in the report); per site: the same against the site's own window; lines: the top 1 percent of lines under 1 percent plus chance. A FINDING is any site or program whose top-f share exceeds the control by a counted factor; its price is Q4 | Table 0.1 per day (two days); real programs about 0.1 each (2^19 warps, a few ms per warp per thread, estimate); 32 drawn programs about 0.5; controls included: 1.0 |
|
|
| Q3 (brief c) | Steering: whether the item index `t`, the day key or the era and program parameters let anyone bias which lines an item reads or which items a hash reads | Three tests. (1) `t` to line: the 32 x 22 bit-sensitivity table of the round-0 line index (flip bit `b` of `t`, which address bits flip, over 2^16 items; every cell at 0.5 within 6 sigma), the same for rounds 1 and 7; the count of pairs `(t, t XOR 2^b)` with an equal round-0 line against the uniform expectation; distinct lines and distinct segments per item (8 expected, repeats counted) over 2^24 items. (2) The day key: a weak-day scan over 4,096 consecutive chain days x 2^16 items (2^28 derivations): per day the 2^16 segment histogram's chi-square per dof and largest bucket in sigma, the depth histogram's largest bucket; any day beyond 6 sigma is listed. (3) The parameters: the window layer (1.13.1) sends each site's reads to the dataset, a half or a quarter; per program the share of reads landing in the hottest quarter and hottest half of the item space is counted (the "best f = 1/4 store" of a chip) over the two real and 32 drawn programs, and over 1,024 drawn programs by the window draws alone (no interpretation, the expected density per quarter from the 16 sites' `win` and `off`) | `adv-cache-2 steer --day 20730 --items-log2 24`; `adv-cache-2 days --day0 20730 --days 4096 --items-log2 16`; the quarter shares come out of Q2's runs and `adv-cache-2 windows --programs 1..1024` | (1) `--plant t-low` (the round-0 address replaced by `t AND 0x3fffff`: the table must show the identity rows and the pair count must fire). (2) `--plant quarter-lines` on one day in the scan (that day must be listed). (3) `--plant all-quarter` (every site's `win` forced to 2 and `off` to 0: the top-quarter share must read 100 percent) | (1) every cell within 6 sigma of 0.5, pair count within 6 sigma; (2) no day beyond 6 sigma on 4,096 days, and the fraction of days near the threshold stated; (3) the distribution of the top-quarter share over programs, against the uniform 25 percent: this one is expected to be ABOVE 25 percent by design, and the number is the result, not a flag | 0.1 + 0.4 + 0.1 |
|
|
| Q4 (brief d) | The price: for a chip holding a fraction `f` of lines or of items at the measured hit rate, ops per item and the SRAM column against the chip model | Arithmetic from the measured shares, two stores. Items: a chip holding the hottest `f` of items (from Q2's histogram: the measured top-f share `S_f`, and the top-quarter share from Q3 (3)) recomputes `1 - S_f` of its reads at 9,360 ops: ops per hash `128 (1 - S_f) 9,360 + 512`, against the model's `128 (1 - f) 9,360 + 512`, in the `f = 0.25, 0.5, 0.75` rows of 5.4; SRAM = `f` x 1 GiB (the 2^28-word dataset) at the model's 128 mm^2 per 256 MiB. Lines: a chip holding `f` of the cache, three layouts: the stride store (every `k`-th line, `f = 1/k`), the prefix store (lines `0 .. L - 1` of every segment, `f = L / 64`) and the hottest-lines store (the top `f` of lines by Q1's census); per read the miss probability and the expected recompute depth, in block evaluations and ops; per item 8 reads; against 9,360; SRAM = `f` x 128 mm^2. Hand points: stride `f = 1/2` is 0.5 evaluations per read, `f = 1/64` (line 0 of every segment held) 31.5; prefix `L = 32` is `0.5 x 16.5 = 8.25` | `adv-cache-2 price --from <Q1 and Q2 result files>` | The curve code is checked against the two hand points, and against the model's `f = 0.25` row (899,072 ops per hash) at `S_f = f` | A BREAK is a store whose measured hit rate beats `f` by enough that ops per hash fall under the model's row for that `f` by more than 10 percent; otherwise the bound is the measured excess over `f` and the margin | 0.1 |
|
|
| Q5 | Anything else noticed while reading, each tested or bounded | (a) The address is the low 22 bits of `s[0]` straight after 8 mixer applications; Q1 tests bits 0..5 and 6..21 separately. (b) The first load of a hash depends only on the nonce, the init words and the instructions before the first load site, so an attacker can predict it without the dataset; but every hash must complete all 128 loads to be a lottery ticket, so abandoning on a miss gains nothing: a chip at store fraction `f` pays the recompute on `1 - f` of reads whatever the nonce. Bounded by that argument in the report, plus one count: the fraction of a hash's reads that are predictable before the first dependent read (1 of 128). (c) A day's cache serves every program of the day: the hot set of one program is not the hot set of the day's cache lines unless items are hot, which Q2 measures. (d) Era windows (Q3 (3)) are the one designed non-uniformity; its size is the result | in the report | none | none | 0 |
|
|
|
|
Total estimate: 2.6 box-hours at the brief's floors, up to 4 with the extensions. Memory per run: Q1 the 256 MiB cache plus 16 x 16 MiB histograms plus a 16 MiB control, under 600 MiB; Q2 the cache plus the 1.5 GiB table plus 17 x 64 MiB item histograms plus 17 x 16 MiB line histograms plus two 64 MiB controls, about 3.3 GiB; Q3 (2) the cache plus a 2^16 histogram per day, under 300 MiB. Nothing near the box's 125 GB.
|
|
|
|
## 3. Running rules followed
|
|
|
|
- No cargo build, test or run on the Mac. The harness builds on both boxes through `tools/build-remote.sh --box 1|2 -- build --release` from `tools/attack/adv-cache-2/`. Coordinator's order (19:4x BST): the CPU-bound sweeps go to box 1 explicitly (Q1 lines, Q3 weak-day scan, the Q1 extension), the table-heavy Q2 warps censuses to box 2; both boxes kept busy at nice 10 under the yield rule until the queue is empty. Logs under `/srv/builds/igneum-wt-adv-cache-2/adv/` on whichever box runs the sweep.
|
|
- Runs over 10 minutes start on the box from `tools/attack/adv-cache-2/run-box.sh`: `setsid nohup nice -n 10 <bin> <args> > <log> 2>&1 &`, pid file beside the log under `/srv/builds/igneum-wt-adv-cache-2/adv/`, and the yield rule: the runner polls `/srv/builds/_locks` every 5 s with the `flock -n` probe of `capacity/lib.sh` (every `build-<k>` up to `slots`, `measure`, `quiet`, every `core-*`) and SIGSTOPs the process group while any is held, SIGCONT when clear. Kill by pid file only (`kill -- -<pgid>` from the pid file). No GPU row exists in this lane; a GPU measurement is BLOCKED and the report says so.
|
|
- The queue: every sweep is a self-contained executable file `/srv/builds/_adv/cache/queue/NN-adv-cache-2-<name>.sh` (binary path, args, log path, pid file) on the box that runs it (box 2 as the brief says; box 1 for the sweeps the coordinator moved there, in the same directory layout); a file is claimed before it runs with `mkdir /srv/builds/_adv/cache/claims/<filename>` on that box. When my queue is empty and time remains, I claim the next unclaimed file in name order (a sibling's), run it, and note the owner in my report. No gaps: the next sweep starts the minute the last ends.
|
|
- Box-hours: 8 is a reading (reported when crossed, work continues), 16 is the ask line.
|
|
- Pushes only to `build adv-cache-2`; `origin` never touched; every other branch read-only. Commits as igneum-labs.
|
|
- No secrets, keys or host lines in any document.
|
|
|
|
## 4. Order of work and clock (BST, 7 to 8 October 2026)
|
|
|
|
| Step | What | When |
|
|
|---|---|---|
|
|
| 1 | This plan committed and pushed | by 20:25 |
|
|
| 2 | Harness crate written, built on box 2; the self-test (day 20730 cache FNV `0x448274a57f508cbc`, day 20733 `0x7334fa46e5d972eb`, program ids `a785001687d8688a` and `fce15bf61030be57` regenerated from the seeds, `Epoch::hash_warp` agreement, the pack's warp 0 vectors); every plant fired and logged | 20:25 to 22:00 |
|
|
| 3 | Q1 at 2^28, Q2 real programs, Q3 (1) queued back to back; first rows in the report | 22:00 to 00:00 |
|
|
| 4 | Q2 drawn programs, Q3 (2) weak-day scan, Q3 (3) windows census, Q4 arithmetic, Q1 extension to 2^30 | 00:00 onward |
|
|
| 5 | `docs/analysis/cryptanalysis/report-chained-cache-2.md` pushed as rows land; first results by 00:00 | from 22:30 |
|
|
|
|
## 5. What a result is
|
|
|
|
A BREAK is a method with a counted gain (a measured hit rate above `f`, priced in ops per hash against the model's row for that `f`), reproducible from the command and the seed in the report. A BOUND is what was searched, with which tool, how far (derivations, nonces, days, programs) and the margin (sigma, or the factor between the measured share and `f`). "Nothing found" counts only with its effort stated.
|