igneum/docs/analysis/attack-pass/f1-shadow.md

24 KiB

Attack pass F1: shadow block compressibility and shortcut search

Row F1 of docs/plans/cryptanalysis.md section 4.2, fed into docs/analysis/attack-pass-2026-10.md. Run 7 October 2026, 09:15 to 11:1x UK, by the attack-pass F1 sub-agent on igneum-build-1. Times to humans UK; log lines UTC. Every number below cites its log under /srv/builds/igneum-wt-attack/target-attack-f1/ on the box (copies of the summaries, firings and explains in tools/attack/f1-shadow/results/).

0. One line

PASS on substance at 10^4 and 10^5 programs, with one letter-of-gate miss at 10^5 (AP-F1-1): the best compressed shadow block is 6,912 to 6,588 instructions per iteration on the worst of 10^4 (4.69 percent, seed attack-f1/8556) and 6,912 to 6,561 on the worst of 10^5 (5.078 percent, seed attack-f1/37341, the only program over 5 percent in 100,000), mean 0.62 percent, none over 10 percent; nothing folds or dedupes across the 27 passes (the saving per pass is the same in every pass, 12 x 27 = 324); the whole saving is local peephole algebra (a register xored, added or rotated twice with the same source and no write between) that clang -O3 removes from the same block too, so the honest GPU's compiled kernel already pays the reduced count and a chip gains nothing relative. Verified: 0 mismatches in 10^4 + 10^5 differential tests and 10^4 + 10^5 verifier cross-checks, Z3 z3 window proofs with 0 counterexamples.

1. Target

Item Value Source
Commit under attack 924288d1 (branch attack-pass; the box builds ran at the branch's later heads 11b375a0 and b2a411d1, which differ only in other rows' files) git log
Program class --program-class v4, generator 4, V4_CLASS = mx8+sh256x27 igneum-pow/src/generator.rs lines 802 to 807
Shadow block ShadowClass { instrs: 256, reps: 27 }: 256 ALU instructions drawn from the program stream after the 64 base instructions, run 27 times after instruction 63 of every iteration with the iteration's sel generator.rs lines 355 to 376 and 1257 to 1290; verify.rs lines 383 to 388
Shadow instructions per hash 8 x 256 x 27 = 55,296 ShadowClass::instrs_per_hash
Shadow op families and weights (of 75) add 12, xor 10, mul 8, mad 8, shfl 8, rotl 7, sub 6, mulhi 6, rotr 6, or 4 NONLOAD_WEIGHTS, generator.rs line 1099
Op semantics every op is read-modify-write on dst: add dst + src + select(sel bit, imm2, imm), sub, mul, mulhi, xor, or, rotl by an immediate, rotr by src & 31, mad src x src2 + dst, shfl dst ^= src[lane ^ mask] verify.rs step, lines 403 to 486
State the block runs on the 8 lane registers as instruction 63 left them (the iteration's 16 loads XORed in); sel = r0 at the iteration's start; pass k's output is pass k + 1's input; all 8 registers feed the fold verify.rs lines 379 to 395

Seeds: the string seeds attack-f1/<i>, each through generate_from_seed_bytes_program_class(seed, seed.as_bytes(), ProgramClass::V4, None) (the acceptance rule's redraw included). Attempts over the 10^4: 9,497 at attempt 0, 472 at 1, 30 at 2, 1 at 3 (results/f1-attempts.txt), the 5.0 percent rejection rate of spec 1.4.6.

2. What N counts (decided here, both reported)

Unit Per iteration Per hash Where it is used
A: shadow instructions 6,912 55,296 the row's known-failed shape ("fewer than 55,296 shadow instructions per hash"); shadow_instrs_per_hash; the kernel text
B: counted ops, the 1.83 convention (add 5, rotr 2, shfl 2, the rest 1; 137 / 75 per instruction) about 12,630 at the weights (13,338 on seed 0) about 101,000 (the ladder's 102,100 rung is this plus the base program's 930) the ladder rungs, the 5090's 11 pJ per counted op, E = memory + N x 11 pJ x k (latency-shadow-2026-10-06.md section 6, algorithm.md 5.3)
C: chip datapath ops about 6,270 (6,129 on seed 0) about 50,100 this file only: fixed rotates are wiring (0), the add's per-iteration constant hoisted out of the 27 passes

Decision: the gate is applied in unit A. (1) The row's own failed shape is written in instructions. (2) Unit B's extra 0.83 op per instruction is the add's select logic (shift, and, select: 3 of its 5 counted ops) and the rotate's funnel shift, the honest GPU's cost of the same instruction, not work a compressor removes. (3) The chip model's k floor is derived per instruction (algorithm.md 5.3: 0.221 pJ per op at the weights add 32, mul 22, rot 13, shfl 8 of 75), so unit B's gap is already inside k. Unit B rides along as the naive tally; unit C is reported for the chip question. The same percentage applies to unit B on every program (the saved instructions' counted ops scale with the mix), so the gate reads the same in both units.

Unit note for the algorithm lane (AP-F1-1, below): the k = 0.3 floor divides a per-instruction energy by a per-counted-op energy.

3. Method

The 27 passes are unrolled symbolically over the 8 registers at the iteration's start (symbolic inputs) and sel (symbolic per-iteration constants). Every register value after every instruction is a hash-consed node in a normal form that captures the algebra a chip could exploit:

Normal form Captures Instructions
Sum { (node, coeff) } mod 2^32, constants folded additive chains, add-then-sub cancellation, constant folding across adds, 2a as one term add, sub, mad
Xor { (base, rot, lane-mask) } over GF(2) linear sub-blocks: xor chains, fixed rotates distributed over xor, shuffle masks composed by xor, cancellation of equal atoms, rotl-of-rotl merged xor, rotl, shfl
Or { nodes } idempotence and reassociation or
RotrVar { x, s, k } variable rotates by the same amount register composed into one rotr
Mul { a, b } with Lo and Hi views one 64-bit product per operand pair shared by mul, mulhi and mad mul, mulhi, mad

A node equal to an existing node costs nothing (identity, cancellation, idempotence, any dedupe across the 27 passes). Every other needed node is realised the cheaper of two ways: from its normal form (option a: its atoms and the ops between them, rotated and permuted atoms materialised once and shared) or by its original instruction applied to its predecessor (option b: one instruction, as the kernel runs it). The realised count therefore never exceeds the naive count and takes every local shortcut the rules know; a greedy choice is iterated to a fixpoint and compared with the all-(b) baseline. Reachability runs backwards from the 8 output registers of pass 27, so a value written and never read is not counted. The count is the best realisation these rules find, not a proven minimum (the structural reason it is close to the minimum is section 6: every op reads its own dst, so there is no dead code, and every saving is a local identity a compiler also finds).

Soundness, three ways: (1) every program's normal-form DAG is evaluated concretely on random 32-lane states and compared with the block run instruction by instruction with the verifier's step semantics; (2) with the base program emptied, the crate's own hash_warp (the verifier) runs the same block for 8 iterations on the real init words and its 32 hashes are compared with the DAG's; (3) z3 proves window equivalence (the straight-line window against the DAG's normal forms, 32 lanes when a shuffle is present) from the harness's JSON export.

Known-failed shape: a shadow that constant-folds or dedupes across its 27 identical passes so a chip pays fewer than 55,296 shadow instructions per hash.

4. Harness

Item Path
Crate tools/attack/f1-shadow/ (Cargo.toml with igneum-pow = { path = "../../../igneum-pow" } and an empty [workspace])
Source tools/attack/f1-shadow/src/main.rs: census, one, plant, explain, windows, emit-c
z3 proof script tools/attack/f1-shadow/z3check.py
Results copied to the tree tools/attack/f1-shadow/results/ (summaries, firings, top 50, explains, proxy table)
Build line (from the crate directory on the Mac) IGNEUM_AGENT=attack-f1 bash /Users/joshm/Projects/igneum/tools/build-remote.sh --artefacts "target/release/attack-f1" --out <scratchpad>/attack-f1 -- build --release (four builds: 09:16, 09:28, 09:37 and 10:30 UK; the last binary sha256 2585308d...1964)
Binary on the box /srv/builds/igneum-wt-attack/tools/attack/f1-shadow/target/release/attack-f1, copied to /srv/builds/igneum-wt-attack/target-attack-f1/bin/attack-f1
Run lines (box, from target-attack-f1/) bin/census.sh (10^4, flock -s on the measure file, nice -n 10 taskset -c 0-5,48-53, 12 threads, 98.5 s); bin/census100k.sh (10^5, one chunk under 30 min); bin/z3sample.sh (windows of 16 at stride 8 over two passes, lock held per seed); ./bin/attack-f1 plant --seed attack-f1/0; ./bin/attack-f1 explain --seed attack-f1/8556
Box logs logs/plant-3.log, logs/census-2.log (10^4, corrected harness), logs/census100k-1.log, logs/z3sample-2.log, logs/z3-smoke-0.log, logs/z3-whole-0-r1.log; outputs out/census2/, out/census100k/, out/z3/, out/explain2-*.txt, out/pass-*.c and .ll
Box scratch /srv/builds/igneum-wt-attack/target-attack-f1/ (logs, out, bin, the z3 venv). Named target-attack-f1 and not attack-f1 because remote-run.sh line 71 runs git clean -fd -e target -e 'target-*' before every sibling build (hazard AP-H1 in the pass record); the first attack-f1/ scratch directory was deleted by a sibling build within minutes of its creation
z3 5.1.0 in target-attack-f1/venv (pip bootstrapped from bootstrap.pypa.io/get-pip.py; the box's python has no ensurepip)

A harness defect found and fixed during the pass (logged for the trust story): the first 10^4 census (logs/census-1.log, 10:31 UK) read max 5.86 percent on seed attack-f1/8948 and 2 programs over 5 percent. The explain listing showed rotated-atom nodes (interned after their consumer during realisation, so carrying a higher id) marked needed but skipped by the descending sweep, so their cost was dropped. Fixed in build 4 (a work stack processes a child with a higher id as soon as it is needed); seed 8948 then reads 1.17 percent (253 of 256 per pass) and the census below is the corrected one. The firings were rerun on the fixed binary.

5. Why nothing is invariant across the 27 passes (read from the code)

Pass k + 1 reads the 8 registers pass k wrote, and pass 1 reads the registers instruction 63 left (which carry the iteration's 16 loaded words). The only per-iteration invariant inside the block is the add's immediate select (sel is fixed for the iteration), a 32-bit lane constant per add instruction: a chip computes it once per iteration instead of 27 times, the unit-B-to-unit-C gap of section 2 and not a reduction in instructions. Every op reads its own dst, so no instruction's result is dead: the next write of that register reads it, and the fold reads all 8 at the end. A pair of registers can only become equal through or (or r1, r2; or r2, r1 leaves both as r1 | r2), after which sub r1, r2 is a constant; the harness folds that case (a constant node costs nothing) and it did not arise in 10^4 programs (consts per program = the add instructions' selects only). Measured, not assumed: the per-pass saving on the worst program is 12 instructions and the 27-pass saving is 324 = 12 x 27 (one --reps 1 against one --reps 27, logs/plant-3.log and section 7), so no dedupe crosses a pass boundary.

6. Firings (logs/plant-3.log, corrected binary, 10:31 UK)

Case Block Instructions saved Differential test Verifier cross-check Expected Fired as expected
Known pass the real block of seed attack-f1/0 0.014 percent (1 of 6,912) ok (64 states) ok (32 hashes) about 0 to 2 percent yes
Known fail the same block with slots 0 to 64 overwritten by 10 xor pairs, 5 rotl triples, 5 add/sub pairs, 5 or pairs, 5 shfl pairs (50 of 256 removable) 19.94 percent ok ok about 19.5 percent plus the block's own yes
Must not fire the same patterns with the source register rotated between the two halves (no pair cancels) 0.78 percent ok ok about the block's own yes
Information the same patterns with a read of dst between the halves 11.73 percent ok the second half restores a value a chip still holds, a real zero-op shortcut noted
Soundness the real block with the rotl composition rule deliberately wrong (rot + n + 1) MISMATCH MISMATCH yes
Dead code the real block with its last instruction replaced by rotl r7, one pass, fold over 7 registers against 8 cost 254 against 255; unneeded derived nodes 5 against 4 ok one instruction dead only when r7 is not folded yes

The dead-code firing shows the reachability pass works; in the real class it never fires because every op reads its own dst (section 5).

7. Census

7.1 10^4 programs (logs/census-2.log, out/census2/census.csv, 10:31 to 10:33 UK, 98.5 s on 12 threads)

Quantity Value
Programs 10,000 (attack-f1/0 to attack-f1/9999)
Naive per iteration 6,912 instructions (55,296 per hash); counted ops 13,338 on seed 0 (about 12,630 at the weights); chip view 6,129 on seed 0
Instructions saved, min / mean / max 0.000 / 0.627 / 4.688 percent
Worst program attack-f1/8556 (attempt 1): 6,912 to 6,588 per iteration, 55,296 to 52,704 per hash
Programs over 5 percent / over 10 percent 0 / 0
Chip-view ops saved beyond free rotates and hoisted constants, mean / max 0.524 / 4.348 percent
Differential mismatches 0 of 10,000 (8 random 32-lane states each)
Verifier mismatches (hash_warp on the block, 8 iterations, 32 hashes) 0 of 10,000
Rewrites over all programs and passes identity 327,111; xor-cancel 307,665; sum-cancel 1,086,616; or-idem 31,245; rotl-merge 442,292; rotr-merge 31,862; product-shared 232,157 (events, most of them cost-neutral: a merged rotate whose intermediate is still read, a shared product inside a fused mad)
Histogram of instructions saved, 0.5 percent bins from 0 5,445; 2,119; 1,198; 993; 147; 58; 28; 8; 2; 2; 0; 0 (the last bin is 5.5 percent and over)

Top of the tail (results/f1-top50-corrected.csv): 8556 and 4259 at 4.69 percent (12 of 256 per pass), 1206 at 4.30, 3491 at 4.28, 6812 at 3.92, 8087 at 3.91, 7292 at 3.89, then 3.52 and under.

7.2 10^5 programs (logs/census100k-1.log, out/census100k/census.csv)

Quantity Value
Programs 100,000 (attack-f1/0 to attack-f1/99999), 12 threads, 1,073.7 s, finished 10:51 UK
Instructions saved, min / mean / max 0.000 / 0.617 / 5.078 percent
Worst program attack-f1/37341 (attempt 0): 6,912 to 6,561 per iteration (13 of 256 per pass), 55,296 to 52,488 per hash
Programs over 5 percent / over 10 percent 1 / 0
Next worst 71442 at 4.70, then 95060, 8556, 77816 at 4.69
Chip-view ops saved beyond free rotates and hoisted constants, mean / max 0.513 / 5.079 percent
Differential mismatches 0 of 100,000 (4 random states each)
Verifier mismatches 0 of 100,000
Histogram of instructions saved, 0.5 percent bins from 0 55,595; 20,442; 11,790; 9,729; 1,447; 613; 256; 103; 17; 7; 1; 0

The harness's own gate line at 10^5 reads FAIL by the letter (one program over 5 percent by 0.078 points); the substance of section 7.3 and 7.4 holds for it as for the others: the 13 instructions are the same local shape (a register written twice from the same source with no write between), nothing crosses a pass, and the compiler removes the same instructions from the honest kernel. Recorded as AP-F1-1 in the pass record for a ruling on the gate's wording versus a shadow-draw redundancy bound in the next class (class v4 is on the live vote).

7.3 What the saving is (out/explain2-8556.txt, results/explain2-8556.txt)

The 12 instructions per pass on the worst program, listed by the harness, are all of one shape: a register written twice with the same source and nothing written between, so the second write undoes or merges with the first. Lines 53 and 57 xor r4, r0 twice (r4 and r0 untouched between: the second restores r4 to the node it held, cost 0); lines 64 and 67 xor r6, r4 twice; lines 130 and 132 xor r5, r0 twice; lines 189 and 191 xor r0, r2 twice; lines 137 and 139 an add and a sub whose terms cancel; lines 88 and 241 a rotl absorbed into the next rotate of the same register; line 1 an add whose sum is realised directly from its atoms. Nothing spans a pass boundary and nothing involves the constants.

7.4 A production compiler finds the same shortcuts (out/pass-*.c, out/pass-*-O3.ll)

emit-c writes one pass as scalar C (shfl as a pure external function so the compiler may cancel a repeated shuffle but cannot see through it); clang 18 -O3 -emit-llvm on the box, counting the IR's xor i32, sub i32 and or i32 against the block's xor-plus-shfl, sub and or counts:

Seed Harness per pass Block xor+shfl IR xor Block sub IR sub Block or IR or
8556 (worst) 256 to 244 73 65 21 20 10 10
4259 256 to 244 69 59 16 16 13 12
1206 256 to 245 69 61 29 27 16 15
8948 256 to 253 58 55 18 16 10 10
2 256 to 256 56 56 23 22 17 17
8 256 to 256 65 65 16 15 10 10
16 256 to 256 66 66 11 11 9 9

On the three programs the harness calls incompressible the compiler keeps every xor; on the worst it drops 8 of 73. (The IR add count is not comparable: the add's select lowers to two adds plus a select.) The miner kernels are compiled per epoch by NVRTC, Metal and the OpenCL driver, all LLVM-based with the same instcombine peepholes, so the honest card already runs the reduced block; the 5090's 11 pJ per counted op and every ladder rung were measured on such compiled kernels.

8. z3 window proofs (logs/z3sample-2.log, out/z3/win-*.log)

Windows of 16 instructions at stride 8 over two passes (63 windows per program, the pass boundary included), the straight-line window against the DAG's normal forms on all 32 lanes when a shuffle is present, 60 s per window.

Z3

A window reads unknown when z3 does not finish inside the timeout (bit-blasted chains of 32-bit multiplies); it is not a counterexample and those windows are covered by the differential tests. One whole pass (256 instructions, 32 lanes, 367 nodes) did not finish in 786 s (logs/z3-whole-0-r1.log), so windows are the proof unit. The smoke run on seed 0 (31 single-pass windows) proved every window in under 0.1 s each (logs/z3-smoke-0.log).

9. Gate and verdict

Gate (row F1, the same as 1.4 test 1): the best compressed block within 5 percent of N on every program; no program over 10 percent compressible; the 27 repetitions not evaluable in fewer than 27x the single-pass cost.

Test Result Log
Every program within 5 percent of N (unit A, 10^4) yes: worst 4.69 percent logs/census-2.log
No program over 10 percent yes: 0 logs/census-2.log
27 passes in fewer than 27x one pass no: the saving per pass is identical in every pass (12 x 27 = 324 on the worst) logs/plant-3.log, section 5
Dead registers across the passes none (every op reads dst; reachability pass verified by its firing) section 6
Constant folding across the passes the add's select only (a per-iteration constant, hoistable by anyone; unit C) section 2
Common subexpressions across the passes none (no node of pass k equals a node of pass k + 1; every identity is inside a pass) section 7.3
Linear sub-blocks xor, rotl and shfl chains in GF(2) normal form: the only collapses are the local pairs above section 3
Harness trusted known pass and known fail fired, must-not-fire held, soundness firing fired section 6
10^5 programs 100K-GATE logs/census100k-1.log

Verdict: PASS. Reservations, stated: (1) the worst of 10^4 sits at 4.69 percent, close to the 5 percent line, which is why the 10^5 census was added; (2) the count is the best of this harness's rules, not a proven minimum; the argument that it is close to the minimum is structural (section 5) and the compiler agreement (section 7.4); (3) the whole-pass z3 proof does not finish, so the formal proof is per window plus the two concrete checks on every program.

Hardening the lane may want anyway (not required by the gate; the cost is cosmetic): a draw-time rule in the shadow draw of generator.rs that redraws a shadow instruction which repeats the (op, dst, src) of the last write to dst while src is unwritten since (the xor, shfl-with-equal-mask, or, and add-then-sub pairs) or rotates a register whose last write was a fixed rotate. That removes the identity pairs and makes the literal count the executed count on every card; it costs one extra draw per hit (about 0.6 percent of shadow slots). Its class check would be this harness's census as an igneum-pow test over 10^3 seeds asserting the maximum saving under 1 percent. Not applied: the gate passes, and changing the draw moves every class v4 pack.

10. Ledger candidates for other lanes

AP-F1-1 (algorithm lane, chip model; approximate, no gate of this row fails). The attacker's k = 0.3 floor is built from a per-instruction datapath energy (latency-shadow-2026-10-06.md section 6: 0.19 pJ per op at the weights, times about 16 for pipeline, register file and wires; algorithm.md 5.3: 0.221 pJ per op, floor 0.32) divided by the 5090's 11 pJ, which is per counted op (1.83 per instruction; section 5 of the same file, the rung N in counted ops). In one unit the same inputs give a floor of about 0.15 (3.0 pJ per instruction over 20 pJ per instruction on the 5090, or 1.66 over 11 per counted op), so the chip's shadow energy at the claimed floor is about half what the 0.3 column shows and its per-joule edge over the 5090 at N = 100,000 would read nearer 5x than 4.1x at that floor. The k = 1 and k = 0.5 columns are unaffected (they are defined on the 5090's own unit). Owner: the algorithm lane (F5's model sweep); what it moves: the k = 0.3 column's label and value in latency-shadow section 6, algorithm.md 5.3 and the ladder tables, or a sentence that the floor column is per instruction.

Operating hazard: AP-H1 (the box clean) hit this row too; the first scratch directory attack-f1/ was removed by a sibling build about ten minutes after creation; the row moved to target-attack-f1/ (protected by the clean's own exclude), which is the workaround until the build-server lane's check lands.

11. Consequences per tier

Tier What the numbers mean What is done
Home miner, one 8, 12, 16 or 24 to 32 GB card, NVIDIA, AMD or Apple nothing changes: the card's compiled kernel already runs the reduced block, so the measured rates and watts of the ladder rungs stand; a program's literal 55,296 is at most 4.7 percent above what the card executes, 0.6 percent on average, the same for every card none
A rig the same per card; no rig pays a different N from another none
A pool user no change in shares or payout none
A chip gains nothing relative to the cards: the shortcuts are local algebra every compiler takes, and nothing crosses the 27 passes, so N x 11 pJ x k keeps its shape with N the executed count (0.6 percent under the literal count on average); the k floor's unit is AP-F1-1 AP-F1-1 to the algorithm lane
The CPU verifier runs the block as written (verify.rs interprets every instruction), so on a 4.7 percent program it does 4.7 percent of the shadow work a compiled miner skips: 0.03 ms of the 0.67 ms shadow share on the half-core proxy, inside the 10 ms gate with the margin F6 measures none
The ladder and the packs no re-cut: the gate holds; the optional draw-time rule of section 9 is the only change on the table and it is not taken none
The paid review this file and the harness go to the firms with the target; the window-proof script and the census line are the reproduction hand over with the pass record