attack-pass: snapshot of the row drafts and harnesses as they stood at the Mac reboot (10:5x UK); the collector finishes them

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-07 10:02:44 +00:00
parent 537ff5b226
commit d7db49701b
81 changed files with 67348 additions and 0 deletions

View file

@ -0,0 +1,261 @@
# 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 at 10^4 and at 10^5 programs: the best compressed shadow block is 6,912 to 6,588 instructions per iteration on
the worst program (4.69 percent, seed `attack-f1/8556`), mean 0.63 percent, no program over 5 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`)
[[100K]]
### 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 |

View file

@ -0,0 +1,211 @@
# F10. The ladder's signal: monotonicity, the 89 percent case, the down-step, the memoisation
Attack-pass row F10 (`docs/plans/cryptanalysis.md` section 4.2; the pass record `docs/analysis/attack-pass-2026-10.md`).
7 October 2026, 09:05 to 10:30 UK. Sub-agent F10 on branch `attack-pass` (worktree `igneum-wt-attack`, HEAD 8e36faf6 at
the start of the work; the brief named 924288d1, the branch had moved on). Files: `tools/attack/f10-ladder/` and this
record. Nothing under `vendor/`, `infra/` or the node was edited.
## 1. Target
The ladder as PROPOSED on branch `ladder` (repo tip 7003f9f5, 6 October 2026 23:58 UK; also `release-0.3.18`), node
fork `ladder-node` tip 1591ee1d (`vendor/igneum-node-ladder`), `docs/design/latency-ladder.md` sections 3, 5a, 9 and 11.
| Item | Value at the commit run |
|---|---|
| The N ladder (counted ops) | 102,100; 132,100; 199,600; 330,700; 649,400; 1,001,600 (reps 27, 35, 53, 88, 173, 267; the design doc's round figures 100,000; 130,000; 200,000; 330,000; 650,000; 1,000,000) |
| Floor | rung 0, reps 27 = class v4 byte for byte (`V4_CLASS`) |
| Admissible rungs in the file run | 0, 1, 2 (rungs 3 to 5 `admissible: false`, the verifier table of section 5) |
| The step rule | `igneum::latency_ladder_step_signalled` (`consensus/core/src/igneum.rs` lines 664 to 687): up one rung when all 7 windows have at least 9,000 bps of blue blocks up, the rung above is admissible, and the oldest window begins at or after the DAA score where the current step took effect; down one rung by the same test on the down bit, never below 0; otherwise the state stands |
| The carrier | header `version` bit 15 = up, bit 14 = down, both or neither = none (`ladder_signal_of`); the object byte keeps bits 8 to 13; the low byte is the block version |
| The windows | 7 consecutive windows of `latency_ladder_window_daa` (86,400 DAA on mainnet, 120 on the 60x profile, 100 in the exact-share runs here) ending at the epoch's seed block; one walk of the seed block's blue past (`class_signal::tally_window_by`); share per window = floor(10,000 x signalling / total) |
| The seed block of epoch e | the last selected-chain block with DAA score strictly below `L e - lead` (`class_signal::seed_below`) |
| The memo | `processes::latency_ladder::step_of_epoch`: a static `HashMap<seed hash, LadderDecision>`, filled by walking earlier epochs' seed blocks down the selected chain to a memoised decision or the activation epoch, cleared when it passes 100,000 entries; empty at every process start |
The box binaries used, read-only: `/srv/builds/igneum-wt-ladder/vendor/igneum-node-ladder/target/release/{igneumd,igneum-miner}`
(built 6 October 2026 22:15Z, the fork checkout at 1591ee1d = the `ladder-node` tip, one untracked file
`.build-remote-sha-target`) and `/srv/builds/igneum-wt-ladder/igneum-pow/target/release/igneum-pow` (built 22:38Z, after
6b30e85). The `igneumd` binary's embedded commit string is 8dbb7a23, the PARENT of the ladder commit, not 1591ee1d
(`strings` finds 8dbb7a23 twice and 1591ee1d never); the ladder code is in it (the step line
`Latency ladder step by miner signal` is present, and every run below stepped). This is the stale-commit-string class
of `tools/ci/commit-string-check.sh` (CLAUDE.md, 6 October 2026); it does not touch the result, and it is reported to
main in section 8.
## 2. Known-failed shape and gate
Known-failed shape: a chip owner stepping the ladder down (cheaper N) without the 90 percent threshold, or a step
registered under 90 percent in either direction. Gate (plan 4.2 row F10, the same as 1.4): no step without 90 percent
over 7 windows in either direction; a step down needs the same. What a failure moves: the step rule's text in spec 01
before the ladder is frozen.
## 3. Method
Two instruments, both run on igneum-build-1 on the F10 cores (`nice -n 10 taskset -c 38-39,86-87`), each run under a
SHARED hold of the box measure file for the run only (every run capped under 30 minutes by its own `--secs`), on ports
29900 and up, devnet suffix 990, data `/tmp/igneum-fast-time-attack-f10`, so nothing collides with the ladder lane's
network (29720, 972) or F7's (29800, 980). Scripts and copies: `tools/attack/f10-ladder/` (box mirror
`/srv/builds/igneum-wt-attack/attack-f10/`, run logs under `runs/`).
| Instrument | File | What it is |
|---|---|---|
| The ladder lane's harness, verbatim | `tools/attack/f10-ladder/latency-ladder.mjs` | `infra/fast-time/latency-ladder.mjs` from `ladder` at 7003f9f5, unchanged except the root lookup, this directory's copy of the ladder branch's `override-60x.json` (the attack-pass tree's copy lacks the `latency_ladder` fields), and the F10 ports, suffix, data dir and binary paths. Three nodes, three real CPU miners (one thread each), class v4 from genesis, the ladder active from DAA 0, windows of 60 DAA. Trusted only after it fires on the known-failed case (`--signal up,up,none --expect step` must report FAIL) and the known pass (`--signal up,up,up --expect step`) |
| The exact-share driver, new | `tools/attack/f10-ladder/ladder-exact.mjs` | Three nodes on the same fork with `skip_proof_of_work`; ONE producer takes node 0's template, writes the ladder bits it wants into the header version and submits the block, one block per DAA score on a linear chain, so every window of W = 100 DAA holds exactly 100 blue blocks, one of each residue modulo 100. A schedule names per DAA range the direction and how many residues carry no signal: 11 residues give 8,900 bps in every window whatever the window's alignment, 10 give 9,000. "None" blocks alternate between no bits and both bits, so the chain shows both forms read as none. The driver polls every node's template (rung, weakest up, weakest down) through the run, restarts a node mid-window on request (SIGINT, same data dir, same arguments), and at the end re-tallies the chain in JavaScript (an independent copy of the rule: the seed rule, the 7 buckets, floor rounding, admissibility, the cool-down) and compares it with what the nodes did |
Why the second instrument: three equal miners cast 0, 33, 67 or 100 percent, and a real miner's share in any one
window scatters by several points (the lane's own runs: 5,833 to 6,333 bps weakest for a 67 percent population), so no
real-mining run can hold 8,900 to 8,999 bps in the weakest of seven windows. The rule is consensus-side and reads the
chain's headers, not the miner, so a chain whose headers carry exact shares asks it the exact question. The skip-PoW
network accepts every submitted block (each node logs `PoW rejected ... by igneum-lottery-v2-bound (daa N, nonce 0x0)` at
INFO and accepts the block; the chain-side fact is the block count on every node).
The arithmetic of the exact-share cases (L = 60 DAA per epoch, lead 10, W = 100, 7 W = 700; genesis and the first
produced block both sit at DAA 0, then one block per DAA): the seed block of epoch e is at DAA 60 e - 11; the seven
windows are full from epoch 12 (seed 709); the oldest window of epoch e is DAA [60 e - 710, 60 e - 611]; after a step
that took effect at DAA S the next decision is the first epoch with 60 e - 710 >= S.
| Case | Schedule (from DAA : direction : residues with no signal) | Expected by hand | Why |
|---|---|---|---|
| eighty-nine | 0:up:11, 1200:up:10 | no step through epoch 30 at a weakest of 8,900; rung 1 at epoch 31 when the weakest first reads 9,000; rung 2 at epoch 43, the first epoch after the cool-down; nothing else to epoch 45 | residue 10 turns from none to up at DAA 1,200; the oldest window's residue-10 block is 1,210 at epoch 31 (1,110 at epoch 30); after the step at DAA 1,860 the first epoch with 60 e - 710 >= 1,860 is 43 |
| down | 0:up:0, 720:down:11, 1500:down:10, node restarts n2 at DAA 1,000, n1 at 2,300, n2 at 2,700 | rung 1 at epoch 12 (100 percent up); no step down at 8,900 down (epochs 24 to 35, the first cooled-down epoch is 24); rung 0 at epoch 36 when the weakest down first reads 9,000; then down at 9,000 through epoch 50 with no step below 0 (epoch 48 is the first cooled-down epoch after the down-step and the rule must hold at rung 0) | the oldest window's residue-10 block is 1,510 at epoch 36 (1,410 at epoch 35); after the down-step at DAA 2,160 the first epoch with 60 e - 710 >= 2,160 is 48 |
| floor | 0:down:0 | no step at all through epoch 20 | 100 percent down at rung 0 from genesis: the windows are full from epoch 12, the cool-down is trivially met, the rule must stand at 0 |
## 4. Runs
All on igneum-build-1, 7 October 2026. Times UK (BST, UTC+1); the logs are UTC. Every run held the measure file
shared for its own length only; the first waited behind F6's exclusive hold (its batch A, 09:15 to 09:25 UK). Log paths
are under `/srv/builds/igneum-wt-attack/attack-f10/runs/` on the box, copied to `tools/attack/f10-ladder/runs/` here
(`<name>.log` = harness stdout, `<name>.json` = summary, `<name>-n{0,1,2}.log` = node logs).
### 4.1 The harness, trusted: the known-failed case and the known pass (real CPU mining, W = 60 DAA)
| Case | Run (UK) | Result | Numbers | Files |
|---|---|---|---|---|
| Known-failed, `--signal up,up,none --expect step` | 09:25:51 to 09:36:50 | FAIL rc=1, as it must: no step | no step over epochs 0 to 10; weakest-of-seven up share at the sink 5,833 bps from epoch 7 (5,500 at epoch 10); on the chain 385 blocks up, 221 none (6,353 bps up); 606 blocks; 0 rejected; one sink 4a7f20cc at 605/605/605; the 8 step checks failed (template_stepped_to_rung_1 ... rung1_ids_differ_from_the_same_seed_rung0_id); the lane's genesis low-byte fault did not fire (fixed in the file) | `baseline-fail.log`, `.json` |
| Known pass, `--signal up,up,up --expect step` | 09:36:50 to 09:48:00 | PASS 18 of 18 | step line on 3 of 3 nodes at epoch 8: `420 of 420 blue blocks up`, weakest up 10,000 bps, shares [10000 x 7]; template rung 1 (35 passes) from epoch 8 (DAA 480) at 538.2 s; epochs 9 and 10 at rung 1, one step line per node (no second step inside seven windows); 481 / 132 blocks across the boundary; 612 blocks up and genesis none (9,984 bps); 0 rejected; one sink 41e81944 at 612/612/612; the miners' rung-1 ids on epochs 8, 9, 10 equal the CLI's `--shadow-reps 35` id and differ from rung 0 (e8 218fa530b4c599b0 against 5c5a326a31a4795d, e9 8f30ce6666b4ea8f against c73f3c63daac3748, e10 e2ea0a1ea8b4ca44 against 626455372164a1b5) | `baseline-pass.log`, `.json` |
Both reproduce the ladder lane's runs of 6 October (`docs/design/latency-ladder-harness/`), on the F10 cores.
### 4.2 The exact-share cases (skip-PoW, one block per DAA, W = 100 DAA, 8 blocks per second)
| Case | Run (UK) | Harness line | What the chain did | Files |
|---|---|---|---|---|
| eighty-nine (first run, driver v1) | 09:48:00 to 09:54:27 | FAIL rc=1 on three harness faults (section 4.3); the chain's facts are those of the re-run | identical to the re-run below | `exact-89.log`, `.json` |
| eighty-nine (re-run, driver v2) | 10:04:45 to 10:11:14 | PASS 19 of 19 | 2,701 blocks, linear; 2,418 up, 283 none (135 of them with both bits); weakest up 8,900 bps at every epoch 12 to 30 and NO step (19 epochs, "stands" on every node); epoch 31: weakest 9,000 exactly, step line on 3 of 3: `630 of 700 blue blocks up`, shares [9000 x 7], rung 1 (35 passes); epochs 32 to 42 at 9,000 with no step (cool-down: the oldest window begins 1,210 to 1,810, the step took effect at 1,860); epoch 43: rung 2 (53 passes), `630 of 700`; 44 and 45 cool-down; 0 disagreements between nodes at any poll; one sink 1dd776b4 at 2700/2700/2700; 2 step lines per node; 382 s | `exact-89b.log`, `.json`, `-n0.log` |
| floor (driver v2) | 10:01:40 to 10:04:38 | PASS 19 of 19 | 1,201 blocks; 1,200 down, genesis none; from epoch 12 every window reads 10,000 bps down at rung 0; the rule stands on every node for epochs 12 to 20 ("down signalled at rung 0: the floor"); no step line on any node; one sink a52e6a71 at 1200/1200/1200 | `exact-floor.log`, `.json` |
| down (first run, driver v1) | 09:54:27 to 10:01:40 | FAIL rc=1 on the same three harness faults | identical to the third run below, restarts included | `exact-down.log`, `.json`, `-n1.log`, `-n2.log` |
| down (second run, driver v2) | 10:11:14 to 10:18:26 | FAIL rc=1 on one harness fault (the anchor comparison at the two boundary epochs 13 and 23, section 4.3); 17 comparable epochs equal; the step lines' own weakest equal the oracle | identical to the third run | `exact-downb.log`, `.json`, `-n{0,1,2}.log` |
| down (third run, driver v3) | 10:19:13 to 10:26:25 | PASS 19 of 19 | 3,001 blocks, linear; 720 up, 2,053 down, 228 none (110 with both bits); epoch 12: rung 1 on 3 of 3 (`700 of 700 blue blocks up`, weakest up 10,000); epochs 13 to 23 cool-down (the oldest window begins 70 to 670, the step took effect at 720); epochs 24 to 35: weakest down 8,900 bps on every node, NO step down (12 epochs "stands"); epoch 36: weakest down 9,000 exactly, step line on 3 of 3: `0 of 700 blue blocks up, 630 down`, rung 0 (27 passes, from rung 1); epochs 37 to 47 cool-down; epochs 48 to 50: 9,000 down at rung 0, the rule stands (never below 0), no third step line; restarts: n2 at DAA 1,004 (1 step line before, 4 after), n1 at DAA 2,304 (2 before, 2 after), n2 at DAA 2,704 (3 before, 2 after), every line after a restart identical in epoch, rung, origin and weakest to the lines before; 0 disagreements; one sink 20c6b367 at 3000/3000/3000; step lines 2 / 4 / 5 per node; 425 s | `exact-downc.log`, `.json`, `-n{0,1,2}.log` |
Per epoch, the down case as the nodes and the oracle saw it (from `exact-downc.json`; "rungs" = the first template of the
epoch on n0 / n1 / n2; "weakest" = the decision's number from the step line where one exists, else the template's live
sink tally, which equals the seed-anchored oracle at every epoch with no schedule boundary inside the windows):
| Epoch | Seed DAA | Rungs n0/n1/n2 | Weakest up / down (bps) | Oracle rung | Oracle reason |
|---|---|---|---|---|---|
| 11 | 649 | 0/0/0 | partial | 0 | windows not full |
| 12 | 709 | 1/1/1 | 10,000 / 0 | 1 | up: 700 of 700 |
| 13 to 23 | 769 to 1,369 | 1/1/1 | mixed, under 9,000 both ways | 1 | cool-down (oldest window begins before 720) |
| 24 to 35 | 1,429 to 2,089 | 1/1/1 | 0 / 8,900 | 1 | stands: 8,900 is under 9,000 |
| 36 | 2,149 | 0/0/0 | 0 / 9,000 | 0 | down: 630 of 700 |
| 37 to 47 | 2,209 to 2,809 | 0/0/0 | 0 / 9,000 | 0 | cool-down (oldest window begins before 2,160) |
| 48 to 50 | 2,869 to 2,989 | 0/0/0 | 0 / 9,000 | 0 | down signalled at rung 0: the floor |
And the eighty-nine case (`exact-89b.json`):
| Epoch | Seed DAA | Rungs n0/n1/n2 | Weakest up (bps) | Oracle rung | Oracle reason |
|---|---|---|---|---|---|
| 12 to 30 | 709 to 1,789 | 0/0/0 | 8,900 | 0 | stands, 19 epochs |
| 31 | 1,849 | 1/1/1 | 9,000 | 1 | up: 630 of 700 |
| 32 to 42 | 1,909 to 2,509 | 1/1/1 | 9,000 | 1 | cool-down (oldest window begins 1,210 to 1,810, the step took effect at 1,860) |
| 43 | 2,569 | 2/2/2 | 9,000 | 2 | up: 630 of 700 |
| 44 to 45 | 2,629 to 2,689 | 2/2/2 | 9,000 | 2 | cool-down |
### 4.3 Harness faults found and fixed on the way (the driver's, never the chain's)
| Fault | Seen | Fix |
|---|---|---|
| `every_produced_block_on_every_node` compared `blockCount` with produced + 1; the node's `blockCount` excludes genesis | exact-89 first run, 09:54 UK | compare with produced (2,700 = 2,700) |
| `zero_rejected_by_nodes` grepped `ban` and matched the finality parameter line `... ban 120 ...` | same run | the word dropped; the skip-PoW INFO line `PoW rejected ... by igneum-lottery-v2-bound` excluded by its own text |
| `node_weakest_equals_oracle_weakest` compared the template's weakest with the seed-anchored oracle at every epoch; the template's number is the LIVE tally anchored at the sink (`consensus/mod.rs` `get_pow_epoch_info`, `tally_ladder(..., sink, ...)`), read at the epoch's first template, sink = seed + lead (10 DAA) | epoch 11 of exact-89 (49 of 59 at the sink against 39 of 49 at the seed); epochs 13 and 23 of the second down run (40 up in (679, 779] against 50 in (669, 769], the boundary at 720 inside both) | compared only at epochs with seven full windows and no schedule boundary inside the windows plus the lead; a new check compares the decision's own weakest (the step line) with the oracle at every stepped epoch, which passed in every run |
The smoke run (`smoke.log`, 09:14 UK, 3 epochs) validated the template round trip (`submitBlock` reports
`{"type":"success"}`, 180 blocks on 3 of 3 nodes at 8 per second).
## 5. What the runs show against the gate
| Gate clause | Shown by | Numbers |
|---|---|---|
| No step up without 90 percent over 7 windows | eighty-nine: 19 epochs at 8,900 bps in every window, rung 0 held on every node; the step came at the first epoch whose weakest read 9,000, 630 of 700 blue blocks | epochs 12 to 30 stand; 31 steps |
| No step down without 90 percent over 7 windows | down: 12 cooled-down epochs at 8,900 bps down in every window, rung 1 held on every node; the step down came at the first epoch whose weakest down read 9,000, 630 of 700 | epochs 24 to 35 stand; 36 steps |
| A step down needs the same cool-down | down: epochs 13 to 23 at rung 1 with the oldest window beginning before the step took effect: the rule stood although the up share had collapsed | 11 epochs |
| Never below 0 | floor: 10,000 bps down at rung 0 for 9 epochs, no step line; down: 9,000 bps down at rung 0 for epochs 48 to 50 after the cool-down, no step line | 12 epochs across two runs |
| Monotone: one rung per decision, seven windows between decisions | eighty-nine: rung 1 at 31, rung 2 not before 43 with 9,000 in every window throughout; down: rung 1 at 12, rung 0 at 36 | the cool-down held 11 epochs each time |
| The decision computed once per seed block and reused | one or two step lines per process per stepped epoch (two when the first template and header processing walked concurrently), none afterwards | n0: 2 lines for 2 steps in every exact run |
| A node restarted mid-window reaches the same decision | three restarts in the down case: every step line after a restart repeats the lines before it in epoch, rung, origin and weakest; the restarted node's template rung equals the others' at every epoch | n2 at 1,004 and 2,704, n1 at 2,304 |
| Two nodes never disagree on the rung at the same height | 0 disagreements at every observation (every fifth block) and at every epoch's first template, in every run | 5 exact runs, 2 baseline runs |
| Both bits = none | 135 and 110 both-bits blocks counted as none by the oracle and by the nodes (the shares matched) | eighty-nine, down |
| The known-failed shape (a chip owner stepping down under 90 percent; a step registered under 90 percent) | did not occur; 8,900 held in both directions, floor rounding puts 8,999 below the line (unit test, `igneum.rs` 1161) | gate holds |
## 6. Static reading of the rule (what the harness cannot show)
Read in the fork at 1591ee1d before the runs. Each line is a property of the code as written, with the place.
| Property | Where | Reading |
|---|---|---|
| Symmetry of the two directions | `igneum.rs` 676 to 686 | one closure `all(shares)` serves both bits; the up branch runs first, then `all(down) && previous.step > 0`; up and down cannot both reach 9,000 bps of one window's blocks, so the order never decides |
| The cool-down is direction-free | `igneum.rs` 674 | `first_counted_daa < previous.since_daa` returns the previous state before either branch is read; a step down waits the same seven windows after a step up as a step up does after a step down |
| Never below 0 | `igneum.rs` 681 | `previous.step > 0` guards the subtraction; a 100 percent down signal at rung 0 stands (the floor case below shows it on the chain) |
| Never past an inadmissible rung | `igneum.rs` 679 | `ladder.admissible(previous.step + 1)`; rung 3 is `admissible: false` in the file, so from rung 2 a 100 percent up signal stands (unit test `latency_ladder_rule`, `igneum.rs` 1161) |
| Floor rounding | `igneum.rs` 431 to 437 | `signal_share_bps` = floor(10,000 x signalling / total); 89 of 100 blue blocks is 8,900, 90 is 9,000; on a mainnet window of 86,400 blocks 77,759 up is 8,999 and 77,760 is 9,000 |
| Both bits set | `igneum.rs` 639 to 645 | `version & 0xc000 == 0xc000` falls to `None`; a header cannot vote both ways and cannot vote twice |
| Weakest of seven | `class_signal.rs` `SignalTally::weakest_bps` and the rule's `all` | the decision rests on the lowest of the seven windows; one bought window at 100 percent moves nothing (unit test "one bought day does not move it") |
| The windows are the seed block's own past | `class_signal.rs` `tally_window_by` | the anchor and the mergeset blues of each selected-chain block walking down, bucketed by `daa_c - daa`, stopping once `daa_cur + merge_depth < window_start`; blocks above the seed are never counted, so the seven windows are fixed once the seed block is |
| The memo is sound | `latency_ladder.rs` `step_of_epoch` | keyed by the seed block's hash; the decision is a function of that block's selected-chain past and of process-global constants installed from the file (ladder, activation, window), so two processes with the same file and the same chain compute the same value; the memo is never read across a param change because the params are fixed at start; cleared above 100,000 entries, then rebuilt by the walk |
| Concurrent first computation | `latency_ladder.rs` `memo_get` / `memo_put` | the lock is not held across the walk, so two concurrent callers may both walk and both log the step line; both write the same value, so the chain's decision is unaffected (the runs below show one or two step lines per process for the same epoch, identical in content) |
| A node without the history | `latency_ladder.rs` `step_of_epoch`, the two `warn!` returns | a node whose seed block's windows cannot be walked (synced from a pruning proof) decides RUNG 0 and logs "a ladder witness is owed". After a step up, such a node runs rung 0's program and refuses rung 1's blocks: a split between full-history nodes and proof-synced nodes. The design doc lists the witness as owed (section 9). This is not a fault of the step rule and the harness cannot reach it (every node here has the history); it is a precondition on activation: no network activates the ladder while any peer syncs from a proof without the witness. Routed to main in section 8 |
Nothing in the reading admits a step under 9,000 bps in either direction, a step down under the cool-down, a step
below rung 0, or a decision that depends on which node computes it or when.
## 7. Consequences per tier
The rule holds, so a step in either direction costs 90 percent of blue blocks in each of seven consecutive days, and
the earliest second step is seven days after the first. What a WRONGFUL step would have done, had the rule admitted one
under 90 percent, is the measured per-rung table of `docs/design/latency-ladder.md` section 8 (algorithm.md 5.3a rungs,
igneum-build-1 verifier) read in each direction. Every row below is that table's number, not a new measurement.
| Wrongful step | M5 Max (Apple tier) | RTX 5090 at 431 W | RTX 4070 at 160 W | RX 9070 XT | 8 / 12 / 16 GB cards, rigs, pools | Verifier (half-core) | f = 1 chip's per-joule edge over the 5090 |
|---|---|---|---|---|---|---|---|
| Up 0 to 1 (102,100 to 132,100 ops) under 90 percent | -3.3 points of rate, 0 W more | 0 | 0 | 0 | 0 (the shadow costs ALU, not memory; the dataset size is the schedule's, not the ladder's) | +0.2 ms | 2.1x to 1.7x at k = 1 (3.9x to 3.4x at k about 0.33) |
| Up 1 to 2 (to 199,600) under 90 percent | -6 more points | -2.7 percent | +21 W | 0 | 0 | +0.5 ms | to 1.3x (2.8x) |
| Up 2 to 3 (to 330,700): inadmissible, never entered | -21 percent | -35 percent (compute-bound at the cap) | -12 percent | +3.6 percent | 0 | +0.9 ms | 3.0x at k about 0.33 |
| Down 2 to 1, 1 to 0 under 90 percent (the chip owner's step) | the Apple tier gets its 6 then 3.3 points back | +2.7 percent then 0 | -21 W then 0 | 0 | 0 | -0.5 then -0.2 ms | the chip regains 1.3x to 1.7x to 2.1x (2.8x to 3.4x to 3.9x): every rung down hands the stored-dataset chip back the edge the miners paid for |
Reading per tier, with the rule as it stands:
| Tier | What the result means |
|---|---|
| Home card, 8 / 12 / 16 / 24 GB, any vendor, any OS | A step up costs rate only on the Apple tier at rungs 1 and 2, and on NVIDIA from rung 2; no step happens unless 90 percent of blocks over seven days ask for it, so a minority that would lose rate cannot be moved by a bought day or a 89 percent week, and a chip owner under 90 percent cannot move the rung down to cheapen its core. A 90 percent majority can step the chain down one rung per week to the floor (rung 0 = class v4 as it ships), which is the design's floor and not a weakness of the rule: at 90 percent of blocks the owner already orders the chain |
| Rig, pool user | The same; a pool signals per block through its node's `IGNEUM_LADDER_SIGNAL` (the app's toggle later), so a pool's share of blocks is its weight |
| Verifier (the node, the proof) | Admissibility is a genesis flag per rung; rung 3 is never entered by any signal until a quiet re-measurement before genesis moves the flag (section 4 of the design doc); the memo keeps the per-template cost to one walk per seed block per process |
| A node synced from a pruning proof | Decides rung 0 until the ladder witness lands (section 6, last row): the ladder must not activate on a network where such nodes exist before the witness. This is the one consequence the rule's text does not state and the spec line should |
## 8. Verdict, and what goes to main
PASS. No step without 90 percent of blue blocks in each of seven consecutive windows in either direction; a step down
needs the same 90 percent and the same seven-window cool-down; the floor holds under 100 percent down; the decision is
per seed block, memoised per process, recomputed identically after a restart, and never differs between nodes at the
same epoch. The known-failed harness case fails, the known pass passes, and three new cases (89 percent up, 89 then 90
percent down with restarts, the floor) pass on the chain and on the harness's own 19 checks. The step rule's text in
spec 01 needs no change for the gate.
To main, not findings against the gate:
| Item | What | Proposed route |
|---|---|---|
| Stale commit string in the ladder lane's `igneumd` | the binary built 6 October 22:15Z from the fork at 1591ee1d carries 8dbb7a23 (its parent) and no 1591ee1d; the ladder code is in it | the commit-string-check class (CLAUDE.md, 6 October 2026); the ladder lane rebuilds with the two-step before any Devnet 2 crossing; nothing in this row depends on it |
| Proof-synced nodes decide rung 0 until the witness lands | `processes::latency_ladder::step_of_epoch` returns rung 0 with a warning when the seed block's windows cannot be walked; after a step, such a node runs the wrong program and splits from full-history peers | a precondition line for the step rule's text in spec 01 when the ladder is adopted: "the ladder activates only once every node can walk the seven windows below every seed block, or carries the ladder witness in its pruning proof"; the design doc already lists the witness as owed (section 9); node lane |
| Spec text for the ladder, when adopted (none in spec 01 today; the only ladder there is `epoch_len`'s) | the rule as run: 90 percent of blue blocks in each of 7 consecutive windows ending at the seed block, floor rounding, one rung per decision, the oldest window at or after the last step in either direction, never below rung 0, never into an inadmissible rung; the template's weakest is the live sink tally and the decision's is at the seed | the algorithm lane's spec line; this record is the test it cites |
| Three harness faults in the F10 driver | section 4.3; all three were the driver's reading of the node, fixed in `ladder-exact.mjs` v3 | none owed; recorded so the firm does not repeat them |
Blocked: nothing. Not run: a real-mining 89 percent case (three equal miners cannot cast it; the exact-share driver
asks the rule the same question through the same submit path and the same consensus code).

View file

@ -0,0 +1,148 @@
# Attack pass F2: the mixer's round margin
Row F2 of `docs/plans/cryptanalysis.md` section 4.2 (branch `cryptanalysis`), fed into
`docs/analysis/attack-pass-2026-10.md`. Run 7 October 2026, 09:00 to [FILL] UK, by the attack-pass sub-agent F2 on
igneum-build-1 (cores 6-11 and 54-59, nice 10, the measure file held shared in chunks under 30 minutes).
## 1. Target
Commit `924288d1` (worktree `igneum-wt-attack`, branch `attack-pass`). The x8 mixer of `igneum-pow/src/memhard.rs`,
`mixer` (lines 300 to 313): one application on 16 words of 32 bits is, per word, `(s[i] ^ (RC[i] + rk)) * MUL[i]`
with `MUL[i]` odd, then one ChaCha-shaped double round: four column quarter rounds with rotations `ROT[0..3]`,
four diagonal quarter rounds with `ROT[4..7]`. `ROT`, `MUL`, `RC` are drawn per day from the 64-bit SplitMix64 seed
`K[0] | K[1] << 32` by `MixParams::with_shape` (lines 237 to 258). Under class v3 and v4 (`m = 8`) an item is 8
dependent cache reads, each preceded by 8 applications with round keys `round_key(r * 8 + j)`, and 8 more after the
last read: 72 applications per item (`derive_items_mask`, lines 517 to 550). The chip model prices one application
at 128 hoisted operations and an item at 9,360 (`docs/analysis/chip-model-v3.md` 5.2).
The days modelled: the genesis day `2026-10-03` (`ROT = 20 20 19 4 26 3 3 27`, as `proto-metal/MEMHARD.md` line 82
states; the harness reads the same draw from the code) and two other days, `2026-10-04` (`ROT = 28 15 9 26 2 2 22
8`) and `2027-03-01` (`ROT = 31 16 15 15 2 9 19 4`). Their full `MUL` and `RC` are in the box files
`/srv/builds/igneum-wt-attack/target-attack-f2/params/<day>.real.txt`.
Known-failed shape (the plan's row): a differential or linear trail, a rotational-XOR relation, or an algebraic fold
that distinguishes or shortcuts more than 2 of the 8 applications between dependent reads. Gate: none beyond 2 of 8.
## 2. Method
Four searches and two checks, every one on the bit-level definition in `memhard.rs` (the harness calls
`igneum_pow::memhard::mixer` itself; the SAT models consume one op list whose value evaluator is checked against
the Rust output on 64 applications per day and variant, 9 files, all matching).
| Piece | What it is | Exact or model |
|---|---|---|
| Differential, MSB family | XOR differences; at every multiply each word's difference is 0 or `0x80000000`. These are the only word transitions through an odd multiply with probability 1 (`(x ^ 2^31) * c = (x * c) ^ 2^31`; any other nonzero difference passes with probability at most 1/2, since its lowest active bit below the MSB leaves a carry to chance). Modular addition by Lipmaa-Moriai (exact per adder), XOR and rotation linear | exact family, trail probabilities exact per operation |
| Differential, general | The same ARX model with every word difference allowed through the multiply: XOR difference to modular difference (each set bit below the MSB is a sign choice, 2^-1 each, exact), times `MUL` (exact, a circuit on the difference variables), modular back to XOR (a carry chain, one bit per position where the difference bit and the carry differ, exact), the two conversions taken as independent | Markov trail model; its per-word cost sits 1 to 2 bits above the sampled best transition (section 4.1), so it is a trail model, slightly pessimistic for the attacker |
| Linear, low-bit family | Masks; at every multiply the output mask lies in bits 0 and 1, the only F2-linear output bits of an odd multiply (`(cx)_0 = x_0`, `(cx)_1 = x_1 ^ (c_1 & x_0)`). Modular addition by the exact carry-mask automaton (per bit a carry-mask bit; checked against brute force at n = 8 on 500 mask triples, max error 0) | exact family |
| Linear, general | The same with the multiply as its shift-and-add decomposition (one adder per set bit of `MUL`, the low known-zero bits of a shifted copy transparent), each adder under the automaton | trail model; over-optimistic for the attacker (section 4.3) |
| Rotational-XOR | Measured on the real code: for every rotation r in 1..31 and k = 1..4, the per-bit bias of `rot_r(M^k(x)) ^ M^k(rot_r(x))` over 2^20 states, the largest |z| of the 512 bits, and the count of exact rotational pairs; plus the word-level prologue `g(x) = (x ^ C) * MUL` alone: the most frequent value of `rot_r(g(x)) ^ g(rot_r(x))` over 2^20 inputs | measurement |
| The fold | The identities a chip would need to pay less than k x 128 for k applications, each tested on 2^20 random inputs, plus the algebraic argument (section 4.5) | measurement and argument |
Search: for each (model, day, k = 1..4) the weight bound W is probed upward (SAT means a trail of weight at most W
exists, UNSAT means none does in the model), then narrowed to the minimum. A k-application trail restricted to one
application is a valid 1-application trail, so every application is held to the proven k = 1 minimum of the same
model (the Matsui floor in the tables). Solver CaDiCaL 1.9.5 through python-sat 1.9. Every trail found of
measurable weight is measured on the real code before it counts: per application and as a chain, 2^20 to 2^28
samples (`attack-f2 verify-diff` / `verify-lin`), with the multiply-layer word transitions counted exactly over all
2^32 inputs (`verify-mults`). A trail that does not hold is blocked and the solver asked again at the same bound.
Linear trails whose correlation cancels inside one adder's hull are caught first by the exact signed sum over the
adder's carry masks.
What "reaches k applications" means here, two readings: (a) the shortcut reading, the one with a cost consequence:
a relation of probability 1 (weight 0) over k applications, which a chip could use to skip work; (b) the
distinguisher reading: a trail of weight under 64 over k applications, the usual practical line. For the gate both
are reported.
## 3. Harness
| Item | Path |
|---|---|
| Crate (ground truth: parameters, vectors, verification, RX, fold) | `tools/attack/f2-mixer/` (`Cargo.toml`, `src/main.rs`), `igneum-pow` by path, own `[workspace]` |
| SAT models and the search | `tools/attack/f2-mixer/model.py` (`selftest`, `search`, `show`) |
| Box queue runner, tables | `tools/attack/f2-mixer/run_jobs.sh`, `tools/attack/f2-mixer/summarise.py` |
| Build line (from the crate directory) | `IGNEUM_AGENT=attack-f2 bash /Users/joshm/Projects/igneum/tools/build-remote.sh --artefacts "target/release/attack-f2" --out <scratch> -- build --release`; binary on the box `/srv/builds/igneum-wt-attack/tools/attack/f2-mixer/target/release/attack-f2` (ELF x86-64, sha256 `150337ec...`, the third build; 12 s incremental) |
| Box scratch (states, trails, logs, venv) | `/srv/builds/igneum-wt-attack/target-attack-f2/` (`state/`, `logs/`, `params/`, `vectors/`, `venv/`). The brief's path `attack-f2/` was wiped within ten minutes by another lane's worktree-root rsync (`--delete` spares only `target-*`), so the scratch moved under a `target-` name, as F1 and F6 did |
| Run lines | `venv/bin/python3 model.py selftest --vectors vectors --params-dir params`; `bash run_jobs.sh jobs.txt 11` (each job `model.py search --kind diff|lin --family msb|general|low2 --params params/<day>.<variant>.txt --apps k --state state/<name>.json --budget <chunk> --per-app-min <k=1 floor> --verifier <binary>` under `flock -s /srv/builds/_locks/measure`, `nice -n 10 taskset -c 6-11,54-59`); `attack-f2 rx --day D --variant V --apps 4 --log2 20`; `attack-f2 rx-word --day D --log2 20`; `attack-f2 fold --day D --log2 20` |
| Logs | `logs/<model>-<day>-<variant>-k<k>.log` per search, `logs/rx.<day>.<variant>.log`, `logs/rx-word.<day>.<variant>.log`, `logs/fold.<day>.log`, `logs/summary.md` (the tables below), `logs/verify*.log` |
## 4. Results
### 4.1 The harness fires (known pass, known fail)
| Case | Expected | Got | Log |
|---|---|---|---|
| Selftest: evaluator against `attack-f2 vectors`, 3 days x 3 variants, 4 applications x 16 states each | all match | 9 of 9 files, 64 of 64 applications each | `selftest` output, `logs/selftest.log` |
| Selftest: linear add automaton against brute force, n = 8 | exact | 300 random triples and 200 shifted-copy triples, max error 0.00e+00 | same |
| Selftest: Lipmaa-Moriai against brute force, n = 8; both SAT encodings against their rules at n = 32 | exact | max error 0; 0 mismatches of 40 and 40 | same |
| Selftest: the multiply model's word cost against the sampled best transition (word 3, genesis day) | MSB exact; others within a few bits | MSB: weight 0, measured 2^-0 (exact); bit 30: model 2, sampled best 2^-1.00; bits 31+5: model 8, sampled best 2^-6.03; bit 0: model 10, sampled best 2^-8.97 | same |
| Known pass, 0 applications | the identity trail, weight 0 | trivial (input = output, no weights); not run as a job | |
| Known fail, `rot0` (every rotation 0), differential, k = 1, 2, 4 | a weight-0 trail (MSB-only differences stay MSB-only when nothing rotates) | weight 0 found at k = 1, 2, 4 (both families); measured probability 1 on the real code (`verified_chain -0.0`) | `state/diff-msb-2026-10-03-rot0-k{1,2,4}.json`, `state/diff-general-2026-10-03-rot0-k{1,2}.json` |
| Known fail, `rot0`, linear, k = 1, 2, 4 | a weight-0 trail (LSB masks) | weight 0 at k = 1, 2, 4; measured correlation 1 per application and as a chain | `state/lin-low2-2026-10-03-rot0-k{1,2,4}.json`, `state/lin-general-2026-10-03-rot0-k{1,2}.json` |
| Known fail, `nomul` (MUL 1, RC 0, rk 0: the bare double round), rotational-XOR, k = 1 | a large per-bit bias | max |z| 134.2 (r = 31) against 4.2 for the real mixer; word-level: the prologue is exactly rotational (2^20 of 2^20) against 3 of 2^20 | `logs/rx.2026-10-03.nomul.log`, `logs/rx-word.2026-10-03.nomul.log` |
| Known fail, `rot0`, rotational-XOR | bias | max |z| 32.3 at k = 1, 9.0 at k = 2 | `logs/rx.2026-10-03.rot0.log` |
| Known fail, `nomul`, differential k = 1 | the bare double round's best trail, below the real mixer's | weight 7 found (model), measured 2^-5.0 on the real code | `state/diff-general-2026-10-03-nomul-k1.json` |
### 4.2 Differential trails
[FILL: table from logs/summary.md]
### 4.3 Linear trails
[FILL]
### 4.4 Rotational-XOR
Per k and day, the largest |z| over all 31 rotations and 512 bits at 2^20 states (15,872 bit tests per k; the
noise ceiling of that many tests is about 4.3), and the count of exact rotational pairs.
| Day | k = 1 | k = 2 | k = 3 | k = 4 | Exact pairs | Log |
|---|---|---|---|---|---|---|
| 2026-10-03 | 4.22 (r 19) | 4.29 (r 30) | 4.22 (r 3) | 4.62 (r 27) | 0 | `logs/rx.2026-10-03.real.log` |
| 2026-10-04 | 4.35 (r 17) | 4.00 (r 19) | 4.24 (r 25) | 4.49 (r 28) | 0 | `logs/rx.2026-10-04.real.log` |
| 2027-03-01 | 3.96 (r 7) | 4.07 (r 2) | 4.04 (r 28) | 4.17 (r 19) | 0 | `logs/rx.2027-03-01.real.log` |
| 2026-10-03, bare double round (`nomul`) | 134.24 (r 31) | 4.37 | | | 0 | `logs/rx.2026-10-03.nomul.log` |
The word-level prologue `(x ^ C) * MUL`: over 2^20 inputs the most frequent value of `rot_r(g(x)) ^ g(rot_r(x))`
occurs at most 3 times for every word and every r on all three days (`logs/rx-word.<day>.real.log`, the
`rxw_worst` lines), against 2^20 of 2^20 without the multiply. The odd multiply by a random constant is not
rotational to any measurable degree, and one application already shows no per-bit bias. Rotational-XOR does not
reach 1 application.
### 4.5 The fold of the multiply layer
One application is `D o P_rk`, with `P_rk(s)_i = (s_i ^ (RC_i + rk)) * MUL_i` and `D` the double round (fixed per
day). Multiplication by an odd constant distributes over modular addition and over nothing else in `D` (XOR,
rotation); the XOR with a constant commutes with XOR and rotation and with nothing else (addition, multiply). A fold
across applications would need one of the identities below. Each was tested on 2^20 random inputs on every day
(`logs/fold.<day>.log`):
| Identity a chip would need | Holds on | Meaning |
|---|---|---|
| `(xa ^ Ca) * ma + (xb ^ Cb) * mb = ((xa ^ Ca) + (xb ^ Cb)) * ma` for the four column pairs (0,4), (1,5), (2,6), (3,7) | 0 of 1,048,576 for every pair on every day (`MUL` distinct in every pair) | the multiply does not fold into the first add of a quarter round; it would if a column pair drew the same `MUL` (probability 2^-31 per pair per day, the weak-day class of F4) |
| `(x ^ C) * m = (x * m) ^ (C * m)`, or `= (x * m) ^ C'` for any single `C'` | 0 of 1,048,576; the best single `C'` agrees on 33 of 1,048,576 (2^-15) | the constant cannot be moved past the multiply, so application j + 1's prologue cannot share application j's multiply |
| an XOR constant on one word commuting with the bare double round (so the next prologue's constant could be folded back) | 0 of 65,536 for every word | every word's value feeds an add inside the double round |
| the MSB passing the prologue and the add for free; the LSB passing the prologue | 1,048,576 of 1,048,576 each | the structural residue: the only free passages, both moved by the rotations (the family deaths in 4.2 and 4.3) |
So k applications cost k times one application, 128 hoisted operations each (16 multiplies, 32 adds, 32 XORs, 32
rotations with the constants hoisted); `chip-model-v3.md` 5.2's 9,360 per item stands. The trail weights of 4.2
and 4.3 growing with k is the quantitative side of the same fact: a composition that collapsed to one application's
shape would keep one application's trail weights.
## 5. Gate and verdict
[FILL]
## 6. Consequences per tier
[FILL]
## 7. What this does not do
- It does not bound the mixer from below: the general models are trail models (Markov for the multiply's
differential, piling-up for the linear), and the family models are exact only inside their families. The firm's
job (funding.md B5 rank 1) is the effort-bounded version of the same search with their tools.
- Three days, not a census: the ROT, MUL, RC classes over 2^24 days are F4's row. One cheap addition for F4 from
this harness: the MSB-family death at k = 2 (`model.py search --kind diff --family msb --apps 2`) runs in seconds
per day, and a day where it does not die is a weak day of the kind the gate is about.
- Differential and linear only, as the row says: no boomerang, no integral or cube property, no related-key (the
round keys are public constants).

View file

@ -85,6 +85,7 @@ public calendar), never a taped-out chip. Costs are in 32-bit adder-equivalents
|---|---|
| Crate | `tools/attack/f4-weakday/` (`Cargo.toml` with `igneum-pow = { path = "../../../igneum-pow" }` and an empty `[workspace]`; `src/main.rs`); `igneum-pow` untouched |
| Build | `cd tools/attack/f4-weakday && IGNEUM_AGENT=attack-f4 bash /Users/joshm/Projects/igneum/tools/build-remote.sh --artefacts "target/release/attack-f4" --out <scratch> -- build --release`; box binary `/srv/builds/igneum-wt-attack/tools/attack/f4-weakday/target/release/attack-f4`: sha256 `fda006d7...835f52` ran every census and firing (`build-1.log`); the rebuild `5eb081cf...7f0355` (`build-2.log`) removes one unused import and nothing else |
| Unit tests | `build-remote.sh --no-fetch -- test --release` on the box (`test-1.log`): 2 passed, 0 failed (`naf_weights`: 0, 1, 3, 7, 2^32 - 1, the alternating maximum 17, and the planted weight-3 constant; `genesis_day_draw_matches_memhard_md`: the string day `2026-10-03` draws `ROT 20 20 19 4 26 3 3 27`, MEMHARD.md section 1.1, through the same `with_shape` path the census uses) |
| Census (gate) | `flock -s /srv/builds/_locks/measure -c 'nice -n 10 taskset -c 16-21,64-69 attack-f4 census --from 20729 --count 16777216 --threads 12 --dedupe --out census-2p24.md'`; 4.2 s |
| Census (extended) | the same with `--count 268435456 --out census-2p28.md`; 68.6 s |
| Expectation tables | `attack-f4 expect --threads 12 --median 231` (every odd 32-bit constant: NAF weight and popcount, then the 16-fold convolution); 14.9 s |

View file

@ -0,0 +1,198 @@
# F6: the verifier's worst case over 10^5 class v4 programs
Attack-pass row F6 (`docs/plans/cryptanalysis.md` 4.2; the record `docs/analysis/attack-pass-2026-10.md`), the box search.
The O-1.14 laptop relay run is not in this record (main runs it separately). Written 7 October 2026.
## Target
| Item | Value |
|---|---|
| Commit | `924288d1` on branch `attack-pass` (`igneum-pow` is byte-identical at the worktree HEAD `8e36faf6`: `git diff --stat 924288d1..HEAD -- igneum-pow` is empty) |
| Class | `--program-class v4`: generator 4 on `V4_CLASS` = `mx8+sh256x27` (`LoadClass::MX8` plus `ShadowClass { instrs: 256, reps: 27 }`), no era bytes (the same draw `igneum-pow bench --program-class v4 --seed S` makes) |
| Dataset | day `2026-10-03`, `Shape::for_class_day(V4_CLASS, 0)`: cache 2^26 words (256 MiB), mixer x8, dataset 2^28 words, memory-hard |
| Work per hash | 64 base instructions x 8 iterations (16 loads) plus 256 shadow instructions x 27 passes x 8 iterations = 55,296 shadow instructions, 101,192 counted ops at the 1.83 convention |
| Gate | 10 ms per 32-lane warp, cold, on the half-core proxy (plan 1.4 item 6; spec 01 section 1.9 and 1.11; `algorithm.md` 3.3 and 5.5) |
| Programs | 10^5 deterministic string seeds `attack-f6/0` to `attack-f6/99999` through the class v4 chain draw with its acceptance rule (5.22 percent needed a second or third attempt, max attempt 3) |
The era draw is not in the search: `generator.rs` draws the shadow block from `NONLOAD_WEIGHTS` with no era perturbation
(no `perturb` path exists in the code at this commit), and the era parameters change only the load addressing, not the op
counts. Every drawn program has exactly 48 non-load base instructions and 256 shadow instructions, so the verifier's cost
differs between programs only through the family mix (the per-family cost on the CPU) and the data.
## Known-failed shape
A drawn program whose verifier warp exceeds 10 ms cold on the half-core proxy. The acceptance rule (spec 1.4.6) bounds the
miner's side (distinctness, bias, saturation); nothing bounds the verifier's cost per program, and the half-core headroom
of the average program is 1.8 ms (`algorithm.md` 5.5), so a family mix that costs the CPU interpreter more than the average
could cross the gate.
## Harness
`tools/attack/f6-verifier/` (its own cargo crate, `igneum-pow` as a path dependency, the same release profile as the CLI:
opt-level 3, LTO, one codegen unit). It builds the day's dataset ONCE (`DatasetSource::new_shape`, 0.58 s on the box) and
swaps programs under it: `Epoch { program, dataset }` is only the pair, and `verify::hash_warp(&program, base, &dataset)`
takes both, so one dataset serves every program. The naive path (`igneum-pow bench` per seed) refills the cache every
time (370 ms) and would take 10 hours per core.
| Command | What it does |
|---|---|
| `attack-f6 scan --program-class v4 --count N --start S --threads T --cold-reps R --flush swap --out F` | program i = seed `attack-f6/<S+i>`; one CSV line per program: generation time, R timed warps (each after a flush), their min, the op counts by family for the base and the shadow block |
| `attack-f6 time (--program-class v4 \| --class dr736) --seeds-file F --cold-reps R --steady W --flush sweep` | the deep re-time: R cold warps (each after a 256 MiB write sweep, what the cache fill does before `bench`'s "single cold run"), max, median, min, a steady average of W warps, and `GATE 10 ms PASS/FAIL` on the max |
| `attack-f6 micro --program-class v4` | the genesis program with its shadow block rewritten to one family at a time against the same program with no shadow: the per-family cost of a shadow instruction (ranking weights only) |
| `attack-f6 load --program-class v4 --seconds 0` | hashes class v4 warps on the calling core until killed: the SMT sibling's load for the half-core proxy, the same class the 3.3 proxy ran on both siblings |
| `rank.py --scan ... --weights ... --column ... --top 50 --out-prefix P` | the proxy ranking (sum over families of weight x (8 x base count + 216 x shadow count)), the distribution (min, median, p99, p99.9, max with the seed), the worst-N lists, a no-intercept regression of time on the family counts |
Build line (from the crate directory):
`IGNEUM_AGENT=attack-f6 bash /Users/joshm/Projects/igneum/tools/build-remote.sh --artefacts "target/release/attack-f6" --out <scratch> -- build --release`
(build 08:04 to 08:06 UTC, rustc 1.99.0, binary sha256 `89c35674...17f3f9`, on the box at
`/srv/builds/igneum-wt-attack/tools/attack/f6-verifier/target/release/attack-f6`).
Box scratch: `/srv/builds/igneum-wt-attack/target-attack-f6/` (not the `attack-f6/` the brief named: that path is an
untracked directory in the worktree mirror and `build-remote.sh`'s checkout step runs `git clean -fd` on the mirror
before every build of this worktree by any agent, which removed it once at 08:04 UTC; `target-*` is on the clean's keep
list, so this name survives). Logs there: `phase1.log`, `scan-0.csv`, `scan-50000.csv` (phase 1), `phase2a.log`,
`clock-2a.log`, `full-0.csv`, `phase2b.log`, `clock-2b.log`, `full-50000.csv`, `phase2c.log`, `clock-2c.log` (phase 2),
`smoke.log` (the functional check). Copies on the Mac under the session scratchpad `attack-f6/`.
Run lines:
| Phase | Hold | Cores | Line |
|---|---|---|---|
| 1, pre-screen (counts and a coarse time, no timing claim) | `flock -s` per 50,000-program chunk (2.6 min each) | `nice -n 10 taskset -c 42-47,90-95`, 12 threads | `attack-f6 scan --program-class v4 --count 50000 --start {0,50000} --threads 12 --cold-reps 2 --flush swap` |
| 2A, firings, micro, full pass first half | `flock -x` | `nice -n 19 taskset -c 40`; half-core: core 88 running `attack-f6 load` | `phase2a.sh` |
| 2B, full pass second half, worst 50 by proxy and worst 50 by coarse time re-timed on both proxies | `flock -x` | same | `phase2b.sh` |
| 2C, worst 1,000 by the full one-core pass on the half-core; worst 10 deep re-timed on both proxies | `flock -x` | same | `phase2c.sh` |
The clock of cores 40 and 88 (`scaling_cur_freq`) and the load average were read every 5 s during every exclusive hold
(`clock-2*.log`).
## The two firings (batch A, exclusive hold taken 08:15:20 UTC, core 40 at 3,799.9 MHz throughout, `clock-2a.log`)
`attack-f6 time ... --cold-reps 5 --steady 20 --flush sweep`, the gate applied to the max of the 5 cold warps
(`phase2a.log`). Lane-0 vectors equal the known ones (dr736 `e23d389f3eea0c83`, the Mac's).
| Case | Proxy | Cold max / median / min (ms) | Steady, avg of 20 | Known value (`algorithm.md` 3.3) | Verdict |
|---|---|---|---|---|---|
| known-fail, `--class dr736`, genesis seed | one core | 10.284 / 9.982 / 9.952 | 9.562 | 10.51 cold, 9.76 steady | FAIL (fired) |
| known-fail, `--class dr736`, genesis seed | half-core | 14.135 / 13.795 / 13.400 | 13.225 | 15.49 | FAIL (fired) |
| known-pass, `--program-class v4`, genesis seed | one core | 5.156 / 5.140 / 5.135 | 4.909 | 5.06 cold, 4.90 steady | PASS (fired) |
| known-pass, `--program-class v4`, genesis seed | half-core | 8.624 / 8.510 / 8.389 | 8.268 | 8.23 | PASS (fired) |
The harness reads the known-fail class over the gate and the known-pass class under it on both proxies, within 2 percent
of the 3.3 one-core numbers and within 9 percent on the half-core (the earlier half-core run loaded the sibling with
`igneum-pow bench` of the same class; this one hashes class v4 warps on it continuously).
## What one program can move (batch A, `micro`, core 40 solo)
The genesis class v4 program with no shadow block: 4.511 ms steady; with its drawn shadow: 4.920 ms. So the whole 55,296-
instruction shadow block costs 0.41 ms per warp on this core (7.4 us per 1,000 shadow instructions; `model.py` carries
7.0) and the base program with its 128 loads and 4,096 item derivations costs the other 4.5 ms. The verifier's cost is
92 percent dataset derivation (the x8 mixer, 8 dependent cache reads per item), which no drawn program changes: every
class v4 program has 16 loads and the acceptance rule's distinctness test keeps the items per warp near 4,096. The family
mix of the shadow can move at most a fraction of 0.41 ms. The per-family rewrite (the 256 shadow instructions all one
family) reads add 4.798, sub 4.703, xor 4.683, rotl 4.818, mad 4.805, shfl 5.042, rotr 5.160 ms per warp; the mul,
mulhi and or rows (0.97, 1.55, 1.13 ms) are degenerate (the registers collapse to 0 or all-ones, every lane then loads
the same item and the memory side vanishes) and are not ALU costs. Batch A's micro line printed its per-1,000 column
1,000x too small (ns per instruction); fixed in the source, the numbers above are the ms-per-warp column, which is right.
## Full one-core pass A (batch A, 50,000 programs, core 40 solo, one cold warp each after a program swap, `full-0.csv`)
617 s for 50,000 programs (12.3 ms each, 2.5 ms of it generation and acceptance). The per-family regression on the
50,000 timings (no intercept) gives 86 to 98 us per 1,000 executed instructions by family, R^2 0.001: the family mix
explains none of the program-to-program variation. Those regression weights (add 89.1, sub 87.7, mul 90.6, mulhi 98.4,
xor 89.6, or 86.0, rotl 89.4, rotr 86.3, mad 91.2, shfl 95.6) are the proxy weights used for the worst-50-by-proxy list.
| Pass A | n | min | median | p99 | p99.9 | max |
|---|---|---|---|---|---|---|
| all rows | 50,000 | 4.610 (`attack-f6/1278`) | 4.950 | 6.753 | 7.834 | 10.710 (`attack-f6/26705`) |
| rows outside the three disturbed blocks | 44,000 | 4.610 | 4.948 | 5.606 | 5.662 | 6.194 (`attack-f6/48484`) |
The rows over 6 ms sit in three 2,000-program blocks (26,000 to 27,999: 337 rows; 36,000 to 37,999: 300; 38,000 to
39,999: 294) and nowhere else (0 in each of the other 22 blocks); the neighbours of the 10.71 ms seed, unrelated programs,
all read 7.6 to 8.5 ms. `clock-2a.log` shows core 88 (the idle sibling of a solo run) at 3.8 GHz for stretches in those
minutes (08:21:25, 08:21:45, 08:22:05 to 08:22:25 UTC) and core 40 dipping to 3.68 GHz at 08:21:00: a foreign process
(the box's hands run outside the measure lock) sat on the sibling, which is the half-core condition, and those rows read
half-core numbers. They are not program properties and not numbers; the three blocks are re-scanned in batch B, and from
batch B on a per-second sampler logs every process whose last CPU was 40 or 88 (`clock-2b.log`, `clock-2c.log`) so a
disturbed row can be named.
## Batches B and C: queued, starved of the exclusive hold (state at 09:46 UTC)
Batch B (the second 50,000 of the full one-core pass, the re-scan of the three disturbed blocks, the worst 50 by proxy
and the worst 50 by phase-1 coarse time re-timed 10 cold reps each on both proxies) was queued with `flock -x -w 7200`
at 08:33:56 UTC and had not taken the file by 09:46 UTC. Linux `flock` gives a pending exclusive waiter no priority over
new shared takers; eight lanes re-take the file in chunks (17 to 38 shared holders at every reading, F2 spawning many
short ones, one F9 hold 33 minutes old at 09:06, over the 30-minute cap), so the file is never free. Left on the box:
`run2b-retry.sh` re-queues batch B up to four more times (2 h each); `run2c-auto.sh` waits for `BATCH B DONE`, builds the
batch C lists on the box (`mklists.py`: the worst 1,000 and worst 10 by the full one-core pass, pass A's three disturbed
blocks replaced by their re-scan) and queues batch C (the worst 1,000 on the half-core at 2 cold reps each, the worst 10
at 20 cold reps on both proxies). A marker sits beside the lock (`/srv/builds/_locks/measure.wanted-by-attack-f6`). When
they land, `timeparse.py --log phase2b.log --clock clock-2b.log` (and `2c`) prints the per-seed tables with the sampler's
foreign-process column, and this record is completed.
## Numbers so far, the gate, the verdict
| Quantity | Value | Where |
|---|---|---|
| Programs drawn and counted (phase 1) | 100,000 | `scan-0.csv`, `scan-50000.csv` |
| Programs timed cold on core 40 alone (one-core proxy) | 50,000 (44,000 clean, 6,000 in disturbed blocks awaiting the re-scan) | `full-0.csv` |
| One-core cold, clean rows: min / median / p99 / p99.9 / max | 4.610 / 4.948 / 5.606 / 5.662 / 6.194 ms (`attack-f6/48484`), single warps, not yet re-timed | `full-0.csv` |
| Genesis class v4, half-core, max of 5 cold | 8.624 ms | `phase2a.log` |
| Half-core over one-core, genesis class v4 | 1.67x (8.624 / 5.156) | `phase2a.log` |
| Programs timed on the half-core proxy | 1 (the genesis seed) | `phase2a.log` |
| Core 40 clock during every exclusive timing | 3,799.9 MHz (dips to 3,680 MHz only in the disturbed minutes) | `clock-2a.log` |
Gate line: the worst program under 10 ms cold on the half-core proxy. Not yet measured: no drawn program other than the
genesis seed has a half-core number, and the one-core worst (6.194 ms, a single warp with no sampler running) has not been
re-timed. Carried to the half-core at the genesis ratio it would read 6.19 x 1.67 = 10.3 ms, over the gate; carried at the
additive half-core cost of the genesis program (8.624 - 5.156 = 3.47 ms) it would read 9.66 ms, under it by 0.34 ms. The
2.5x bracket of `algorithm.md` 5.5 is between. Whether `attack-f6/48484` (and the other clean rows over 5.6 ms: 1 in 100
of the pass) is a program property or a short disturbance is what batch C's 20-rep re-time with the sampler decides;
the record says FINDING if its half-core cold max reads 10 ms or more.
Verdict: INCOMPLETE. 100,000 programs drawn and ranked, 50,000 timed on the one-core proxy, 0 of the worst re-timed on
the half-core proxy; the two firings fired; the box search's second half and the half-core re-times are queued and
starved of the exclusive hold.
## The ladder ceiling implied so far
`algorithm.md` 5.5 and `model.py --section ladder` set the ceiling from the half-core headroom at 12.1 us per 1,000 shadow
instructions, N = 101,192 + instructions x 1.83.
| Worst program on the half-core | Headroom to 10 ms | Shadow instructions it buys | Ceiling N (counted ops) |
|---|---|---|---|
| 8.23 ms (3.3's average, the published figure) | 1.77 ms | 146,000 | about 370,000 |
| 8.62 ms (genesis seed, this run's max of 5) | 1.38 ms | 114,000 | about 310,000 |
| 9.66 ms (48484 if the additive carry holds) | 0.34 ms | 28,000 | about 152,000 |
| 10.3 ms (48484 if the 1.67x carry holds) | none | 0 | below today's 101,192: the floor rung 100,000 is the ceiling |
The proposed genesis ladder {100,000; 130,000; 200,000; 330,000; 650,000; 1,000,000} already exceeds the 310,000 ceiling
at its fourth rung on the genesis program alone; on the worst program the ceiling could be the floor. This is the
ladder's own open question (plan 1.1, "ceiling set by the verifier"), and the number that sets it is the half-core
worst case still queued.
## Consequences per user tier (at the numbers measured so far; the model's table of 5.5 at 10 ms beside them)
| | At 8.62 ms (genesis, half-core max) | At 9.66 ms (worst, additive carry, unverified) | At 10.3 ms (worst, 1.67x carry, unverified) | Model at 10 ms |
|---|---|---|---|---|
| A node on a 2019-class laptop core at 1 bps | 0.9 percent of one core | 1.0 | 1.0 | 1 |
| At 10 bps (the Devnet 2 experiment) | 8.6 percent of one core | 9.7 | 10.3 | 10 |
| IBD over the 108,000-header pruning window, one core | 15.5 min | 17.4 | 18.5 | 18 |
| Header flood: invalid headers per second that saturate one core | 116 | 104 | 97 | 100 |
| A pool core verifying shares, shares per second per core | 116 | 104 | 97 | 100 |
What each tier does with it: a home miner (8, 12, 16, 24 or 32 GB card, any vendor, any OS) runs a node that spends about
1 percent of one CPU core on the hash at 1 bps whatever the drawn program, and 9 to 10 percent at 10 bps; the card is not
involved. A rig is the same per node. A pool verifying shares at 100 per second per core needs one core per 100 shares
per second at the worst program, 116 at the average: a pool that sized its share verification at the average loses 14
percent of its per-core headroom on the worst program, so pools size at 97 shares per second per core (the 10 ms figure)
and never at the average. A node under header flood holds at about 100 invalid headers per second per core on any
program, the M15 figure. The 2019-class core itself is still the half-core proxy until the O-1.14 laptop run lands
(main's lane).
What this lane does about it: completes batches B and C when the hold comes (automatic, on the box); if the worst
program reads 10 ms or more on the half-core proxy, the finding goes to main with the seed, the reproduction line
`igneum-pow bench --program-class v4 --seed <seed> --day 2026-10-03 --warps 50` on core 40 and the half-core, and the
proposed fix: an acceptance-rule bound on verifier cost (a per-program cost model over the family counts checked at
draw time, a redraw when it exceeds the bound, exactly as rule (c) redraws on bias) and the ladder's ceiling set from
the measured worst, not the average; `igneum-pow` is not edited by this lane.

View file

@ -0,0 +1,259 @@
# F8. Uniformity censuses of the class v4 derivation
Attack-pass row F8 (`docs/plans/cryptanalysis.md` section 4.2; the pass record `docs/analysis/attack-pass-2026-10.md`).
Run 7 October 2026, 08:13 to 09:3x UTC (09:13 to 10:3x UK) on igneum-build-1. Verdict: **FINDING** (AP-F8-1 below).
The line-index census is a PASS at its full sample size; the cross-hash item histogram is not uniform, and the cause
is in the base program, inside the acceptance rule's blind spot.
## 1. Target
| Item | Value |
|---|---|
| Commit | `igneum-pow` at 924288d1 (`attack-pass`); the box built HEAD b2a411d1, whose `igneum-pow` is byte-identical (`git diff --stat 924288d1 HEAD -- igneum-pow` is empty) |
| Class | `--program-class v4`: `V4_CLASS` = `mx8+sh256x27`, generator 4, mixer x8, the era layout drawn inside the class, the shadow block of 256 instructions x 27 reps |
| Line index | `proto-metal/MEMHARD.md` section 1.6: `a = s[0] AND 0x003fffff`, 4,194,304 lines of 64 B, 8 dependent reads per item (`memhard.rs` `derive_items_mask`, `cache.line_const(s[0])`) |
| Item index | `verify.rs` `load_index`: `y = rotl(x * M, R)`, the site's window `(y & (MASK >> k)) \| off`, then `Layout::split` removes the four interleave bits; 2^24 items at the 2^28-word dataset |
| Reads per hash | 128 loads (16 sites x 8 iterations), so up to 1,024 cache lines per hash and 32,768 per warp. The spec's analytic bound of 832 lines per hash (`docs/spec/01-lottery-hash.md` line 347, 104 loads x 8) predates generator 2's fixed 16 load slots; the current bound is 128 x 8 = 1,024 |
| Prior figures | `chip-model-v3.md` section 1: "median 128.00 distinct" items per hash (the 20,000-program census); `weak-program-census-2026-10-03.md` line 291: 127.7 distinct addresses per hash under the proposed generator |
| Day | the devnet pack's day, `bind::day_bytes(20730)` (2026-10-04), day 0 of the growth schedule: a 2^26-word cache, a 2^28-word dataset. Census 1 uses days 20730 to 20745 |
| Programs | p1 = the devnet epoch-0 derivation (epoch seed and era seed both the genesis hash `edc4fa84...fb07`, program id `c120d7963abdcd96`, attempt 0); p2 and p3 = chain-shaped seeds from tag strings (section 4), attempts 1 and 0 |
## 2. Method and harness
Harness: `tools/attack/f8-uniform/` (crate `attack-f8`, a path dependency on `igneum-pow`, nothing in the library
modified). Built on the box through `tools/build-remote.sh`: sha256 `590668...f913` for the firings of 2.1,
`ef8042...11b2` for sections 3, 4.1 and the first item runs (flat null, `log/c-*`, `log/d-*`), `890955...fbd3` for the
window-model runs and the seed census (`log/e-*`). The committed source carries one later label fix (the
"uniform-on-window" entropy reference in the per-site line is 16 - k_off bits; the 09:04 UTC logs print 16 - 2 k_off). Box scratch `/srv/builds/igneum-wt-attack/target-attack-f8/`
(the name `target-*` is what the box's checkout clean spared at the time; the fix at b92a5fd4 now also spares
`attack-*`). Every run: `nice -n 10 taskset -c 22-27,70-75`, 12 threads, under `flock -s /srv/builds/_locks/measure`
in chunks under 3 minutes each (the longest, phase D, under 25 minutes).
Two mirrors, each trusted only while it agrees with the library bit for bit:
| Mirror | What it records | Agreement check | Result |
|---|---|---|---|
| `derive_traced`: `memhard::derive_items_mask` instruction for instruction (`mixer`, `round_key_mult`, `cache.line` from the library), the line index of every round kept | 8 line indices per item | every item of every day also derived by the library's `derive_items` and compared on all 16 words | 0 mismatches on 268,435,456 items (section 3) and on 16,777,216 items per table build (section 4) |
| `Mirror::warp`: `verify::interpret_warp_init` for the class v4 op set, dataset words from a table of the day's 2^24 items, the item index and the source register of every load kept | 128 item indices per lane, the source value's saturation per position | the 32 hashes of warp 0 to 63 and of every 997th warp compared with `Epoch::hash_warp` | 0 mismatches on 95 warps per program (section 4) |
Three censuses:
1. `lines`: all 2^24 items of each of 16 consecutive day keys (2^28 item derivations, 2^31 line reads), the full 2^22-line
histogram per round and pooled, the 2^16-bucket histogram (64 lines, one chained segment per bucket), a uniform
SplitMix64 control of the same size.
2. `warps`: 10^6 nonces (31,250 warps) of each of three programs: distinct lines and items per hash and per warp, the
cross-hash item histogram, per-position diagnostics, an attribution pass from the hottest items back to the load
positions that read them.
3. `warps` at 2^26 nonces on p1: the one-epoch cross-hash item histogram at 512 expected reads per item.
The tests, defined before the runs:
- **6-sigma test**: the largest (and smallest) bucket of a histogram within 6 sigma of its expectation, sigma =
sqrt(expectation). The gate's bucket is the 64-line segment for lines and the 64-item bucket for items. The
full-resolution histograms are reported beside a uniform control of the same size, because at a small mean the
Poisson tail puts the maximum of 4 million bins above 6 sigma by chance (control at mean 8: +6.72 sigma; at mean
32: +5.83; at mean 512: +5.61).
- **Hot-set test** (F8's definition, written for F9's reuse): sort items by read count; S_f = the share of all reads
on the top-f fraction of items, for f in {0.1%, 0.5%, 1%}; E_f = the same share on a control of the same size drawn
from the design's own null (flat uniform for lines; the window-weighted null for items, section 4.2); the excess
X_f = S_f - E_f. **A hot set exists at f when X_f >= f**: after the chance excess is removed, the top f of items
capture at least one extra proportional share, which is what an on-die copy of f of the items would have to win to
matter. X_f / f is printed as the gain in proportional shares. The acceptance-style form of the same metric (for
rule (c)'s 2,048 evaluations): per load position, the largest count of one masked address, and the count of
saturated (0 or 2^32 - 1) source values.
### 2.1 The harness fires (known-fail and known-pass)
| Plant | What it does | 6-sigma test | Hot-set test | Log |
|---|---|---|---|---|
| `quarter-lines` | line index masked to a quarter of its range | buckets64 largest +75.97 sigma (2,231 at mean 512), smallest -22.63: FLAGGED | X_1% = +3.54% (S 5.60% vs control 2.06%), X/f = 3.5 at every f: FLAGGED | `log/a1-lines-quarter.log` |
| `half-lines` | line index masked to a half | buckets64 largest +29.26 sigma: FLAGGED | X_1% = +1.25%, X/f = 1.25: FLAGGED | `log/a2-lines-half.log` |
| `const-item` | one constant item at the first load site (1/16 of reads) | items buckets64 largest +92,682 sigma: FLAGGED | X_0.1% = +6.38%, X/f = 63.8: FLAGGED | `log/a4-warps-const-item.log` |
| none, 2^22 items, one day | the real derivation at a small size | buckets64 largest +4.42 sigma, smallest -4.51: within 6 sigma (control +4.33) | X_f = -0.0004%, -0.0006%, -0.0010%: clear | `log/a3-lines-pass-small.log` |
Both tests fire on every plant and neither fires on the real line derivation. Log paths are under
`/srv/builds/igneum-wt-attack/target-attack-f8/`.
## 3. Census 1: the line index over 2^28 derivations (PASS)
Sample reached: 16 days x 2^24 items = 268,435,456 item derivations, 2,147,483,648 line reads into 4,194,304 lines
(512 expected per line, 32,768 per 64-line segment). Mirror mismatches against `derive_items`: 0 of 268,435,456.
Log: `log/b-lines-16days.log`; histograms `out/lines-d20730-n16-i24-none-buckets64.txt` (65,536 rows) and
`out/lines-d20730-n16-i24-none-full.u32le` (4,194,304 x u32).
| Histogram | Bins | Expected | Largest | Sigma | Smallest | Sigma | chi2/dof | Top 1% share |
|---|---|---|---|---|---|---|---|---|
| Pooled, 64-line buckets (the gate) | 65,536 | 32,768 | 33,645 | +4.84 | 31,998 | -4.25 | 1.00226 | 1.01427% |
| Pooled, full 2^22 lines | 4,194,304 | 512 | 639 | +5.61 | 402 | -4.86 | 0.99937 | 1.11979% |
| Control, 64-line buckets | 65,536 | 32,768 | 33,524 | +4.18 | 31,960 | -4.46 | 0.99930 | 1.01426% |
| Control, full 2^22 lines | 4,194,304 | 512 | 639 | +5.61 | 408 | -4.60 | 0.99952 | 1.11956% |
| Per round 0 to 7, full, pooled (64 per line) | 4,194,304 | 64 | 107 to 113 | +5.38 to +6.12 | 26 to 29 | -4.75 to -4.38 | 0.99855 to 1.00062 | 1.3483% to 1.3488% |
| One day (20730), 64-line buckets | 65,536 | 2,048 | 2,291 | +5.37 | 1,859 | -4.18 | 0.99985 | 1.05826% |
| One day, control, 64-line buckets | 65,536 | 2,048 | 2,250 | +4.46 | 1,874 | -3.84 | 1.00377 | 1.05901% |
Per day, the gate bucket's largest value ran +4.00 to +5.37 sigma on all 16 days (control +4.46), every day within
6 sigma. Hot-set test on the pooled lines: X_0.1% = -0.00002%, X_0.5% = +0.00010%, X_1% = +0.00023% (X/f under
0.0003): clear. Round 0, whose input is the sequential item index through the init `t * MUL[i] + RC[i]` and eight
mixer applications, is as flat as rounds 1 to 7 (chi2/dof 0.99926; its +5.50 sigma maximum is below the control's
+5.61 at the pooled size). Round 5's +6.12 sigma at mean 64 is one bin of 4 million at a Poisson tail where the
control at mean 8 reached +6.72; its chi2/dof is 0.99855.
Gate line: the largest bucket is within 6 sigma of uniform (+4.84 on the 64-line buckets, +5.61 on the full 2^22
lines, both at or below the control), chi2/dof 0.99937, no hot set. **PASS at 2^28 derivations.**
## 4. Census 2 and 3: distinct lines per hash and warp, and the cross-hash item histogram
Setup per program: the day's 16,777,216 items derived once into a table with their 8 lines (6 to 9 s on 12 threads,
0 mismatches against `derive_items` on every item), then the warps interpreted from the table at 2.7 to 3.2 ms per
warp per thread. Logs: `log/c-warps-p{1,2,3}-1e6.log` (first run, flat null) and `log/e-warps-p{1,2,3}-1e6.log`
(windowed null, section 4.2); distributions `out/warps-<program>-d20730-n1000000-none-distinct.txt`, item histograms
`...-items.u32le` (16,777,216 x u32), per-position tables `...-positions.txt`.
### 4.1 Distinct lines and items per hash and per warp (10^6 nonces each)
| Program | Epoch seed / era seed | Lines per hash min / p1 / median / max / mean | Items per hash min / median / mean | Lines per warp min / median / max / mean | Items per warp min / median / mean |
|---|---|---|---|---|---|
| p1 `c120d7963abdcd96` (devnet epoch 0) | genesis / genesis | 1,008 / 1,023 / 1,024 / 1,024 / 1,023.867 | 126 / 128 / 127.9989 | 32,579 / 32,636 / 32,680 / 32,635.84 | 4,090 / 4,096 / 4,095.41 |
| p2 `82f0696f823e9c65` | `59cef1aa...bfdfa` / `9cba001f...1f69` | 1,014 / 1,023 / 1,024 / 1,024 / 1,023.871 | 127 / 128 / 127.9995 | 32,580 / 32,637 / 32,684 / 32,636.08 | 4,091 / 4,096 / 4,095.48 |
| p3 `e282eed7d47e425e` | `c54e2ddd...c95d` / `1b04f607...b58a` | 999 / 1,016 / 1,024 / 1,024 / 1,023.600 | 125 / 128 / 127.9656 | 32,276 / 32,481 / 32,601 / 32,480.32 | 4,050 / 4,076 / 4,075.85 |
| Uniform expectation | | 1,023.875 of 1,024 | 127.9995 of 128 | 32,640.3 of 32,768 | 4,095.50 of 4,096 |
Per hash, every program reads its 128 items and 1,024 lines as the design intends (p1 and p2 at the uniform
expectation; p3 a shade under, 127.97 items, which is the same site-15 effect as the finding below: the saturated
site repeats an item inside a hash 3 times in 100). Per warp, 32 lanes read 32,636 distinct lines of 2^22, a 2 MiB
working set of cache lines and 256 KiB of dataset items, within 0.01% of uniform on p1 and p2.
### 4.2 The cross-hash item histogram and the window layer
The era layout's window layer (`docs/plans/era-layout.md` section 1.4, layer 8) makes each load site read an
aligned half or quarter of the dataset with probability 2/3. The per-site item distribution is therefore not flat by
design (the diagnostic's "worst bit" reads P(1) = 1.0000 or 0.0000 at every windowed site: the fixed top bits), and
the summed item histogram has density steps between quarters. For p1 the 16 windows (site:shrink:offset
`7:2:1 8:1:1 9:1:1 10:1:1 11:0:0 13:1:1 29:0:0 30:2:2 31:1:1 44:1:1 46:2:0 47:0:0 52:0:0 56:0:0 58:2:0 63:1:1`) give
expected reads per item by quarter of 3.25 : 2.25 : 5.75 : 4.75 in sixteenths of the flat value. Against a flat
uniform the 64-item buckets of p2 (a program without the finding) read +10.03 and -9.06 sigma, which is the window
layer and not a flaw. The item tests are therefore judged against the **window-weighted null**: the expected count of
every item from the program's 16 windows, and a control that draws each read from a uniformly chosen site's window.
A chip gains nothing from the window steps: the union of the windows is the whole dataset every hour (era-layout.md
section 7), the floor window is 2^26 words (256 MiB), and which quarter is dense changes with the program.
#### The window model (reproducible by the firms)
For load site s with window draw `(k_s, o_s)` at the 2^28-word dataset: `k = min(k_s, 28 - 26)`, the word window is
`[o_s << (28 - k), (o_s + 1) << (28 - k))`; the item window is `[o_s << (24 - k), (o_s + 1) << (24 - k))` of
`2^(24 - k)` items (the four interleave positions all lie below bit 16, so the top bits of the word index are the top
bits of the item index). The expected reads per item is `E[t] = sum over sites s with t in window_s of N x 8 / 2^(24 - k_s)`
for N nonces (8 iterations per site), a density constant on each quarter of the item space. The windowed control draws
each of the N x 128 reads as (site = read index mod 16, item uniform on that site's window). Both controls are drawn from
SplitMix64 with a fixed seed. The tests on items are run against E[t] (chi-square, sigma of the largest and smallest
64-item bucket) and against the windowed control (the top-f shares); the flat uniform numbers are kept beside them as
what an auditor sees first.
#### Results, 10^6 nonces per program, 128,000,000 reads (`log/e-warps-p{1,2,3}-1e6.log`)
| Program | Windows (k_off:offset per site) | Quarter densities (reads per item) | Buckets64 largest sigma, windowed (control) | chi2/dof windowed (control) | Top 0.1% share: real / window control / flat control | Ratio to window control at 0.1% (gate 1.2x) | Ratio to flat control | Hot set (X_f >= f) |
|---|---|---|---|---|---|---|---|---|
| p1 devnet epoch 0 | 2:1 1:1 1:1 1:1 0 1:1 0 2:2 1:1 1:1 2:0 0 0 0 2:0 1:1 | 6.20 / 4.29 / 10.97 / 9.06 | +45.77 (+4.95) | 1.2336 (0.9981) | 0.5458% / 0.2891% / 0.2429% | 1.888x BEYOND | 2.247x | yes at 0.1% (X/f 2.57) and 0.5% (1.20); not at 1% (0.46) |
| p2 | 2:0 1:0 0 0 2:3 2:2 1:0 0 2:3 0 0 0 0 1:1 2:0 0 | 9.54 / 5.72 / 6.68 / 8.58 | +4.59 (+4.64) | 1.0061 (0.9986) | 0.2716% / 0.2639% / 0.2429% | 1.029x within | 1.118x | no (X/f 0.08, 0.05, 0.05) |
| p3 | 1:0 0 0 2:2 0 0 1:0 1:0 1:0 2:2 2:2 1:1 0 0 0 0 | 7.63 / 7.63 / 10.49 / 4.77 | +12,245.66 (+5.06) | 907.67 (0.9993) | 4.5954% / 0.2792% / 0.2433% | 16.46x BEYOND | 18.92x | yes at every f (X/f 43.2, 9.9, 5.0) |
p2 is what the class is designed to be: against the window model its largest bucket is +4.59 sigma (the control +4.64),
chi2/dof 1.006, the top 0.1% of items hold 1.029x their window-model share, and the flat-control ratio of 1.118x is
the window layer. p1 and p3 are the finding (section 5). The one-epoch histogram at 2^26 nonces (8,589,934,592 reads,
512 per item, `log/d-warps-p1-2e26.log`, flat null): p1's top 0.1% hold 0.5199% of reads against 0.1152% flat
control (X/f 4.05), the top 1% 2.4946% against 1.1198% (X/f 1.37), item 0xca5b92 78,479 reads at a mean of 512, and
site 15 feeds 6.37% of its reads into the top 0.1% in each of the 8 iterations; the excess grows with N as the
control's chance excess shrinks, which is the signature of a structural skew. Distinct lines and items per hash and
per warp at 2^26 nonces: 1,023.866 / 127.9989 / 32,635.6 / 4,095.41, unchanged from 10^6.
## 5. AP-F8-1: a saturated load source makes a cross-hash hot set (FINDING)
**What**: an accepted class v4 program can read one load site from a register whose last writes after its last
injecting write are `or` (and, mildly, `mul`), so the site's address has fewer than 32 bits of entropy across nonces
and the same items are read by many hashes. The per-hash figures (128 distinct items, 1,024 lines) stay intact; the
cross-hash item histogram does not. It is not the window layer (p2 shows the window layer alone is clean against its
model) and not the shadow block (iteration 0's load, which runs before any shadow block, is as hot as iterations 1 to
7: p1 6.372% vs 6.371% to 6.378%; p3 71.9% vs 72.4% to 72.6%).
**Where it hides from rule (c)** (`accept.rs`, 2,048 evaluations of the base program): the tests are constant bits
in FINAL register values, one address in ALL 32 lanes of a unit, saturated FINAL values, output-bit bias, and distinct
addresses WITHIN a hash. A site whose address is concentrated across hashes but refreshed before the end of the
iteration passes every one. Rule (a) accepts any write, `or` included, as the refresh between two loads from the same
register (`check_stale_loads`); `Op::injects` (add, sub, xor, mad, shfl, load) is only used by rule (b), once per
register per program.
**The index derivation at the hot site** (the "writers back to the last injecting one" lines of `log/e-warps-p*.log`):
| Program | Hot site | Source | Writes after the last injecting write | Site's reads into the top 0.1% of items (flat expectation) | Index entropy, 256-item buckets (uniform on window) | Saturated source (x = 0 or 2^32 - 1) | Most repeated address at one position in 2,048 evaluations (uniform: 1 to 2) |
|---|---|---|---|---|---|---|---|
| p3 | site 15, instr 62 | r5 | `load@17` then `or@19`, `or@30` | 72.43% (0.10%) | 13.411 bits (16) | 1.368% | 44 of 2,048; 32 saturated |
| p1 | site 15, instr 63 | r6 | `add@51` then `rotl@53`, `or@61` | 6.93% (0.11%) | 14.985 bits (15) | 0.005% | 2 of 2,048; 0 saturated |
| p2 (clean) | every site | | injecting, or bijective (`rotl`), or `mul`/`mulhi` | 0.41% to 0.97% (0.20%; the window densities) | 13.999 / 14.997 / 15.994 bits (14 / 15 / 16) | 0.000% | 2 of 2,048; 0 saturated |
In p3 two `or`s on r5 after its load make the source 1 with probability 7/8 per bit; x = 2^32 - 1 in 1.37% of
evaluations and the images of the near-saturated values under the stride (`y = rotl(x * M, R)`, 256 x-values per
item) pile onto a few items: 0xffdf69 takes 213,913 of the site's 8,000,000 reads (2.67%), the top 0.1% of items
72.4%, and 4.6% of ALL reads of the hash land on 0.1% of the items. In p1 one `or` after `rotl(add)` gives 3/4 per bit
on the ORed positions: no saturation to speak of (0.005%), but 6.9% of the site's reads on 0.11% of the items (the
hot items share the low 20 bits `5b92`: 0xca5b92, 0x8a5b92, 0xaa5b92, 0xba5b92, 0x825b92, 0xe65b92, 0x985b92), a
2.6x proportional excess at f = 0.1%. p2's `mul` sites (10, 13: `mul` after a load or a shuffle) read 0.65% and 0.70%
into the top 0.1% against 0.41% and 0.48% for their window class (an even multiplier zeroes low bits; the hot items
0xd6a680, 0xe44400, 0xd25600 end in zero bits), a mild effect that the window-model ratio (1.029x) absorbs.
**How common** (the seed census, 64 chain-shaped programs p4 to p67, 262,144 nonces each, `log/e-seed-census-4-67.log`,
`out/seed-census-d20730-n262144-p4-67.txt`): CENSUS-LINE
**Reproduction**: `attack-f8 warps --program 3 --nonces 1000000 --diag 1` (or `--program 1`); the acceptance-style
numbers come from the same run's "acceptance-style" line. The program is `Epoch::chain_program(epoch_seed, Some(era),
ProgramClass::V4, label)` with the seeds of section 4.1.
**Proposed fix** (not applied; `igneum-pow` untouched, the Counter ASIC lane re-gates on `ca3-v4-uniform` with this
harness):
1. Rule (a'), static: between the last injecting write of a load's source register and the load (cyclically), no
`or` and no `mul` writes that register; `rotl`, `rotr` and `mulhi` may (bijective, or measured flat: p2 site 15
reads `mulhi` after `add` at 1.00x). This rejects p1 and p3 at draw time and costs nothing at run time. Programs
rejected are redrawn as today (`MAX_ATTEMPTS` 32); the census gives the rejection rate.
2. Rule (c'), dynamic, the same 2,048 evaluations: no load site reads a saturated source (0 or 2^32 - 1) in more
than 2 evaluations, and no address repeats more than 4 times at one position (uniform expectation 1 to 2; p3 shows
44 and 32). This catches the strong class only; p1's class needs about 2^16 evaluations to show at a site (65,536
nonces: largest item count 67 at a mean of 0.5), so (a') is the rule that closes it and (c') is the check that
fails loudly if (a') is ever loosened.
3. Packs re-cut for the seeds the new rule rejects (the devnet epoch-0 program p1 is one of them: its site 15 is
`or@61`), with the gate pack ids re-pinned; the chain's own epochs redraw automatically.
**Reuse for F9**: the hot-set metric (section 2) on the per-program item histogram at 2^18 nonces, and the
acceptance-style pair (most repeated address at a position, saturated sources at a position) at 2,048 evaluations,
are both emitted by `warps --programs a..b`; a header-grinding search that steers a program to a hot set would show as
ratio-to-window-model above 1.2x at f = 0.1%.
## 6. Consequences per tier
| Number | What it means | Per tier |
|---|---|---|
| Line index uniform at 2^28 derivations (largest segment +4.84 sigma, chi2/dof 0.99937) | the 256 MiB cache has no hot segment: a chip or a card cannot serve the 8 dependent reads of an item from a cache smaller than the whole 256 MiB (the floor window of era-layout.md) | no change for any card; the verifier's cache stays 256 MiB in RAM on every node |
| Distinct lines per hash 1,023.87 of 1,024, items 127.999 of 128 (p1, p2); per warp 32,636 lines, 4,095 items | the per-hash working set is 64 KiB of cache lines and 8 KiB of items, per warp 2 MiB of lines and 256 KiB of items; the item-derivation chip's "128 items per hash" input (`chip-model-v3.md`) stands | the 8 GB card and up: unchanged; the recompute chip pays 128 derivations per hash, as modelled |
| p3-class programs: 4.6% of all dataset reads on 0.1% of items (1 MiB of a 1 GiB dataset); p1-class: 0.59% on 0.11% | a stored-dataset chip with 1 MiB of on-die SRAM serves 4.6% of its reads without touching DRAM on such an epoch; a GPU's L2 (96 MiB on the 5090, 64 MB Infinity Cache on the 9070 XT, vendor figures) holds the same 1 MiB, so both sides gain the same 4.6% of reads and the chip's edge from it is about 0 (the per-joule edge of `evidence.md` row 17 is a DRAM-read figure; a 4.6% read saving on both sides moves it by under 5% on such epochs). The recompute chip (f = 0, SRAM cache) caches the derived hot items and skips up to 4.6% of its 128 derivations per hash on such epochs, a 4.8% rate gain on those epochs only | home cards 8 to 32 GB, rigs, pools: no action; a few percent of epochs run a few percent faster for everyone with an L2. The verifier: `MemhardCpu::fetch` dedupes within a fetch only, so no change. The chip model: the headline 2.1x at k = 1 moves by under 5% on affected epochs and 0 on others; the fix below returns it to 0 everywhere |
| The acceptance rule's blind spot (cross-hash concentration at one site) | a program class property, not a day or era property: the same seed is hot on every day and under every era, so a chip or a pool that selects epochs cannot gain more than the epoch's own 4.6%; but the public claim "the item map is uniform per program up to the window layer" is false for the affected fraction of seeds until rule (a') lands | the fix is a generator rule plus packs re-cut: a class change under the 95% signalling rule if it lands after the flip, a plain re-cut if it lands in the class v4 cut itself (the lane's call) |
## 7. Gate line and verdict
| Gate (plan 4.2 F8, the same as 1.4 (4)) | Result | Status |
|---|---|---|
| The largest bucket within 6 sigma of uniform on the stated sample sizes (line index, 2^28 derivations) | +4.84 sigma on 64-line segments, +5.61 on 2^22 lines (control +4.18 / +5.61), chi2/dof 0.99937 | PASS |
| The item distribution within 6 sigma of uniform (against the window model, the design's own null) | p2 +4.59 sigma (control +4.64); p1 +45.77; p3 +12,245.66 | FAIL on p1 and p3 |
| No hot set under 1% of items among passing seeds (10^6 nonces on three programs; the 64-seed census at 2^18) | p2 none; p1 top 0.1% at 1.888x the window model (2.247x flat), X/f 2.57; p3 16.46x (18.92x flat), X/f 43.2; census: CENSUS-GATE | FAIL |
| The Counter ASIC lane's record gate: top 0.1% within 1.2x of the window-model control on every seed | p2 1.029x; p1 1.888x; p3 16.46x; census: CENSUS-GATE2 | FAIL |
**Verdict: FINDING (AP-F8-1).** The line index passes at 2^28 derivations. The cross-hash item histogram fails
the hot-set gate on 2 of the 3 named programs (one of them the live devnet epoch-0 program) and on CENSUS-FRACTION of
the 64-seed census, from `or` (and mildly `mul`) writes on a load's source register after its last injecting write,
outside every test of rule (c). What it moves: not the mask or the fold (the derivation is uniform) but the
acceptance rule, (a') and (c') above, and the packs re-cut. Ownership: the Counter ASIC lane (generator and rule),
re-gated with this harness on the fixed branch; the row reads FIXED-AND-PASSED when every seed of the census passes
both the hot-set test and the 1.2x gate under the new rule.
Sample sizes reached: 2^28 derivations (lines); 10^6 nonces on three programs (distinct lines, hot set); one epoch at
2^26 nonces (cross-hash histogram); 64 seeds at 2^18 nonces (the census).
Times UTC in the logs; the runs ran 08:13 to 09:2x UTC on 7 October 2026 (09:13 to 10:2x UK).

View file

@ -0,0 +1,218 @@
# F9: acceptance edges, the hot-set search, header grinding
Attack-pass row F9 of `docs/plans/cryptanalysis.md` section 4.2 (record: `docs/analysis/attack-pass-2026-10.md`).
Sub-agent attack-f9, 7 October 2026. Status: IN PROGRESS (rewritten as each run lands; the numbers below are the
ones already final, each with its log).
## Target
| Item | Value |
|---|---|
| Commit | 924288d1 (branch attack-pass, worktree igneum-wt-attack) |
| Generator | 4, class v4 `mx8+sh256x27` composed with the era draw (`LoadClass::era(V4_CLASS, E, [4 bytes])`), era seed E = the devnet epoch-0 seed `edc4fa84...fb07` (`proto-cuda/packs-ca3-v4/v4-devnet-epoch0/seeds.txt`) |
| Rule | `igneum-pow/src/accept.rs`: (a) stale load sources, (b) injecting writes, (c) the 2,048-evaluation dynamic test on the closed-form stand-in `dataset_elem` at 2^28 words with init words = seed words; redraw on rejection up to 32 attempts |
| Header binding | `igneum-pow/src/bind.rs`: init words `I = seed_words_from_bytes("igneum-block/" \|\| H \|\| nonce_hi_le32)`, one `I` per 32-lane warp, the lane nonce in the low 32 bits |
| Memory-hard dataset for the edges | the devnet day 20730 (`day_seed_hex 69676e65756d2d6461792ffa50000000000000`), class v4 shape (mixer x8, cache 2^26 words, dataset 2^28 words), `Epoch::chain_dataset_day` |
| Card | RunPod RTX 5090 (170 SMs, 32,120 MiB, driver 570.195.03, CUDA 12.8.1), pack `v4-devnet-epoch0` built there with `nvcc -O3 -arch=sm_120` |
## Known-failed shape
A seed grind that steers a program to a hot cache set for DRAM locality, or an edge where the closed-form stand-in
disagrees with the live verifier in the attacker's favour.
## Gate
Zero passing programs with a hot set under 1 percent of items among 10^6 seeds; the grinding gain under 1 percent of
rate at any search cost. What a failure moves: the closed-form stand-in replaced by the live verdict at the edges; a
locality term in rule (c).
## Harness
`tools/attack/f9-grind/` (crate `attack-f9`, `igneum-pow` as a path dependency, nothing in igneum-pow edited; built
on igneum-build-1 through `tools/build-remote.sh`; the binary on the box at
`/srv/builds/igneum-wt-attack/tools/attack/f9-grind/target/release/attack-f9`):
| Sub-command | What it does |
|---|---|
| `selftest` | the firings listed below |
| `edges` | sub-row (a): every candidate of every seed through the re-implemented dynamic test twice, closed form and memory-hard, every metric of rule (c) measured to the end (no early abort) with its margin; the attempts continue until both stand-ins have accepted, so the chosen program under each is known |
| `hotset` | sub-row (b): the seed's accepted program, its 2,048 x 128 address record under the acceptance init and under a block init; per site the nonce-independent address bits, the distinct addresses and the most-read address; the histogram at bucket scales 2^8 to 2^24 words against a window-aware Poisson expectation (the era windows send a site to the dataset, a half or a quarter of it) with a Bonferroni tail; the taint count of init-determined loads |
| `inspect` | one seed's program with every site that repeats an address |
| `grind`, `table-random` | sub-row (c), CPU side: the init-determined load sites of the devnet epoch-0 program, the per-warp search over K nonce_hi values for the fewest distinct 128 B lines inside those load instructions (`--mode intra`, what the coalescer merges) or across them (`lines`, `pages`), the search cost per hit, and the per-warp init tables the card reads |
| `reference` | the 64 bound hashes the card's known-pass compares against; the file used on the pod came from the pre-built `igneum-pow hash-bound` instead (`ref.txt`, sha256 e831458a...) |
| `summarise.py` | the census summaries quoted below |
`tools/attack/f9-grind/pod/` (the card): `make-variants.py` copies the pack's `kernel_bound.cu` into five kernels
(per-warp init table; the five init-determined loads broadcast to lane 0's address; every load broadcast; the first
such load broadcast; lane 1 reading lane 0's address at the first such load), `f9-host.cu` fills the cache and dataset
with the pack's own kernels, checks them against `vectors.h`, checks the bound hash against the reference, checks the
per-warp kernel on an all-equal table against the honest kernel, then times the eight variants in interleaved rounds;
`run.sh` builds on the pod, samples `nvidia-smi` once a second and joins the samples to the phases (`join-power.py`).
Hot-set metric (F8's record `docs/analysis/attack-pass/f8-uniform.md` did not exist when this harness was written, so
the metric is defined here). Strict reading, "any hot bucket": a site with 7 or more nonce-independent address bits
(support at most 2^21 of 2^28 words, 0.78 percent of items; the window's own fixed bits not counted), or any bucket
of at most 2^20 words (0.39 percent of the dataset) at scales 2^8, 2^12, 2^16, 2^20 whose count has a Poisson tail
against its window-aware expectation under 10^-6 after the Bonferroni correction. Gate reading, "flagged": the reads
above expectation in those hot buckets (the hot share, what a cache of the hot set saves at most) reach 1 percent of
the program's reads, or a site has 7 constant bits.
## Firings (the harness is trusted only after these)
Log: `/srv/builds/igneum-wt-attack/attack-f9/selftest.log` (copy in the Mac scratchpad `f9-box/selftest.log`).
| Check | Known-pass | Known-fail | Result |
|---|---|---|---|
| 1 | the re-implemented dynamic test against `accept::check` on 300 class v4 candidates: every verdict, the first failing condition, and distinct, saturated and bias of every accepted report equal | | PASS (300 candidates, 16 rejected by accept, all equal, 1.5 s) |
| 2 | both stand-ins forced equal (closed form twice) on 200 candidates | | PASS, 0 disagreements |
| 3 | | the memory-hard stand-in gives different words: distinct 262,117 against 262,106, bias 56 against 64 on one program | PASS (they differ) |
| 4 | 50 accepted programs, none with 7 constant bits (worst 0) | 50 plants (an accepted program rewritten to `xor a,a; add a,a,256; mulhi a,b` before a load from `a`, 48 of 50 still pass rule (c)) all flagged (support 256 words, site distinct 255 or 256) | PASS for the plant; 4 of the 50 clean programs have a hot bucket (sub-row (b): that is the finding, not a harness fault) |
| 5 | taint on the devnet epoch-0 program against the hand reading of `kernel_bound.cu`: loads 7, 8, 9, 10 read r7, r4, r2, r0 (no load before them); load 31 reads r5 = r5 x r4 from instruction 12, both untouched by any load; loads 11, 13, 29, 30 read r1, r6, r4, r3, each written by an earlier load or by `mad` from r7 after load 10 | | PASS: sites (0,7) (0,8) (0,9) (0,10) (0,31) |
| card 1 | cache FNV 448274a57f508cbc, dataset head, last word and 64 samples, the 96 pack vectors | | PASS (`pod log/host.log`) |
| card 2 | the bound hash against the 64 reference lines of `igneum-pow hash-bound` (nonce_hi 0, prehash 000102..1f) | | PASS, 0 wrong |
| card 3 | the per-warp kernel on an all-equal table equals the honest kernel on 64 lanes | the two-init table: warp 0 equal, warp 1 differs in 32 of 32 lanes | PASS both |
| card 4 | | the forced kernels change the hash: forced4 64 of 64 lanes, forcedall 64, forced1 64, pair 64 | PASS (they fire) |
| card 5 | | a deliberately locality-maximising choice shows a measurable change: `pair` (one line of 4,096 saved per warp) +0.21 percent, `forced1` (31 lines) +6.8 percent, `forced4` (155 lines) +43.3 percent, `forcedall` +181.9 percent in the smoke run | PASS (measurable from one saved line up) |
## Sub-row (a): the edges
Run: `attack-f9 edges` over seeds `igneum-f9/0` to `igneum-f9/99999`, 8 threads on cores 28-31,76-79 in 10,000-seed
chunks under a shared hold of the box measure lock (`run-census.sh`); output `edges.part*.tsv`, summary by
`summarise.py edges`. The first 20,000 seeds (parts 0 and 1) are summarised here; the full 10^5 replaces this table
when the run ends.
| Quantity | First 20,000 seeds |
|---|---|
| Candidates evaluated | 21,020 |
| Verdicts agreeing | 21,007 |
| Disagreements | 13 (0.062 percent of candidates; the 3 October census had 39 in 100,000 on its generator) |
| Seeds whose chosen program differs | 13 (every disagreement moves the chosen attempt, because the next attempt was accepted by both) |
| Exhausted seeds | 0 on either stand-in |
| Rejected by the closed form / by the memory-hard dataset | 1,013 / 1,014 (850 static, the rest (c)) |
| First failing (c) condition, closed form | const_bit 80, saturated 54, distinct 24, lane_const 4, bias 1 |
| Accepted margins, closed form | saturated at most 81 of 164, bias at most 120 of 136, distinct sum at least 247,335 (bound 245,760), nearly constant final bits up to 2,047 of 2,048 |
| Accepted margins, memory-hard | saturated at most 82, bias at most 115, distinct at least 247,678 |
The 13 disagreements: 12 are `const_bit`, a final register bit equal in all 2,048 evaluations on one dataset and in
2,044 to 2,047 of them on the other (7 where the closed form accepts, 5 where the memory-hard dataset accepts); 1 is
`bias`, output bias 155 against 93 (6.9 against 4.1 sigma, the two draws' difference 2.7 sigma of sampling noise),
where the memory-hard dataset accepts. No disagreement on saturation, lane-constant sites or the distinct count: those
metrics are the same to within 20 on both datasets. What an attacker gains from a program the closed form accepts
and the live dataset would reject: a register whose final bit is pinned in 2,047 of 2,048 hashes instead of 2,048,
which no test downstream of the fold can see (the 64 output bits stay within 120 of 1,024 on every accepted program)
and which no chip can turn into skipped work; the reverse direction loses the chain a program with one pinned bit.
Either way the chosen program moves to the next attempt, which both stand-ins accept. Nothing in the attacker's
favour: the verdict's dependence on the stand-in is a 0.06 percent coin flip on a one-bit property.
## Sub-row (b): the hot-set search
Run: `attack-f9 hotset` over seeds `igneum-f9/0` to `igneum-f9/999999`, 8 threads on cores 32-35,80-83 in
100,000-seed chunks; output `hotset.part*.tsv`. A 2,000-seed timing sample (`hotset-timing.tsv`, seeds 5,000,000 to
5,001,999) is summarised here; the 10^6 census replaces it when the run ends.
| Quantity | 2,000-seed sample |
|---|---|
| Programs with any hot bucket (strict) | 161 of 2,000 (8.1 percent) |
| Programs flagged at the gate reading (hot share at least 1 percent of reads) | 21 of 2,000 (1.05 percent) |
| Worst hot share | 10.3 percent of the program's reads (seed 5,000,968) |
| Sites with 7 or more constant address bits | 0 (max 0) |
| Fewest distinct addresses at a site in 2,048 evaluations | 434 |
| Most evaluations reading one address at a site | 767 of 2,048 |
| Init-determined loads in iteration 0 (programs by count) | 1: 234, 2: 573, 3: 594, 4: 368, 5: 179, 6: 42, 7: 10; none after iteration 0 |
FINDING F9-1 (or-saturation hot words). `inspect --seed 5000968` (`inspect-5000968.log`): the load at instruction 33
reads r4; r4 is written by `or r4 |= r0` (20), `mulhi` (27) and `or r4 |= r6` (28), and r6 itself by `or r6 |= r7`
(7). `or` is absorbing toward all ones: after two `or` writes from independent words every bit is set with
probability 7/8 and the whole register with probability (7/8)^32 = 1.4 percent; chained across iterations the mass
grows, and on this program r4 is 0xffffffff at that site in 731 to 739 of 2,048 evaluations in iterations 1 to 7
(36 percent). The site then reads one word, `rotl(0xffffffff x M, R) & window | offset` = 0x0ca59e4c for a full
window and 0x04a59e4c, 0x08a59e4c, ... for the windowed sites; near-all-ones values add a few hundred more. The
same word family appears in every flagged program (seeds 4,000,001, 4,000,037, 4,000,040 in the selftest: `or`
writes at 54 and 55 before the load at 60, or at 1 before the load at 3). Rule (c) does not see it: the saturation
test counts final register values only (the register is overwritten before the end), the lane-constant test needs
all 32 lanes equal, the distinct test counts per lane per hash (the hot word repeats across iterations, so it
costs one distinct of 128), and the output bias stays within tolerance. The 3 October census measured an
`or_sat_frac` per program (section 7.3, max 0.0102) but the adopted rule kept only the final-value count.
What it is worth to an attacker: nothing asymmetric. The hot words are the same for every lane that saturates, so
the GPU's coalescer and L1 already serve them without a DRAM transaction, and a chip gets exactly the same. What it
costs the design: those programs do fewer memory-hard reads than rule (c) promises (up to 10 percent fewer on the
worst program in 2,000, at least 1 percent fewer on about 1 program in 100), so the per-hash memory work of class
v4 is not the uniform 128 random reads the chip model assumes on every epoch. Gate reading: FAIL in the strict
reading (zero passing programs with a hot set under 1 percent of items), FAIL in the share reading too (programs
with a hot set capturing at least 1 percent of reads exist at about 1 percent of epochs). Proposed fix, for the hash
lane (not applied here): a per-site line in rule (c), "every load site reads at least 2,000 distinct addresses over
the 2,048 evaluations" (uniform gives 2,048 minus 0.008 expected repeats; the saturated sites read 434 to 1,855),
computed from the addresses the test already collects (one sort of 2,048 per site, 128 sites, under a millisecond);
the redraw rate rises by about the strict-reading fraction (8 percent of candidates) unless the threshold is placed
at the share reading. The alternative, dropping the `or` family from the draw table, changes the frozen weights and
is for the lane to weigh. Class check: a chip gains nothing today, but a stand-in that lets 1 percent of epochs run
with a 1 to 10 percent lighter memory side is a published-number problem (evidence row 17's per-hash reads).
(the 10^6 numbers and the hot-share distribution replace the sample when the census ends)
## Sub-row (c): header grinding
### What an attacker can steer
Only a load whose address register has not yet absorbed a dataset word is a function of the init words and the
nonce alone (taint analysis, `init_determined_sites`). On the devnet epoch-0 program these are the loads at
instructions 7, 8, 9, 10 and 31 of iteration 0; from iteration 1 every register is tainted. Over the 2,000-seed
sample the count is 1 to 7 per program, median 3, always in iteration 0 only. Everything after depends on dataset
words the miner must fetch first. The init words themselves are an FNV hash of the header and nonce_hi, so the
attacker cannot choose them, only draw them; and one draw serves a whole warp (the shuffles couple the 32 lanes),
so a per-lane draw costs 32 hashes per lane.
### The search (CPU)
`grind` draws K init words per warp (nonce_hi 0 to K-1 under the fixed prehash) and keeps the one with the fewest
distinct 128 B lines among the init-determined loads. Each try costs 32 lanes x (8 init + 32 prefix instructions) =
1,280 lane-instructions; the warp's hash costs 32 x (512 + 55,296) = 1,785,856 lane-instructions, the derivation
not counted. Logs: `grind-k10-crosssite.log` (lines counted across the five sites: coincidences that are at best an
L2 hit), `grind-k10-pages.log` (2 KB pages across the sites), and the `intra` runs (lines inside one load
instruction, what the coalescer merges into one transaction) that feed the card.
| Metric | K | Warps | Mean lines or pages saved per warp (of 4,096 loads) | Warps improved | Search per warp in hashes |
|---|---|---|---|---|---|
| lines across the sites | 2^10 | 524,288 | 0.895 (0.022 percent) | 89 percent | 0.73 |
| 2 KB pages across the sites | 2^10 | 65,536 | 1.455 (0.036 percent) | 98 percent | 0.73 |
| lines inside one instruction (intra) | 2^10 | 2^17 | (pending) | | 0.73 |
| lines inside one instruction (intra) | 2^14 | 2^17 | (pending) | | 11.7 |
### The card (RTX 5090)
Smoke run (1 round of 2 s per variant, `pod smoke` logs): the calibration of what one saved line is worth.
| Variant | What changes | MH/s | Against honest | W | MH/J against honest |
|---|---|---|---|---|---|
| honest | the pack kernel, one init per dispatch | 141.76 | | 482 | |
| perwarp-random | per-warp init table, no search | 141.73 | -0.02 percent | 486 | -0.7 percent |
| perwarp-k10 | per-warp table, best of 2^10 (cross-site table) | 141.74 | -0.01 percent | 487 | -1.0 percent |
| perwarp-k14 | (the same table in the smoke run) | 141.74 | -0.01 percent | 488 | -1.1 percent |
| pair | lane 1 reads lane 0's address at load 7: 1 line of 4,096 saved | 142.05 | +0.21 percent | 490 | -1.4 percent |
| forced1 | load 7 broadcast: 31 lines saved | 151.39 | +6.8 percent | 506 | +1.8 percent |
| forced4 | loads 7, 8, 9, 10, 31 broadcast: 155 lines saved | 203.11 | +43.3 percent | 530 | +30 percent |
| forcedall | every load broadcast: 3,968 lines saved | 399.61 | +182 percent | 520 | +161 percent |
Reading: the class v4 kernel on the 5090 is bound by its random reads (141.8 MH/s x 128 = 18.1 G reads per second,
the card's measured random-read ceiling in `docs/bench-log.md`), and a load instruction completes when its slowest
lane's transaction returns, so one saved line is worth about 0.2 percent of rate, 31 lines 6.8 percent, and the
five init-determined loads fully coalesced 43 percent. That ceiling is unreachable by search: it needs the 32
lanes' 28-bit addresses to fall in one line at five sites, probability 2^-115 per draw. What a draw can reach is one
coalesced pair at one site (probability 5 x C(32,2) / 2^23 = 3 x 10^-4 per try, about 3,400 tries per pair);
two pairs need about 6 million tries, m pairs about 3,400^m / m! tries. One pair is worth 0.2 percent of one warp's
hash and costs 3,400 x 1,280 lane-instructions = 2.4 hashes of search. The measured per-warp tables (5 rounds of
8 s, pending) are the direct check.
(the full run's table replaces the smoke run when it ends)
## Consequences per tier
(filled in with the verdict)
## Logs
| Log | Path |
|---|---|
| selftest, inspect, grind, census parts and drivers | `/srv/builds/igneum-wt-attack/attack-f9/` on igneum-build-1 (`selftest.log`, `inspect-*.log`, `grind-*.log`, `edges.part*.tsv`, `edges.driver.log`, `hotset.part*.tsv`, `hotset.driver.log`, `hotset-timing.tsv`, `ref.txt`, `table-*.bin`) |
| card smoke run and full run | the pod's `/workspace/f9/podjob/smoke/log/` and `log/` (`run.log`, `nvcc.log`, `host.log`, `power.csv`, `power-by-variant.txt`, `sha256.txt`), copied to `/srv/builds/igneum-wt-attack/attack-f9/pod/` at the end |

14
tools/attack/f1-shadow/Cargo.lock generated Normal file
View file

@ -0,0 +1,14 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "attack-f1"
version = "0.1.0"
dependencies = [
"igneum-pow",
]
[[package]]
name = "igneum-pow"
version = "0.2.0"

View file

@ -0,0 +1,20 @@
# Attack-pass row F1 (docs/plans/cryptanalysis.md 4.2): shadow block compressibility and shortcut search.
# Own crate, outside every workspace; built on igneum-build-1 through tools/build-remote.sh from this directory.
[package]
name = "attack-f1"
version = "0.1.0"
edition = "2021"
publish = false
[[bin]]
name = "attack-f1"
path = "src/main.rs"
[dependencies]
igneum-pow = { path = "../../../igneum-pow" }
[workspace]
[profile.release]
opt-level = 3
debug = 1

View file

@ -0,0 +1,312 @@
seed attack-f1/8556 reps 1: instructions 256 -> 244; chip 236 -> 226
idx op d a b rot mask | node new needed opt cost(A) cost(C) | normal form
0 rotl r2 r5 r6 10 8 | #8 new yes a 1 0 | xor{#2<<10}
1 *add r3 r6 r3 22 4 | #10 new no - 0 0 | sum{#3+#6+#9}
2 shfl r1 r0 r4 23 1 | #11 new yes b 1 1 | xor{#0^l1 ^ #1}
3 mulhi r1 r6 r0 9 2 | #13 new yes a 0 0 | hi(#12)
4 sub r4 r0 r6 2 8 | #14 new yes a 1 1 | sum{-#0+#4}
5 rotl r1 r4 r4 26 16 | #15 new yes a 1 0 | xor{#13<<26}
6 shfl r6 r0 r0 17 1 | #16 new yes b 1 1 | xor{#0^l1 ^ #6}
7 add r3 r7 r3 11 4 | #18 new yes a 2 2 | sum{#3+#6+#7+#9+#17}
8 mad r1 r6 r3 27 8 | #21 new yes b 1 1 | sum{#15+#20}
9 add r6 r1 r0 26 1 | #23 new yes b 1 1 | sum{#15+#16+#20+#22}
10 xor r0 r5 r3 10 1 | #24 new yes a 1 1 | xor{#0 ^ #5}
11 sub r7 r4 r2 13 2 | #25 new yes b 1 1 | sum{#0+-#4+#7}
12 mul r0 r7 r2 15 1 | #27 new yes a 0 0 | lo(#26)
13 shfl r3 r0 r7 22 1 | #28 new yes b 1 1 | xor{#18 ^ #27^l1}
14 mad r7 r3 r5 30 1 | #31 new yes b 1 1 | sum{#0+-#4+#7+#30}
15 sub r7 r1 r5 2 16 | #32 new yes b 1 1 | sum{#0+-#4+#7+-#15+-#20+#30}
16 xor r0 r1 r3 17 4 | #33 new yes a 1 1 | xor{#21 ^ #27}
17 add r1 r3 r0 30 8 | #35 new yes b 1 1 | sum{#15+#20+#28+#34}
18 xor r6 r7 r0 3 16 | #36 new yes a 1 1 | xor{#23 ^ #32}
19 add r6 r4 r1 31 1 | #38 new yes b 1 1 | sum{-#0+#4+#36+#37}
20 rotr r1 r0 r0 23 8 | #39 new yes a 1 1 | rotr(#35, 1*#33)
21 shfl r5 r1 r1 19 8 | #40 new yes b 1 1 | xor{#5 ^ #39^l8}
22 shfl r1 r6 r3 29 2 | #41 new yes b 1 1 | xor{#38^l2 ^ #39}
23 mul r2 r7 r4 12 2 | #43 new yes a 0 0 | lo(#42)
24 rotr r6 r4 r4 14 4 | #44 new yes a 1 1 | rotr(#38, 1*#14)
25 mad r0 r3 r2 3 8 | #47 new yes b 1 1 | sum{#33+#46}
26 xor r6 r0 r1 27 2 | #48 new yes a 1 1 | xor{#44 ^ #47}
27 mul r6 r5 r1 1 1 | #50 new yes a 0 0 | lo(#49)
28 rotr r5 r3 r6 11 16 | #51 new yes a 1 1 | rotr(#40, 1*#28)
29 mad r3 r5 r7 6 16 | #54 new yes b 1 1 | sum{#28+#53}
30 mad r2 r5 r6 19 2 | #57 new yes b 1 1 | sum{#43+#56}
31 mad r5 r3 r2 14 1 | #60 new yes b 1 1 | sum{#51+#59}
32 mulhi r1 r6 r6 16 1 | #62 new yes a 0 0 | hi(#61)
33 xor r2 r3 r7 28 8 | #63 new yes a 1 1 | xor{#54 ^ #57}
34 sub r5 r6 r0 5 2 | #64 new yes b 1 1 | sum{-#50+#51+#59}
35 xor r2 r6 r4 22 4 | #65 new yes b 1 1 | xor{#50 ^ #54 ^ #57}
36 mad r6 r3 r7 2 4 | #68 new yes b 1 1 | sum{#50+#67}
37 shfl r6 r0 r3 1 8 | #69 new yes b 1 1 | xor{#47^l8 ^ #68}
38 mul r4 r6 r2 17 1 | #71 new yes a 0 0 | lo(#70)
39 xor r1 r5 r5 14 8 | #72 new yes a 1 1 | xor{#62 ^ #64}
40 rotr r3 r4 r0 22 16 | #73 new yes a 1 1 | rotr(#54, 1*#71)
41 mulhi r5 r2 r1 9 1 | #75 new yes a 0 0 | hi(#74)
42 shfl r4 r0 r1 21 8 | #76 new yes b 1 1 | xor{#47^l8 ^ #71}
43 rotl r5 r7 r5 19 16 | #77 new yes a 1 0 | xor{#75<<19}
44 mad r0 r7 r2 15 1 | #80 new yes b 1 1 | sum{#33+#46+#79}
45 xor r3 r4 r7 26 2 | #81 new yes b 1 1 | xor{#47^l8 ^ #71 ^ #73}
46 mulhi r4 r6 r7 14 16 | #83 new yes a 0 0 | hi(#82)
47 mad r6 r4 r4 21 16 | #86 new yes b 1 1 | sum{#69+#85}
48 xor r1 r0 r7 15 4 | #87 new yes b 1 1 | xor{#62 ^ #64 ^ #80}
49 sub r1 r7 r3 30 1 | #88 new yes b 1 1 | sum{-#0+#4+-#7+#15+#20+-#30+#87}
50 or r4 r1 r0 1 2 | #89 new yes a 1 1 | or{#83|#88}
51 mad r1 r5 r6 13 1 | #92 new yes b 1 1 | sum{-#0+#4+-#7+#15+#20+-#30+#87+#91}
52 *mul r5 r6 r1 16 4 | #91 same yes a 0 0 | lo(#90)
53 *xor r4 r0 r6 24 16 | #93 new no - 0 0 | xor{#80 ^ #89}
54 mad r7 r5 r6 5 2 | #96 new yes b 1 1 | sum{#0+-#4+#7+-#15+-#20+#30+#95}
55 xor r7 r1 r3 7 8 | #97 new yes a 1 1 | xor{#92 ^ #96}
56 xor r2 r5 r2 28 1 | #98 new yes b 1 1 | xor{#50 ^ #54 ^ #57 ^ #91}
57 *xor r4 r0 r2 7 1 | #89 same yes a 1 1 | or{#83|#88}
58 rotl r2 r7 r7 7 2 | #99 new yes b 1 0 | xor{#50<<7 ^ #54<<7 ^ #57<<7 ^ #91<<7}
59 shfl r1 r6 r6 29 1 | #100 new yes b 1 1 | xor{#86^l1 ^ #92}
60 rotr r4 r7 r7 8 2 | #101 new yes a 1 1 | rotr(#89, 1*#97)
61 shfl r6 r5 r6 9 16 | #102 new yes b 1 1 | xor{#86 ^ #91^l16}
62 mulhi r5 r6 r1 5 4 | #104 new yes a 0 0 | hi(#103)
63 mulhi r2 r4 r7 21 16 | #106 new yes a 0 0 | hi(#105)
64 *xor r6 r4 r2 14 2 | #107 new no - 0 0 | xor{#86 ^ #91^l16 ^ #101}
65 mulhi r5 r2 r4 12 8 | #109 new yes a 0 0 | hi(#108)
66 mad r7 r5 r5 12 16 | #112 new yes b 1 1 | sum{#97+#111}
67 *xor r6 r4 r3 25 1 | #102 same yes b 1 1 | xor{#86 ^ #91^l16}
68 mul r3 r5 r2 9 8 | #114 new yes a 0 0 | lo(#113)
69 xor r4 r2 r3 30 4 | #115 new yes a 1 1 | xor{#101 ^ #106}
70 mul r6 r1 r4 13 1 | #117 new yes a 0 0 | lo(#116)
71 xor r3 r0 r3 11 4 | #118 new yes a 1 1 | xor{#80 ^ #114}
72 mulhi r3 r5 r2 6 16 | #120 new yes a 0 0 | hi(#119)
73 rotr r6 r4 r5 3 16 | #121 new yes a 1 1 | rotr(#117, 1*#115)
74 add r6 r4 r1 21 16 | #123 new yes a 1 1 | sum{#115+#121+#122}
75 mad r0 r2 r3 14 16 | #126 new yes b 1 1 | sum{#33+#46+#79+#125}
76 mul r1 r7 r4 21 1 | #128 new yes a 0 0 | lo(#127)
77 mulhi r6 r5 r4 24 16 | #130 new yes a 0 0 | hi(#129)
78 mul r5 r1 r6 3 16 | #132 new yes a 0 0 | lo(#131)
79 sub r3 r5 r2 3 16 | #133 new yes a 1 1 | sum{#120+-#132}
80 rotr r2 r4 r5 20 16 | #134 new yes a 1 1 | rotr(#106, 1*#115)
81 rotr r4 r0 r2 7 2 | #135 new yes a 1 1 | rotr(#115, 1*#126)
82 rotr r6 r5 r6 10 8 | #136 new yes a 1 1 | rotr(#130, 1*#132)
83 or r5 r2 r5 7 4 | #137 new yes a 1 1 | or{#132|#134}
84 add r7 r0 r6 31 4 | #139 new yes b 1 1 | sum{#33+#46+#79+#97+#111+#125+#138}
85 mul r3 r0 r7 4 16 | #141 new yes a 0 0 | lo(#140)
86 or r7 r0 r3 5 2 | #142 new yes a 1 1 | or{#126|#139}
87 mad r4 r5 r2 21 16 | #145 new yes b 1 1 | sum{#135+#144}
88 *rotl r0 r6 r6 30 4 | #146 new no - 0 0 | xor{#126<<30}
89 rotl r0 r4 r0 31 16 | #147 new yes a 1 0 | xor{#126<<29}
90 sub r1 r4 r5 4 4 | #148 new yes b 1 1 | sum{#128+-#135+-#144}
91 shfl r5 r0 r1 27 16 | #149 new yes b 1 1 | xor{#126<<29^l16 ^ #137}
92 mul r5 r1 r0 8 4 | #151 new yes a 0 0 | lo(#150)
93 shfl r3 r2 r0 3 4 | #152 new yes b 1 1 | xor{#134^l4 ^ #141}
94 or r1 r4 r3 6 8 | #153 new yes a 1 1 | or{#145|#148}
95 mul r0 r3 r6 1 4 | #155 new yes a 0 0 | lo(#154)
96 xor r0 r7 r4 28 16 | #156 new yes a 1 0 | xor{#142 ^ #155}
97 rotl r0 r3 r6 17 1 | #157 new yes b 1 1 | xor{#142<<17 ^ #155<<17}
98 mul r0 r5 r5 31 8 | #159 new yes a 0 0 | lo(#158)
99 xor r5 r2 r4 27 8 | #160 new yes a 1 1 | xor{#134 ^ #151}
100 shfl r5 r1 r0 26 8 | #161 new yes b 1 1 | xor{#134 ^ #151 ^ #153^l8}
101 rotl r2 r0 r1 10 1 | #162 new yes a 1 0 | xor{#134<<10}
102 mad r0 r7 r5 8 4 | #165 new yes b 1 1 | sum{#159+#164}
103 mul r6 r0 r5 27 8 | #167 new yes a 0 0 | lo(#166)
104 mul r1 r4 r0 17 8 | #169 new yes a 0 0 | lo(#168)
105 shfl r0 r5 r5 28 4 | #170 new yes b 1 1 | xor{#134^l4 ^ #151^l4 ^ #153^l12 ^ #165}
106 xor r0 r3 r1 18 4 | #171 new yes b 1 1 | xor{#141 ^ #151^l4 ^ #153^l12 ^ #165}
107 add r0 r2 r1 24 8 | #173 new yes a 1 1 | sum{#162+#171+#172}
108 mulhi r7 r2 r2 23 16 | #175 new yes a 0 0 | hi(#174)
109 shfl r1 r7 r4 5 16 | #176 new yes b 1 1 | xor{#169 ^ #175^l16}
110 xor r3 r2 r0 19 4 | #177 new yes b 1 1 | xor{#134^l4 ^ #134<<10 ^ #141}
111 shfl r3 r2 r4 26 8 | #178 new yes b 1 1 | xor{#134^l4 ^ #134<<10 ^ #134<<10^l8 ^ #141}
112 add r3 r5 r7 5 4 | #180 new yes a 1 1 | sum{#161+#178+#179}
113 add r3 r1 r1 24 4 | #182 new yes b 1 1 | sum{#161+#176+#178+#179+#181}
114 *add r7 r5 r2 9 8 | #184 new no - 0 0 | sum{#161+#175+#183}
115 add r5 r2 r6 26 1 | #186 new yes a 1 1 | sum{#161+#162+#185}
116 shfl r0 r4 r2 15 1 | #187 new yes b 1 1 | xor{#145^l1 ^ #173}
117 shfl r1 r2 r7 7 16 | #188 new yes b 1 1 | xor{#134<<10^l16 ^ #169 ^ #175^l16}
118 sub r7 r5 r6 31 16 | #189 new yes a 1 1 | sum{-#162+#175+#183+-#185}
119 add r6 r1 r7 15 4 | #191 new yes a 1 1 | sum{#167+#188+#190}
120 mad r0 r3 r5 13 1 | #194 new yes b 1 1 | sum{#187+#193}
121 shfl r7 r6 r6 10 16 | #195 new yes b 1 1 | xor{#189 ^ #191^l16}
122 shfl r1 r0 r1 31 4 | #196 new yes b 1 1 | xor{#134<<10^l16 ^ #169 ^ #175^l16 ^ #194^l4}
123 mulhi r7 r5 r3 9 8 | #198 new yes a 0 0 | hi(#197)
124 mad r0 r2 r5 23 16 | #201 new yes b 1 1 | sum{#187+#193+#200}
125 mulhi r7 r2 r7 25 1 | #203 new yes a 0 0 | hi(#202)
126 xor r7 r6 r0 24 16 | #204 new yes a 1 1 | xor{#191 ^ #203}
127 rotr r1 r7 r0 18 16 | #205 new yes a 1 1 | rotr(#196, 1*#204)
128 sub r5 r2 r3 5 16 | #206 new yes a 1 1 | sum{#161+#185}
129 mulhi r0 r7 r5 2 4 | #208 new yes a 0 0 | hi(#207)
130 *xor r5 r0 r5 14 2 | #209 new no - 0 0 | xor{#206 ^ #208}
131 mulhi r2 r1 r7 8 16 | #211 new yes a 0 0 | hi(#210)
132 *xor r5 r0 r4 8 4 | #206 same yes a 1 1 | sum{#161+#185}
133 rotr r4 r1 r3 14 8 | #212 new yes a 1 1 | rotr(#145, 1*#205)
134 mul r0 r7 r0 9 16 | #214 new yes a 0 0 | lo(#213)
135 sub r3 r6 r5 1 8 | #215 new yes b 1 1 | sum{#161+-#167+#176+#178+#179+#181+-#188+-#190}
136 rotl r5 r0 r0 2 4 | #216 new yes a 1 0 | xor{#206<<2}
137 *add r5 r3 r3 23 8 | #218 new no - 0 0 | sum{#161+-#167+#176+#178+#179+#181+-#188+-#190+#216+#217}
138 add r6 r1 r0 2 2 | #220 new yes b 1 1 | sum{#167+#188+#190+#205+#219}
139 *sub r5 r7 r1 3 8 | #221 new no - 0 0 | sum{#161+-#167+#176+#178+#179+#181+-#188+-#190+-#204+#216+#217}
140 xor r7 r4 r3 23 2 | #222 new yes b 1 1 | xor{#191 ^ #203 ^ #212}
141 sub r5 r3 r1 3 1 | #223 new yes a 1 1 | sum{-#204+#216+#217}
142 shfl r2 r1 r1 18 16 | #224 new yes b 1 1 | xor{#205^l16 ^ #211}
143 rotl r2 r6 r2 27 8 | #225 new yes b 1 0 | xor{#205<<27^l16 ^ #211<<27}
144 mul r6 r0 r0 22 8 | #227 new yes a 0 0 | lo(#226)
145 shfl r4 r0 r0 16 16 | #228 new yes b 1 1 | xor{#212 ^ #214^l16}
146 shfl r1 r0 r1 14 16 | #229 new yes b 1 1 | xor{#205 ^ #214^l16}
147 mulhi r7 r4 r3 6 16 | #231 new yes a 0 0 | hi(#230)
148 xor r3 r0 r4 26 16 | #232 new yes a 1 1 | xor{#214 ^ #215}
149 mulhi r1 r4 r2 7 4 | #234 new yes a 0 0 | hi(#233)
150 shfl r3 r0 r1 13 4 | #235 new yes b 1 1 | xor{#214 ^ #214^l4 ^ #215}
151 xor r5 r4 r5 16 2 | #236 new yes b 1 1 | xor{#212 ^ #214^l16 ^ #223}
152 mulhi r3 r4 r7 4 2 | #238 new yes a 0 0 | hi(#237)
153 mulhi r4 r6 r2 29 8 | #240 new yes a 0 0 | hi(#239)
154 mul r6 r2 r3 1 8 | #242 new yes a 0 0 | lo(#241)
155 add r1 r0 r7 7 8 | #244 new yes a 1 1 | sum{#214+#234+#243}
156 shfl r6 r1 r4 15 4 | #245 new yes b 1 1 | xor{#242 ^ #244^l4}
157 xor r4 r5 r2 8 4 | #246 new yes b 1 1 | xor{#212 ^ #214^l16 ^ #223 ^ #240}
158 or r1 r3 r1 5 4 | #247 new yes a 1 1 | or{#238|#244}
159 xor r3 r6 r2 14 4 | #248 new yes b 1 1 | xor{#238 ^ #242 ^ #244^l4}
160 add r7 r1 r2 12 2 | #250 new yes a 1 1 | sum{#231+#247+#249}
161 rotr r3 r4 r0 6 8 | #251 new yes a 1 1 | rotr(#248, 1*#246)
162 rotl r7 r4 r0 5 1 | #252 new yes a 1 0 | xor{#250<<5}
163 sub r0 r6 r2 5 2 | #253 new yes a 1 1 | sum{#214+-#245}
164 mad r1 r4 r1 30 4 | #256 new yes a 1 1 | sum{#247+#255}
165 add r5 r1 r4 2 1 | #258 new yes b 1 1 | sum{#236+#247+#255+#257}
166 shfl r2 r0 r5 13 2 | #259 new yes b 1 1 | xor{#205<<27^l16 ^ #211<<27 ^ #253^l2}
167 or r0 r6 r2 29 8 | #260 new yes a 1 1 | or{#245|#253}
168 rotr r7 r4 r0 8 8 | #261 new yes a 1 1 | rotr(#252, 1*#246)
169 rotl r2 r1 r3 19 2 | #262 new yes b 1 0 | xor{#205<<14^l16 ^ #211<<14 ^ #253<<19^l2}
170 add r4 r3 r1 25 4 | #264 new yes a 1 1 | sum{#246+#251+#263}
171 mul r2 r6 r5 29 8 | #266 new yes a 0 0 | lo(#265)
172 rotr r1 r3 r4 13 16 | #267 new yes a 1 1 | rotr(#256, 1*#251)
173 rotr r0 r7 r0 28 1 | #268 new yes a 1 1 | rotr(#260, 1*#261)
174 or r7 r2 r4 19 16 | #269 new yes a 1 1 | or{#261|#266}
175 rotl r4 r2 r5 6 8 | #270 new yes a 1 0 | xor{#264<<6}
176 xor r4 r7 r0 2 4 | #271 new yes a 1 1 | xor{#264<<6 ^ #269}
177 shfl r1 r0 r5 2 2 | #272 new yes b 1 1 | xor{#267 ^ #268^l2}
178 mulhi r4 r0 r5 15 8 | #274 new yes a 0 0 | hi(#273)
179 rotr r4 r6 r2 7 8 | #275 new yes a 1 1 | rotr(#274, 1*#245)
180 xor r5 r1 r4 11 2 | #276 new yes b 1 1 | xor{#258 ^ #267 ^ #268^l2}
181 rotr r4 r0 r4 12 4 | #277 new yes a 1 1 | rotr(#275, 1*#268)
182 mul r6 r7 r5 22 16 | #279 new yes a 0 0 | lo(#278)
183 or r6 r7 r2 24 4 | #280 new yes b 1 1 | or{#261|#266|#279}
184 sub r1 r2 r1 18 8 | #281 new yes a 1 1 | sum{-#266+#272}
185 add r5 r4 r7 9 8 | #283 new yes a 1 1 | sum{#276+#277+#282}
186 mad r4 r3 r7 5 2 | #286 new yes b 1 1 | sum{#277+#285}
187 or r2 r4 r7 17 4 | #287 new yes a 1 1 | or{#266|#286}
188 mul r4 r7 r3 24 16 | #289 new yes a 0 0 | lo(#288)
189 *xor r0 r2 r2 7 8 | #290 new no - 0 0 | xor{#268 ^ #287}
190 rotl r3 r5 r0 20 8 | #291 new yes a 1 0 | xor{#251<<20}
191 *xor r0 r2 r6 27 8 | #268 same yes a 1 1 | rotr(#260, 1*#261)
192 add r2 r6 r1 25 2 | #293 new yes a 1 1 | sum{#280+#287+#292}
193 sub r6 r0 r2 4 8 | #294 new yes a 1 1 | sum{-#268+#280}
194 mad r6 r0 r1 29 1 | #297 new yes b 1 1 | sum{-#268+#280+#296}
195 sub r3 r2 r7 17 1 | #298 new yes b 1 1 | sum{-#280+-#287+#291+-#292}
196 xor r7 r1 r3 9 4 | #299 new yes a 1 1 | xor{#269 ^ #281}
197 or r2 r0 r3 5 8 | #300 new yes a 1 1 | or{#268|#293}
198 add r7 r3 r6 30 4 | #302 new yes b 1 1 | sum{-#280+-#287+#291+-#292+#299+#301}
199 mulhi r1 r6 r6 25 1 | #304 new yes a 0 0 | hi(#303)
200 rotr r1 r3 r1 24 2 | #305 new yes a 1 1 | rotr(#304, 1*#298)
201 mad r6 r2 r0 27 16 | #308 new yes b 1 1 | sum{-#268+#280+#296+#307}
202 xor r0 r7 r4 8 8 | #309 new yes a 1 1 | xor{#268 ^ #302}
203 shfl r7 r5 r1 12 8 | #310 new yes b 1 1 | xor{#283^l8 ^ #302}
204 xor r1 r7 r1 18 2 | #311 new yes b 1 1 | xor{#283^l8 ^ #302 ^ #305}
205 mul r1 r7 r2 9 1 | #313 new yes a 0 0 | lo(#312)
206 mulhi r5 r2 r0 2 2 | #315 new yes a 0 0 | hi(#314)
207 mad r0 r4 r1 14 16 | #318 new yes b 1 1 | sum{#309+#317}
208 mad r0 r4 r2 27 8 | #321 new yes b 1 1 | sum{#309+#317+#320}
209 rotr r6 r3 r2 1 16 | #322 new yes a 1 1 | rotr(#308, 1*#298)
210 mul r0 r1 r7 10 1 | #324 new yes a 0 0 | lo(#323)
211 add r5 r6 r4 20 4 | #326 new yes a 1 1 | sum{#315+#322+#325}
212 sub r6 r3 r1 31 16 | #327 new yes b 1 1 | sum{#280+#287+-#291+#292+#322}
213 xor r3 r0 r2 26 1 | #328 new yes a 1 1 | xor{#298 ^ #324}
214 add r3 r7 r1 15 4 | #330 new yes a 1 1 | sum{#310+#328+#329}
215 xor r3 r0 r1 10 1 | #331 new yes a 1 1 | xor{#324 ^ #330}
216 sub r7 r3 r2 16 8 | #332 new yes a 1 1 | sum{#310+-#331}
217 mad r7 r6 r0 31 2 | #335 new yes b 1 1 | sum{#310+-#331+#334}
218 mul r6 r3 r2 30 1 | #337 new yes a 0 0 | lo(#336)
219 rotr r1 r2 r0 4 1 | #338 new yes a 1 1 | rotr(#313, 1*#300)
220 add r1 r6 r2 2 4 | #340 new yes a 1 1 | sum{#337+#338+#339}
221 add r6 r4 r5 3 2 | #342 new yes a 1 1 | sum{#289+#337+#341}
222 rotr r3 r4 r3 8 16 | #343 new yes a 1 1 | rotr(#331, 1*#289)
223 rotl r6 r5 r5 3 16 | #344 new yes a 1 0 | xor{#342<<3}
224 add r2 r7 r0 25 8 | #346 new yes b 1 1 | sum{#300+#310+-#331+#334+#345}
225 mad r4 r5 r1 19 2 | #349 new yes b 1 1 | sum{#289+#348}
226 sub r3 r0 r2 22 2 | #350 new yes a 1 1 | sum{-#324+#343}
227 shfl r3 r1 r7 4 1 | #351 new yes b 1 1 | xor{#340^l1 ^ #350}
228 mad r7 r2 r4 12 16 | #354 new yes b 1 1 | sum{#310+-#331+#334+#353}
229 add r7 r0 r4 4 2 | #356 new yes b 1 1 | sum{#310+#324+-#331+#334+#353+#355}
230 mad r3 r5 r6 19 1 | #359 new yes b 1 1 | sum{#351+#358}
231 xor r7 r2 r4 29 2 | #360 new yes a 1 1 | xor{#346 ^ #356}
232 rotl r0 r4 r0 18 4 | #361 new yes a 1 0 | xor{#324<<18}
233 rotl r4 r2 r6 26 2 | #362 new yes a 1 0 | xor{#349<<26}
234 xor r6 r3 r3 8 1 | #363 new yes a 1 1 | xor{#342<<3 ^ #359}
235 mad r1 r2 r2 21 2 | #366 new yes b 1 1 | sum{#337+#338+#339+#365}
236 shfl r6 r0 r6 12 8 | #367 new yes b 1 1 | xor{#324<<18^l8 ^ #342<<3 ^ #359}
237 mul r0 r3 r2 4 2 | #369 new yes a 0 0 | lo(#368)
238 add r3 r2 r3 26 16 | #371 new yes b 1 1 | sum{#300+#310+-#331+#334+#345+#351+#358+#370}
239 mulhi r0 r4 r6 9 2 | #373 new yes a 0 0 | hi(#372)
240 sub r2 r5 r0 25 16 | #374 new yes b 1 1 | sum{#300+#310+-#315+-#322+-#325+-#331+#334+#345}
241 *rotl r0 r1 r5 31 8 | #375 new no - 0 0 | xor{#373<<31}
242 shfl r5 r2 r4 28 2 | #376 new yes b 1 1 | xor{#326 ^ #374^l2}
243 mul r6 r2 r3 26 4 | #378 new yes a 0 0 | lo(#377)
244 xor r3 r4 r5 5 1 | #379 new yes a 1 1 | xor{#349<<26 ^ #371}
245 sub r5 r2 r5 30 2 | #380 new yes b 1 1 | sum{-#300+-#310+#315+#322+#325+#331+-#334+-#345+#376}
246 mul r5 r7 r7 17 1 | #382 new yes a 0 0 | lo(#381)
247 add r3 r2 r4 19 2 | #384 new yes b 1 1 | sum{#300+#310+-#315+-#322+-#325+-#331+#334+#345+#379+#383}
248 rotl r6 r7 r2 13 2 | #385 new yes a 1 0 | xor{#378<<13}
249 mad r2 r1 r7 7 4 | #388 new yes b 1 1 | sum{#300+#310+-#315+-#322+-#325+-#331+#334+#345+#387}
250 mul r2 r5 r7 8 1 | #390 new yes a 0 0 | lo(#389)
251 rotl r0 r7 r0 12 1 | #391 new yes a 1 0 | xor{#373<<11}
252 mul r5 r7 r0 14 2 | #393 new yes a 0 0 | lo(#392)
253 mul r2 r4 r6 26 1 | #395 new yes a 0 0 | lo(#394)
254 xor r4 r6 r0 25 1 | #396 new yes a 1 1 | xor{#349<<26 ^ #378<<13}
255 shfl r7 r0 r3 28 4 | #397 new yes b 1 1 | xor{#346 ^ #356 ^ #373<<11^l4}
lines flagged * (result is an existing node, or a node the realisation does not need): 15
extra needed node #12 cost 1 : mul64(#6,#11)
extra needed node #26 cost 1 : mul64(#24,#25)
extra needed node #42 cost 1 : mul64(#8,#32)
extra needed node #49 cost 1 : mul64(#40,#48)
extra needed node #61 cost 1 : mul64(#41,#50)
extra needed node #70 cost 1 : mul64(#14,#69)
extra needed node #74 cost 1 : mul64(#64,#65)
extra needed node #82 cost 1 : mul64(#69,#76)
extra needed node #90 cost 1 : mul64(#77,#86)
extra needed node #103 cost 1 : mul64(#91,#102)
extra needed node #105 cost 1 : mul64(#99,#101)
extra needed node #108 cost 1 : mul64(#104,#106)
extra needed node #113 cost 1 : mul64(#81,#109)
extra needed node #116 cost 1 : mul64(#100,#102)
extra needed node #119 cost 1 : mul64(#109,#118)
extra needed node #127 cost 1 : mul64(#100,#112)
extra needed node #129 cost 1 : mul64(#109,#123)
extra needed node #131 cost 1 : mul64(#109,#128)
extra needed node #140 cost 1 : mul64(#126,#133)
extra needed node #150 cost 1 : mul64(#148,#149)
extra needed node #154 cost 1 : mul64(#147,#152)
extra needed node #158 cost 1 : mul64(#151,#157)
extra needed node #166 cost 1 : mul64(#136,#165)
extra needed node #168 cost 1 : mul64(#145,#153)
extra needed node #174 cost 1 : mul64(#142,#162)
extra needed node #197 cost 1 : mul64(#186,#195)
extra needed node #202 cost 1 : mul64(#162,#198)
extra needed node #207 cost 1 : mul64(#201,#204)
extra needed node #210 cost 1 : mul64(#162,#205)
extra needed node #213 cost 1 : mul64(#204,#208)
extra needed node #226 cost 1 : mul64(#214,#220)
extra needed node #230 cost 1 : mul64(#222,#228)
extra needed node #233 cost 1 : mul64(#228,#229)
extra needed node #237 cost 1 : mul64(#228,#235)
extra needed node #239 cost 1 : mul64(#227,#228)
extra needed node #241 cost 1 : mul64(#225,#227)
extra needed node #254 cost 1 : mul64(#246,#247)
extra needed node #265 cost 1 : mul64(#245,#262)
extra needed node #273 cost 1 : mul64(#268,#271)
extra needed node #278 cost 1 : mul64(#245,#269)
extra needed node #288 cost 1 : mul64(#269,#286)
extra needed node #303 cost 1 : mul64(#281,#297)
extra needed node #312 cost 1 : mul64(#310,#311)
extra needed node #314 cost 1 : mul64(#283,#300)
extra needed node #323 cost 1 : mul64(#313,#321)
extra needed node #336 cost 1 : mul64(#327,#331)
extra needed node #368 cost 1 : mul64(#359,#361)
extra needed node #372 cost 1 : mul64(#362,#369)
extra needed node #377 cost 1 : mul64(#367,#374)
extra needed node #381 cost 1 : mul64(#360,#380)
extra needed node #389 cost 1 : mul64(#382,#388)
extra needed node #392 cost 1 : mul64(#360,#382)
extra needed node #394 cost 1 : mul64(#362,#390)

View file

@ -0,0 +1,319 @@
seed attack-f1/8948 reps 1: instructions 256 -> 253; chip 220 -> 218
idx op d a b rot mask | node new needed opt cost(A) cost(C) | normal form
0 mulhi r3 r7 r7 5 16 | #9 new yes a 0 1 | hi(#8)
1 xor r5 r4 r1 4 4 | #10 new yes a 1 1 | xor{#4 ^ #5}
2 mulhi r6 r3 r4 3 1 | #12 new yes a 0 1 | hi(#11)
3 add r2 r4 r4 27 16 | #14 new yes a 1 1 | sum{#2+#4+#13}
4 add r4 r0 r6 1 4 | #16 new yes a 1 1 | sum{#0+#4+#15}
5 mul r4 r3 r5 3 8 | #18 new yes a 0 1 | lo(#17)
6 rotl r5 r1 r4 2 1 | #19 new yes b 1 0 | xor{#4<<2 ^ #5<<2}
7 mul r4 r0 r2 10 16 | #21 new yes a 0 1 | lo(#20)
8 mad r5 r2 r3 10 4 | #24 new yes b 1 1 | sum{#19+#23}
9 mul r5 r1 r1 5 8 | #26 new yes a 0 1 | lo(#25)
10 xor r7 r0 r6 14 1 | #27 new yes a 1 1 | xor{#0 ^ #7}
11 or r0 r2 r1 21 2 | #28 new yes a 1 1 | or{#0|#14}
12 mul r3 r7 r0 30 1 | #30 new yes a 0 1 | lo(#29)
13 add r6 r0 r6 18 4 | #32 new yes a 1 1 | sum{#12+#28+#31}
14 rotr r3 r4 r2 3 8 | #33 new yes a 1 1 | rotr(#30, 1*#21)
15 xor r2 r1 r6 7 8 | #34 new yes a 1 1 | xor{#1 ^ #14}
16 mul r0 r2 r4 6 1 | #36 new yes a 0 1 | lo(#35)
17 mulhi r4 r3 r0 16 1 | #38 new yes a 0 1 | hi(#37)
18 sub r1 r7 r5 14 16 | #39 new yes a 1 1 | sum{#1+-#27}
19 shfl r6 r7 r5 21 2 | #40 new yes b 1 1 | xor{#0^l2 ^ #7^l2 ^ #32}
20 mul r6 r5 r3 31 4 | #42 new yes a 0 1 | lo(#41)
21 add r4 r6 r2 4 8 | #44 new yes a 1 1 | sum{#38+#42+#43}
22 mad r6 r2 r2 28 8 | #47 new yes b 1 1 | sum{#42+#46}
23 shfl r2 r0 r4 16 1 | #48 new yes b 1 1 | xor{#1 ^ #14 ^ #36^l1}
24 mulhi r5 r1 r5 13 1 | #50 new yes a 0 1 | hi(#49)
25 shfl r4 r6 r4 24 1 | #51 new yes b 1 1 | xor{#44 ^ #47^l1}
26 sub r7 r2 r7 27 2 | #52 new yes a 1 1 | sum{#27+-#48}
27 xor r1 r3 r2 3 1 | #53 new yes a 1 1 | xor{#33 ^ #39}
28 rotl r4 r6 r0 29 8 | #54 new yes b 1 0 | xor{#44<<29 ^ #47<<29^l1}
29 add r2 r4 r1 5 2 | #56 new yes a 1 1 | sum{#48+#54+#55}
30 add r4 r5 r3 20 2 | #58 new yes a 1 1 | sum{#50+#54+#57}
31 xor r4 r2 r3 19 4 | #59 new yes a 1 1 | xor{#56 ^ #58}
32 add r1 r2 r1 18 1 | #61 new yes b 1 1 | sum{#48+#53+#54+#55+#60}
33 sub r4 r7 r3 10 1 | #62 new yes b 1 1 | sum{-#27+#48+#59}
34 mul r7 r6 r0 3 4 | #64 new yes a 0 1 | lo(#63)
35 mul r5 r2 r4 30 2 | #66 new yes a 0 1 | lo(#65)
36 mulhi r6 r3 r5 2 4 | #68 new yes a 0 1 | hi(#67)
37 mad r3 r1 r7 14 8 | #71 new yes b 1 1 | sum{#33+#70}
38 add r0 r2 r5 6 16 | #73 new yes b 1 1 | sum{#36+#48+#54+#55+#72}
39 rotl r2 r1 r3 2 8 | #74 new yes a 1 0 | xor{#56<<2}
40 rotr r1 r0 r3 18 1 | #75 new yes a 1 1 | rotr(#61, 1*#73)
41 mulhi r6 r7 r7 5 4 | #77 new yes a 0 1 | hi(#76)
42 add r0 r4 r1 4 16 | #79 new yes b 1 1 | sum{-#27+#36+2*#48+#54+#55+#59+#72+#78}
43 add r0 r2 r2 6 16 | #81 new yes b 1 1 | sum{-#27+#36+2*#48+#54+#55+#59+#72+#74+#78+#80}
44 add r5 r4 r4 13 2 | #83 new yes b 1 1 | sum{-#27+#48+#59+#66+#82}
45 mul r4 r6 r5 21 1 | #85 new yes a 0 1 | lo(#84)
46 xor r5 r3 r6 14 4 | #86 new yes a 1 1 | xor{#71 ^ #83}
47 or r0 r3 r7 16 2 | #87 new yes a 1 1 | or{#71|#81}
48 shfl r4 r0 r1 23 1 | #88 new yes b 1 1 | xor{#85 ^ #87^l1}
49 sub r0 r1 r5 26 1 | #89 new yes a 1 1 | sum{-#75+#87}
50 mulhi r1 r6 r2 3 8 | #91 new yes a 0 1 | hi(#90)
51 xor r1 r4 r6 9 2 | #92 new yes b 1 1 | xor{#85 ^ #87^l1 ^ #91}
52 mad r6 r0 r4 18 1 | #95 new yes b 1 1 | sum{#77+#94}
53 mul r0 r2 r2 12 2 | #97 new yes a 0 1 | lo(#96)
54 rotl r0 r4 r5 30 2 | #98 new yes a 1 0 | xor{#97<<30}
55 mulhi r0 r1 r5 7 16 | #100 new yes a 0 1 | hi(#99)
56 mul r5 r0 r3 13 1 | #102 new yes a 0 1 | lo(#101)
57 add r2 r3 r7 10 8 | #104 new yes b 1 1 | sum{#33+#70+#74+#103}
58 add r7 r0 r3 23 1 | #106 new yes a 1 1 | sum{#64+#100+#105}
59 mad r7 r4 r1 28 8 | #109 new yes b 1 1 | sum{#64+#100+#105+#108}
60 rotr r1 r2 r5 9 8 | #110 new yes a 1 1 | rotr(#92, 1*#104)
61 xor r7 r4 r2 17 1 | #111 new yes b 1 1 | xor{#85 ^ #87^l1 ^ #109}
62 shfl r5 r4 r4 15 1 | #112 new yes b 1 1 | xor{#85^l1 ^ #87 ^ #102}
63 shfl r3 r5 r4 4 2 | #113 new yes b 1 1 | xor{#71 ^ #85^l3 ^ #87^l2 ^ #102^l2}
64 sub r4 r0 r4 6 1 | #114 new yes a 1 1 | sum{#88+-#100}
65 mul r3 r6 r6 4 4 | #116 new yes a 0 1 | lo(#115)
66 rotl r3 r1 r7 17 1 | #117 new yes a 1 0 | xor{#116<<17}
67 xor r7 r6 r0 3 1 | #118 new yes b 1 1 | xor{#85 ^ #87^l1 ^ #95 ^ #109}
68 mulhi r2 r7 r5 22 16 | #120 new yes a 0 1 | hi(#119)
69 mad r5 r0 r7 16 1 | #123 new yes a 1 1 | sum{#112+#122}
70 mul r6 r5 r0 19 1 | #125 new yes a 0 1 | lo(#124)
71 *mul r0 r7 r7 4 4 | #122 same yes a 0 0 | lo(#121)
72 mad r4 r0 r7 12 4 | #128 new yes b 1 1 | sum{#88+-#100+#127}
73 xor r1 r2 r7 15 16 | #129 new yes a 1 1 | xor{#110 ^ #120}
74 rotl r5 r3 r6 20 4 | #130 new yes a 1 0 | xor{#123<<20}
75 xor r1 r4 r0 17 2 | #131 new yes b 1 1 | xor{#110 ^ #120 ^ #128}
76 add r6 r7 r5 20 2 | #133 new yes a 1 1 | sum{#118+#125+#132}
77 mad r5 r0 r2 17 8 | #136 new yes b 1 1 | sum{#130+#135}
78 xor r7 r1 r5 30 2 | #137 new yes b 1 1 | xor{#85 ^ #87^l1 ^ #95 ^ #109 ^ #110 ^ #120 ^ #128}
79 add r4 r1 r5 29 4 | #139 new yes b 1 1 | sum{#88+-#100+#127+#131+#138}
80 mad r2 r7 r5 24 8 | #142 new yes b 1 1 | sum{#120+#141}
81 mul r0 r3 r0 17 8 | #144 new yes a 0 1 | lo(#143)
82 xor r0 r4 r2 4 2 | #145 new yes a 1 1 | xor{#139 ^ #144}
83 xor r2 r5 r2 29 4 | #146 new yes a 1 1 | xor{#136 ^ #142}
84 add r0 r5 r6 17 16 | #148 new yes b 1 1 | sum{#130+#135+#145+#147}
85 add r7 r0 r6 10 16 | #150 new yes b 1 1 | sum{#130+#135+#137+#145+#147+#149}
86 add r2 r6 r3 26 4 | #152 new yes b 1 1 | sum{#118+#125+#132+#146+#151}
87 mul r2 r0 r3 22 2 | #154 new yes a 0 1 | lo(#153)
88 add r1 r0 r6 22 4 | #156 new yes b 1 1 | sum{#130+#131+#135+#145+#147+#155}
89 rotl r6 r0 r3 12 4 | #157 new yes a 1 0 | xor{#133<<12}
90 mad r7 r6 r7 28 4 | #160 new yes b 1 1 | sum{#130+#135+#137+#145+#147+#149+#159}
91 rotl r0 r7 r2 11 2 | #161 new yes a 1 0 | xor{#148<<11}
92 xor r1 r5 r6 2 8 | #162 new yes a 1 1 | xor{#136 ^ #156}
93 mulhi r4 r5 r3 10 2 | #164 new yes a 0 1 | hi(#163)
94 rotl r3 r0 r7 17 2 | #165 new yes a 1 0 | xor{#116<<2}
95 rotl r1 r5 r4 10 1 | #166 new yes b 1 0 | xor{#136<<10 ^ #156<<10}
96 sub r3 r6 r5 8 1 | #167 new yes a 1 1 | sum{-#157+#165}
97 or r0 r7 r2 5 16 | #168 new yes a 1 1 | or{#160|#161}
98 shfl r1 r0 r0 5 16 | #169 new yes b 1 1 | xor{#136<<10 ^ #156<<10 ^ #168^l16}
99 mul r2 r3 r6 7 8 | #171 new yes a 0 1 | lo(#170)
100 mul r2 r7 r3 31 1 | #173 new yes a 0 1 | lo(#172)
101 mulhi r4 r3 r7 21 8 | #175 new yes a 0 1 | hi(#174)
102 xor r0 r1 r4 20 4 | #176 new yes b 1 1 | xor{#136<<10 ^ #156<<10 ^ #168 ^ #168^l16}
103 rotl r1 r2 r4 6 4 | #177 new yes b 1 0 | xor{#136<<16 ^ #156<<16 ^ #168<<6^l16}
104 *sub r1 r3 r0 9 8 | #178 new no - 0 1 | sum{#157+-#165+#177}
105 mad r7 r5 r6 1 8 | #181 new yes b 1 1 | sum{#130+#135+#137+#145+#147+#149+#159+#180}
106 sub r6 r3 r0 18 16 | #182 new yes a 1 1 | sum{2*#157+-#165}
107 mul r0 r4 r4 18 16 | #184 new yes a 0 1 | lo(#183)
108 shfl r2 r0 r3 22 8 | #185 new yes b 1 1 | xor{#173 ^ #184^l8}
109 or r4 r6 r7 20 8 | #186 new yes a 1 1 | or{#175|#182}
110 add r1 r3 r3 5 1 | #188 new yes a 1 1 | sum{#177+#187}
111 mad r1 r7 r5 24 16 | #191 new yes b 1 1 | sum{#177+#187+#190}
112 *xor r2 r1 r6 30 4 | #192 new no - 0 0 | xor{#173 ^ #184^l8 ^ #191}
113 rotr r0 r3 r1 3 16 | #193 new yes a 1 1 | rotr(#184, 1*#167)
114 mad r0 r1 r1 21 1 | #196 new yes b 1 1 | sum{#193+#195}
115 mulhi r6 r5 r2 4 8 | #198 new yes a 0 1 | hi(#197)
116 *xor r2 r1 r6 14 1 | #185 same yes b 1 1 | xor{#173 ^ #184^l8}
117 mul r2 r5 r4 17 16 | #200 new yes a 0 1 | lo(#199)
118 mulhi r0 r3 r6 17 2 | #202 new yes a 0 1 | hi(#201)
119 xor r6 r0 r2 23 2 | #203 new yes a 1 1 | xor{#198 ^ #202}
120 shfl r5 r6 r5 4 16 | #204 new yes b 1 1 | xor{#136 ^ #198^l16 ^ #202^l16}
121 mad r0 r5 r4 17 4 | #207 new yes b 1 1 | sum{#202+#206}
122 mul r3 r5 r4 30 4 | #209 new yes a 0 1 | lo(#208)
123 add r2 r0 r0 23 16 | #211 new yes b 1 1 | sum{#200+#202+#206+#210}
124 shfl r5 r2 r1 15 16 | #212 new yes b 1 1 | xor{#136 ^ #198^l16 ^ #202^l16 ^ #211^l16}
125 sub r7 r6 r2 15 2 | #213 new yes b 1 1 | sum{#130+#135+#137+#145+#147+#149+#159+#180+-#203}
126 add r0 r3 r4 24 1 | #215 new yes b 1 1 | sum{#202+#206+#209+#214}
127 rotr r1 r5 r1 30 1 | #216 new yes a 1 1 | rotr(#191, 1*#212)
128 mulhi r1 r0 r2 9 4 | #218 new yes a 0 1 | hi(#217)
129 mul r2 r4 r0 12 2 | #220 new yes a 0 1 | lo(#219)
130 rotl r4 r1 r2 9 1 | #221 new yes a 1 0 | xor{#186<<9}
131 mad r3 r7 r0 8 8 | #224 new yes b 1 1 | sum{#209+#223}
132 add r0 r1 r1 14 8 | #226 new yes b 1 1 | sum{#202+#206+#209+#214+#218+#225}
133 shfl r2 r0 r6 17 16 | #227 new yes b 1 1 | xor{#220 ^ #226^l16}
134 rotl r0 r3 r0 6 1 | #228 new yes a 1 0 | xor{#226<<6}
135 shfl r6 r0 r2 21 8 | #229 new yes b 1 1 | xor{#198 ^ #202 ^ #226<<6^l8}
136 mul r4 r0 r0 27 1 | #231 new yes a 0 1 | lo(#230)
137 xor r2 r4 r3 31 4 | #232 new yes b 1 1 | xor{#220 ^ #226^l16 ^ #231}
138 add r3 r7 r6 21 2 | #234 new yes b 1 1 | sum{#130+#135+#137+#145+#147+#149+#159+#180+-#203+#209+#223+#233}
139 rotr r3 r2 r4 9 16 | #235 new yes a 1 1 | rotr(#234, 1*#232)
140 rotl r3 r4 r5 19 1 | #236 new yes a 1 0 | xor{#235<<19}
141 mul r5 r7 r6 24 1 | #238 new yes a 0 1 | lo(#237)
142 shfl r7 r6 r3 10 16 | #239 new yes b 1 1 | xor{#198^l16 ^ #202^l16 ^ #213 ^ #226<<6^l24}
143 mad r5 r3 r1 16 8 | #242 new yes b 1 1 | sum{#238+#241}
144 mul r4 r7 r7 22 1 | #244 new yes a 0 1 | lo(#243)
145 rotl r1 r6 r1 2 16 | #245 new yes a 1 0 | xor{#218<<2}
146 add r5 r4 r0 6 16 | #247 new yes b 1 1 | sum{#238+#241+#244+#246}
147 or r3 r0 r0 17 16 | #248 new yes a 1 1 | or{#228|#236}
148 shfl r7 r5 r2 31 2 | #249 new yes b 1 1 | xor{#198^l16 ^ #202^l16 ^ #213 ^ #226<<6^l24 ^ #247^l2}
149 shfl r4 r3 r2 13 4 | #250 new yes b 1 1 | xor{#244 ^ #248^l4}
150 rotl r3 r5 r4 19 8 | #251 new yes a 1 0 | xor{#248<<19}
151 rotr r4 r0 r3 5 2 | #252 new yes a 1 1 | rotr(#250, 1*#228)
152 mul r7 r0 r0 4 8 | #254 new yes a 0 1 | lo(#253)
153 *rotl r7 r1 r3 17 16 | #255 new no - 0 0 | xor{#254<<17}
154 mulhi r3 r0 r3 22 2 | #257 new yes a 0 1 | hi(#256)
155 or r2 r5 r4 8 1 | #258 new yes a 1 1 | or{#232|#247}
156 *rotl r7 r3 r4 1 4 | #259 new no - 0 0 | xor{#254<<18}
157 xor r2 r1 r1 14 8 | #260 new yes a 1 1 | xor{#218<<2 ^ #258}
158 rotl r7 r1 r2 24 16 | #261 new yes a 1 0 | xor{#254<<10}
159 mulhi r1 r0 r4 13 1 | #263 new yes a 0 1 | hi(#262)
160 mul r7 r2 r1 9 8 | #265 new yes a 0 1 | lo(#264)
161 rotl r2 r5 r5 31 16 | #266 new yes b 1 0 | xor{#218<<1 ^ #258<<31}
162 mul r0 r4 r3 9 16 | #268 new yes a 0 1 | lo(#267)
163 xor r1 r7 r5 22 1 | #269 new yes a 1 1 | xor{#263 ^ #265}
164 xor r0 r2 r7 21 2 | #270 new yes b 1 1 | xor{#218<<1 ^ #258<<31 ^ #268}
165 sub r2 r5 r0 12 1 | #271 new yes b 1 1 | sum{-#238+-#241+-#244+-#246+#266}
166 mul r2 r4 r3 19 1 | #273 new yes a 0 1 | lo(#272)
167 mad r3 r7 r6 8 2 | #276 new yes b 1 1 | sum{#257+#275}
168 shfl r4 r6 r0 7 4 | #277 new yes b 1 1 | xor{#198^l4 ^ #202^l4 ^ #226<<6^l12 ^ #252}
169 rotr r6 r7 r0 7 2 | #278 new yes a 1 1 | rotr(#229, 1*#265)
170 mul r6 r7 r6 7 16 | #280 new yes a 0 1 | lo(#279)
171 mul r0 r4 r0 11 16 | #282 new yes a 0 1 | lo(#281)
172 shfl r0 r4 r2 26 4 | #283 new yes b 1 1 | xor{#198 ^ #202 ^ #226<<6^l8 ^ #252^l4 ^ #282}
173 rotl r6 r3 r5 14 16 | #284 new yes a 1 0 | xor{#280<<14}
174 mul r1 r2 r0 5 2 | #286 new yes a 0 1 | lo(#285)
175 add r4 r7 r4 13 2 | #288 new yes a 1 1 | sum{#265+#277+#287}
176 rotl r5 r3 r5 24 4 | #289 new yes a 1 0 | xor{#247<<24}
177 xor r6 r3 r0 20 16 | #290 new yes a 1 1 | xor{#276 ^ #280<<14}
178 mul r7 r0 r5 2 8 | #292 new yes a 0 1 | lo(#291)
179 sub r4 r0 r6 30 16 | #293 new yes b 1 1 | sum{#265+#277+-#283+#287}
180 mul r3 r1 r4 12 4 | #295 new yes a 0 1 | lo(#294)
181 shfl r7 r6 r2 31 4 | #296 new yes b 1 1 | xor{#276^l4 ^ #280<<14^l4 ^ #292}
182 add r1 r2 r4 3 2 | #298 new yes a 1 1 | sum{#273+#286+#297}
183 add r6 r0 r6 3 2 | #300 new yes a 1 1 | sum{#283+#290+#299}
184 add r1 r5 r4 13 16 | #302 new yes b 1 1 | sum{#273+#286+#289+#297+#301}
185 *rotl r4 r3 r1 3 1 | #303 new no - 0 0 | xor{#293<<3}
186 rotl r1 r5 r5 1 2 | #304 new yes a 1 0 | xor{#302<<1}
187 rotl r3 r7 r5 13 8 | #305 new yes a 1 0 | xor{#295<<13}
188 shfl r1 r3 r4 7 16 | #306 new yes b 1 1 | xor{#295<<13^l16 ^ #302<<1}
189 mad r7 r6 r7 18 1 | #309 new yes a 1 1 | sum{#296+#308}
190 rotl r4 r2 r0 20 2 | #310 new yes a 1 0 | xor{#293<<23}
191 sub r0 r6 r7 15 1 | #311 new yes a 1 1 | sum{-#290+-#299}
192 mulhi r1 r6 r6 2 16 | #313 new yes a 0 1 | hi(#312)
193 shfl r1 r3 r5 4 1 | #314 new yes b 1 1 | xor{#295<<13^l1 ^ #313}
194 rotr r4 r2 r5 6 2 | #315 new yes a 1 1 | rotr(#310, 1*#273)
195 rotl r7 r3 r0 21 4 | #316 new yes a 1 0 | xor{#309<<21}
196 rotl r2 r5 r2 10 8 | #317 new yes a 1 0 | xor{#273<<10}
197 rotl r1 r5 r6 8 2 | #318 new yes b 1 0 | xor{#295<<21^l1 ^ #313<<8}
198 or r4 r7 r6 13 4 | #319 new yes a 1 1 | or{#315|#316}
199 mulhi r6 r2 r3 23 8 | #321 new yes a 0 1 | hi(#320)
200 mul r1 r7 r7 1 8 | #323 new yes a 0 1 | lo(#322)
201 or r7 r5 r6 8 2 | #324 new yes a 1 1 | or{#289|#316}
202 mad r6 r4 r5 23 4 | #327 new yes b 1 1 | sum{#321+#326}
203 shfl r0 r1 r5 15 16 | #328 new yes b 1 1 | xor{#311 ^ #323^l16}
204 add r0 r7 r7 18 16 | #330 new yes a 1 1 | sum{#324+#328+#329}
205 rotr r3 r7 r2 22 16 | #331 new yes a 1 1 | rotr(#305, 1*#324)
206 rotr r2 r0 r3 23 1 | #332 new yes a 1 1 | rotr(#317, 1*#330)
207 mul r5 r2 r4 7 1 | #334 new yes a 0 1 | lo(#333)
208 sub r4 r2 r7 21 8 | #335 new yes a 1 1 | sum{#319+-#332}
209 add r2 r0 r2 23 16 | #337 new yes b 1 1 | sum{#324+#328+#329+#332+#336}
210 shfl r5 r4 r6 10 2 | #338 new yes b 1 1 | xor{#334 ^ #335^l2}
211 mad r1 r3 r6 29 1 | #341 new yes b 1 1 | sum{#323+#340}
212 *shfl r1 r6 r4 26 4 | #342 new no - 0 1 | xor{#327^l4 ^ #341}
213 rotl r1 r2 r7 31 4 | #343 new yes a 1 0 | xor{#327<<31^l4 ^ #341<<31}
214 xor r2 r1 r2 4 2 | #344 new yes b 1 1 | xor{#327<<31^l4 ^ #337 ^ #341<<31}
215 add r2 r1 r3 10 16 | #346 new yes a 1 1 | sum{#343+#344+#345}
216 xor r3 r7 r0 14 2 | #347 new yes a 1 1 | xor{#324 ^ #331}
217 sub r7 r4 r5 23 1 | #348 new yes b 1 1 | sum{-#319+#324+#332}
218 rotl r0 r5 r4 10 8 | #349 new yes a 1 0 | xor{#330<<10}
219 rotr r6 r7 r6 5 4 | #350 new yes a 1 1 | rotr(#327, 1*#348)
220 add r0 r5 r6 29 2 | #352 new yes a 1 1 | sum{#338+#349+#351}
221 shfl r6 r4 r1 26 4 | #353 new yes b 1 1 | xor{#335^l4 ^ #350}
222 *rotl r7 r5 r1 13 8 | #354 new no - 0 0 | xor{#348<<13}
223 rotl r7 r0 r4 7 4 | #355 new yes a 1 0 | xor{#348<<20}
224 sub r0 r1 r5 4 16 | #356 new yes b 1 1 | sum{#338+-#343+#349+#351}
225 rotl r4 r3 r2 18 2 | #357 new yes a 1 0 | xor{#335<<18}
226 or r3 r1 r3 14 16 | #358 new yes a 1 1 | or{#343|#347}
227 mad r6 r2 r6 20 1 | #361 new yes a 1 1 | sum{#353+#360}
228 shfl r0 r1 r0 8 1 | #362 new yes b 1 1 | xor{#327<<31^l5 ^ #341<<31^l1 ^ #356}
229 or r4 r0 r4 7 2 | #363 new yes a 1 1 | or{#357|#362}
230 xor r4 r1 r0 25 4 | #364 new yes b 1 1 | xor{#327<<31^l4 ^ #341<<31 ^ #363}
231 add r6 r7 r0 27 4 | #366 new yes b 1 1 | sum{#353+#355+#360+#365}
232 add r0 r6 r3 12 1 | #368 new yes b 1 1 | sum{#353+#355+#360+#362+#365+#367}
233 mad r2 r3 r4 8 4 | #371 new yes b 1 1 | sum{#343+#344+#345+#370}
234 xor r1 r0 r7 12 4 | #372 new yes b 1 1 | xor{#327<<31^l4 ^ #341<<31 ^ #368}
235 xor r3 r7 r0 8 16 | #373 new yes a 1 1 | xor{#348<<20 ^ #358}
236 add r7 r4 r0 22 4 | #375 new yes a 1 1 | sum{#355+#364+#374}
237 add r5 r3 r4 24 4 | #377 new yes a 1 1 | sum{#338+#373+#376}
238 mad r3 r0 r2 14 1 | #380 new yes b 1 1 | sum{#373+#379}
239 add r1 r0 r3 12 4 | #382 new yes b 1 1 | sum{#353+#355+#360+#362+#365+#367+#372+#381}
240 add r2 r5 r7 17 1 | #384 new yes b 1 1 | sum{#338+#343+#344+#345+#370+#373+#376+#383}
241 rotr r2 r5 r5 16 8 | #385 new yes a 1 1 | rotr(#384, 1*#377)
242 mad r2 r4 r7 30 4 | #388 new yes b 1 1 | sum{#385+#387}
243 sub r2 r0 r7 10 1 | #389 new yes b 1 1 | sum{-#353+-#355+-#360+-#362+-#365+-#367+#385+#387}
244 mulhi r1 r4 r0 30 8 | #391 new yes a 0 1 | hi(#390)
245 sub r5 r6 r7 10 1 | #392 new yes b 1 1 | sum{#338+-#353+-#355+-#360+-#365+#373+#376}
246 mul r0 r4 r0 14 2 | #394 new yes a 0 1 | lo(#393)
247 xor r1 r6 r5 29 4 | #395 new yes a 1 1 | xor{#366 ^ #391}
248 xor r3 r4 r1 16 1 | #396 new yes b 1 1 | xor{#327<<31^l4 ^ #341<<31 ^ #363 ^ #380}
249 mad r5 r1 r1 27 4 | #399 new yes b 1 1 | sum{#338+-#353+-#355+-#360+-#365+#373+#376+#398}
250 xor r3 r6 r0 8 16 | #400 new yes b 1 1 | xor{#327<<31^l4 ^ #341<<31 ^ #363 ^ #366 ^ #380}
251 mad r1 r4 r3 13 16 | #403 new yes b 1 1 | sum{#395+#402}
252 rotl r7 r0 r3 18 2 | #404 new yes a 1 0 | xor{#375<<18}
253 sub r5 r7 r6 31 16 | #405 new yes b 1 1 | sum{#338+-#353+-#355+-#360+-#365+#373+#376+#398+-#404}
254 rotl r1 r3 r5 27 16 | #406 new yes a 1 0 | xor{#403<<27}
255 shfl r2 r4 r4 5 16 | #407 new yes b 1 1 | xor{#327<<31^l20 ^ #341<<31^l16 ^ #363^l16 ^ #389}
lines flagged * (result is an existing node, or a node the realisation does not need): 9
extra needed node #8 cost 1 : mul64(#3,#7)
extra needed node #11 cost 1 : mul64(#6,#9)
extra needed node #17 cost 1 : mul64(#9,#16)
extra needed node #20 cost 1 : mul64(#0,#18)
extra needed node #25 cost 1 : mul64(#1,#24)
extra needed node #29 cost 1 : mul64(#9,#27)
extra needed node #35 cost 1 : mul64(#28,#34)
extra needed node #37 cost 1 : mul64(#21,#33)
extra needed node #41 cost 1 : mul64(#26,#40)
extra needed node #49 cost 1 : mul64(#26,#39)
extra needed node #63 cost 1 : mul64(#47,#52)
extra needed node #65 cost 1 : mul64(#50,#56)
extra needed node #67 cost 1 : mul64(#33,#47)
extra needed node #76 cost 1 : mul64(#64,#68)
extra needed node #84 cost 1 : mul64(#62,#77)
extra needed node #90 cost 1 : mul64(#75,#77)
extra needed node #96 cost 1 : mul64(#74,#89)
extra needed node #99 cost 1 : mul64(#92,#98)
extra needed node #101 cost 1 : mul64(#86,#100)
extra needed node #115 cost 1 : mul64(#95,#113)
extra needed node #119 cost 1 : mul64(#104,#118)
extra needed node #121 cost 1 : mul64(#100,#118)
extra needed node #124 cost 1 : mul64(#95,#123)
extra needed node #143 cost 1 : mul64(#117,#122)
extra needed node #153 cost 1 : mul64(#148,#152)
extra needed node #163 cost 1 : mul64(#136,#139)
extra needed node #170 cost 1 : mul64(#154,#167)
extra needed node #172 cost 1 : mul64(#160,#171)
extra needed node #174 cost 1 : mul64(#164,#167)
extra needed node #183 cost 1 : mul64(#175,#176)
extra needed node #197 cost 1 : mul64(#136,#182)
extra needed node #199 cost 1 : mul64(#136,#185)
extra needed node #201 cost 1 : mul64(#167,#196)
extra needed node #208 cost 1 : mul64(#167,#204)
extra needed node #217 cost 1 : mul64(#215,#216)
extra needed node #219 cost 1 : mul64(#186,#211)
extra needed node #230 cost 1 : mul64(#221,#228)
extra needed node #237 cost 1 : mul64(#212,#213)
extra needed node #243 cost 1 : mul64(#231,#239)
extra needed node #253 cost 1 : mul64(#228,#249)
extra needed node #256 cost 1 : mul64(#228,#251)
extra needed node #262 cost 1 : mul64(#228,#245)
extra needed node #264 cost 1 : mul64(#260,#261)
extra needed node #267 cost 1 : mul64(#228,#252)
extra needed node #272 cost 1 : mul64(#252,#271)
extra needed node #279 cost 1 : mul64(#265,#278)
extra needed node #281 cost 1 : mul64(#270,#277)
extra needed node #285 cost 1 : mul64(#269,#273)
extra needed node #291 cost 1 : mul64(#265,#283)
extra needed node #294 cost 1 : mul64(#276,#286)
extra needed node #307 cost 1 : mul64(#296,#300)
extra needed node #312 cost 1 : mul64(#300,#306)
extra needed node #320 cost 1 : mul64(#300,#317)
extra needed node #322 cost 1 : mul64(#316,#318)
extra needed node #333 cost 1 : mul64(#289,#332)
extra needed node #359 cost 1 : mul64(#346,#353)
extra needed node #390 cost 1 : mul64(#364,#382)
extra needed node #393 cost 1 : mul64(#364,#368)
extra needed node #451 cost 2 : xor{#327<<31^l4}
extra needed node #452 cost 1 : xor{#341<<31}

View file

@ -0,0 +1,4 @@
0 9497
1 472
2 30
3 1

View file

@ -0,0 +1,11 @@
start 2026-10-07T08:31:50Z
attack-f1 census: 10000 programs (attack-f1/{0..9999}), 12 threads, 101.7 s
naive per iteration: 6912 instructions (55296 per hash), 13338 counted ops (106704 per hash, the 1.83 convention), chip view 6129 ops
instructions saved: min 0.000% mean 0.647% max 5.859% (worst seed attack-f1/8948 idx 8948: 6912 -> 6507)
chip-view ops saved beyond free rotates and hoisted constants: mean 0.666% max 8.535%
programs over 5%: 2; over 10%: 0; gate (every program within 5%, none over 10%): FAIL
soundness: differential mismatches 0 of 10000 (8 random states each); verifier mismatches 0 of 10000
dead (never-read) derived nodes under the full fold: 380292
rewrites over all programs and 27 passes: identity 327111 xor-cancel 307665 sum-cancel 1086616 or-idem 31245 rotl-merge 442292 rotr-merge 31862 product-shared 232157
histogram of instructions saved, 0.5% bins from 0: [5355, 2132, 1186, 1040, 160, 72, 33, 11, 3, 6, 1, 1] (last bin = 5.5% and over)
census-exit 0 2026-10-07T08:33:32Z

View file

@ -0,0 +1,11 @@
start 2026-10-07T09:31:09Z
attack-f1 census: 10000 programs (attack-f1/{0..9999}), 12 threads, 98.5 s
naive per iteration: 6912 instructions (55296 per hash), 13338 counted ops (106704 per hash, the 1.83 convention), chip view 6129 ops
instructions saved: min 0.000% mean 0.627% max 4.688% (worst seed attack-f1/8556 idx 8556: 6912 -> 6588)
chip-view ops saved beyond free rotates and hoisted constants: mean 0.524% max 4.348%
programs over 5%: 0; over 10%: 0; gate (every program within 5%, none over 10%): PASS
soundness: differential mismatches 0 of 10000 (8 random states each); verifier mismatches 0 of 10000
dead (never-read) derived nodes under the full fold: 352927
rewrites over all programs and 27 passes: identity 327111 xor-cancel 307665 sum-cancel 1086616 or-idem 31245 rotl-merge 442292 rotr-merge 31862 product-shared 232157
histogram of instructions saved, 0.5% bins from 0: [5445, 2119, 1198, 993, 147, 58, 28, 8, 2, 2, 0, 0] (last bin = 5.5% and over)
census-exit 0 2026-10-07T09:32:48Z

View file

@ -0,0 +1,19 @@
Wed Oct 7 09:31:09 AM UTC 2026
idx,seed,attempt,naive_instrs,cost_instrs,save_instrs_pct,naive_chip,cost_chip,save_chip_pct,naive_counted_ops,nodes,needed,unneeded_derived,option_a,consts,rw_identity,rw_xor_cancel,rw_sum_cancel,rw_or_idem,rw_rotl_merge,rw_rotr_merge,rw_mul_shared,difftest,verify
real,0,attack-f1/0,0,6912,6911,0.0145,6129,6129,0.0000,13338,9478,8182,82,5376,47,0,54,135,0,108,0,0,ok,ok
planted-compressible,0,attack-f1/0,0,6912,5534,19.9363,5778,4752,17.7570,12798,7800,6426,811,4350,43,648,431,270,135,302,0,0,ok,ok
planted-interrupted,0,attack-f1/0,0,6912,6101,11.7332,5994,5265,12.1622,12258,7446,6818,183,3928,40,702,810,243,108,167,0,0,ok,ok
planted-modified,0,attack-f1/0,0,6912,6858,0.7812,5130,5076,1.0526,12258,7959,6866,27,0,40,27,351,135,0,219,0,0,ok,ok
broken-rotl-rule,0,attack-f1/0,0,6912,6884,0.4051,6129,6129,0.0000,13338,9451,8155,82,5349,47,27,54,135,0,108,0,0,FAIL,skip
dead-tail-fold-7-regs-1-rep,0,attack-f1/0,0,256,254,0.7812,226,226,0.0000,494,404,329,5,227,47,0,2,5,0,4,0,0,ok,skip
dead-tail-fold-8-regs-1-rep,0,attack-f1/0,0,256,255,0.3906,226,226,0.0000,494,404,330,4,227,47,0,2,5,0,4,0,0,ok,skip
FIRINGS
known-pass (real block): saved 0.014% instructions, difftest true, verify true
known-fail (planted 50/256): saved 19.936% instructions (expected about 19.5% + the real block's own), difftest true, verify true
must-not-fire (planted, source rotated between): saved 0.781% instructions (expected about the real block's own), difftest true, verify true
information (planted, read between): saved 11.733% instructions (the restore shortcut: xor, or and shfl pairs restore a held value), difftest true
soundness firing (rotl rule broken on purpose): difftest MISMATCH (MISMATCH expected)
dead-code pass: last instruction rotl r7, 1 rep: fold over 7 registers costs 254 and leaves 5 derived nodes unneeded; over 8 registers 255 and 4 (one more needed, one fewer unneeded expected)
plant verdict: ALL FIRINGS AS EXPECTED
plant-exit 0

View file

@ -0,0 +1,9 @@
attack-f1 census: 10000 programs (attack-f1/{0..9999}), 12 threads, 98.5 s
naive per iteration: 6912 instructions (55296 per hash), 13338 counted ops (106704 per hash, the 1.83 convention), chip view 6129 ops
instructions saved: min 0.000% mean 0.627% max 4.688% (worst seed attack-f1/8556 idx 8556: 6912 -> 6588)
chip-view ops saved beyond free rotates and hoisted constants: mean 0.524% max 4.348%
programs over 5%: 0; over 10%: 0; gate (every program within 5%, none over 10%): PASS
soundness: differential mismatches 0 of 10000 (8 random states each); verifier mismatches 0 of 10000
dead (never-read) derived nodes under the full fold: 352927
rewrites over all programs and 27 passes: identity 327111 xor-cancel 307665 sum-cancel 1086616 or-idem 31245 rotl-merge 442292 rotr-merge 31862 product-shared 232157
histogram of instructions saved, 0.5% bins from 0: [5445, 2119, 1198, 993, 147, 58, 28, 8, 2, 2, 0, 0] (last bin = 5.5% and over)

View file

@ -0,0 +1,51 @@
idx,seed,attempt,naive_instrs,cost_instrs,save_instrs_pct,naive_chip,cost_chip,save_chip_pct,naive_counted_ops,nodes,needed,unneeded_derived,option_a,consts,rw_identity,rw_xor_cancel,rw_sum_cancel,rw_or_idem,rw_rotl_merge,rw_rotr_merge,rw_mul_shared,difftest,verify
8556,attack-f1/8556,1,6912,6588,4.6875,6372,6102,4.2373,11556,9758,8044,244,5346,30,135,135,81,0,54,0,27,ok,ok
4259,attack-f1/4259,0,6912,6588,4.6875,6021,5778,4.0359,11529,9299,8073,189,5913,30,162,216,27,0,135,0,0,ok,ok
1206,attack-f1/1206,0,6912,6615,4.2969,6318,6102,3.4188,12690,9014,7773,190,4834,42,108,108,240,0,81,0,0,ok,ok
3491,attack-f1/3491,0,6912,6616,4.2824,6075,5833,3.9835,12339,9391,6651,0,0,41,323,54,108,27,0,0,27,ok,ok
6812,attack-f1/6812,0,6912,6641,3.9207,6102,5966,2.2288,11988,9660,8261,135,5642,40,160,54,243,27,135,0,26,ok,ok
8087,attack-f1/8087,0,6912,6642,3.9062,6210,5940,4.3478,12447,9553,7909,110,4832,41,215,135,189,27,27,0,0,ok,ok
7292,attack-f1/7292,0,6912,6643,3.8918,6291,6075,3.4335,13797,9215,8024,139,5088,54,189,189,189,0,81,0,27,ok,ok
9667,attack-f1/9667,0,6912,6669,3.5156,6399,6183,3.3755,12852,9662,8100,136,5081,42,108,108,80,0,27,0,0,ok,ok
6136,attack-f1/6136,0,6912,6669,3.5156,6345,6156,2.9787,12312,9658,6677,108,0,38,135,135,54,27,54,27,54,ok,ok
5764,attack-f1/5764,0,6912,6669,3.5156,6102,5994,1.7699,12015,9087,7910,246,4893,34,54,54,54,0,243,0,0,ok,ok
1125,attack-f1/1125,0,6912,6669,3.5156,6291,6102,3.0043,13122,9801,7969,189,5136,46,54,54,189,0,81,0,0,ok,ok
9809,attack-f1/9809,0,6912,6670,3.5012,6021,5913,1.7937,12771,8877,7914,190,5190,40,108,54,27,27,135,0,27,ok,ok
1870,attack-f1/1870,0,6912,6671,3.4867,6264,6050,3.4163,12069,8900,7833,135,5001,36,135,188,108,0,54,0,0,ok,ok
990,attack-f1/990,0,6912,6696,3.1250,6237,6075,2.5974,12366,9793,8179,135,5047,38,81,81,108,0,81,0,0,ok,ok
9688,attack-f1/9688,0,6912,6696,3.1250,6210,5994,3.4783,12096,9628,8266,190,5463,35,135,81,135,0,54,0,54,ok,ok
9661,attack-f1/9661,0,6912,6696,3.1250,6345,6156,2.9787,12663,9661,8045,136,5051,41,81,81,81,0,80,0,27,ok,ok
9604,attack-f1/9604,1,6912,6696,3.1250,6291,6129,2.5751,12501,9552,7932,138,4857,40,81,108,189,0,54,0,54,ok,ok
9432,attack-f1/9432,0,6912,6696,3.1250,6345,6129,3.4043,12231,9415,6704,81,0,38,135,108,108,27,54,0,0,ok,ok
8962,attack-f1/8962,0,6912,6696,3.1250,6237,6102,2.1645,13770,9293,7888,190,4896,51,27,27,162,0,108,27,0,ok,ok
8309,attack-f1/8309,0,6912,6696,3.1250,6318,6102,3.4188,12393,9035,6704,54,0,36,162,135,54,27,27,0,0,ok,ok
7054,attack-f1/7054,0,6912,6696,3.1250,6372,6210,2.5424,12906,9607,6704,81,0,41,135,108,54,0,54,0,0,ok,ok
6586,attack-f1/6586,0,6912,6696,3.1250,6021,5832,3.1390,12663,9419,8025,135,5650,42,81,108,54,0,54,0,0,ok,ok
5679,attack-f1/5679,0,6912,6696,3.1250,6399,6264,2.1097,11988,9927,8236,162,5591,37,108,108,162,0,54,0,27,ok,ok
5241,attack-f1/5241,0,6912,6696,3.1250,6291,6129,2.5751,13905,8996,6758,108,0,51,162,135,216,0,0,27,54,ok,ok
4895,attack-f1/4895,0,6912,6696,3.1250,6345,6183,2.5532,13311,9503,7862,162,4623,45,54,81,108,0,54,0,0,ok,ok
4814,attack-f1/4814,0,6912,6696,3.1250,6318,6102,3.4188,12258,9605,7990,190,4914,39,81,81,189,0,0,0,27,ok,ok
4650,attack-f1/4650,0,6912,6696,3.1250,6129,5994,2.2026,13014,9154,7934,163,4858,47,108,108,135,27,108,0,54,ok,ok
3402,attack-f1/3402,0,6912,6696,3.1250,6318,6210,1.7094,12879,9364,7962,189,4885,41,27,54,108,0,108,0,27,ok,ok
332,attack-f1/332,0,6912,6696,3.1250,6264,6102,2.5862,12690,9716,7883,135,4942,42,81,81,27,27,107,27,27,ok,ok
3070,attack-f1/3070,0,6912,6696,3.1250,6453,6237,3.3473,11664,9649,6704,108,0,29,108,108,54,0,27,0,54,ok,ok
3063,attack-f1/3063,0,6912,6696,3.1250,6156,6048,1.7544,11772,9681,8200,162,5312,34,81,81,108,0,134,27,54,ok,ok
2657,attack-f1/2657,0,6912,6696,3.1250,5994,5886,1.8018,12069,9009,7691,163,4483,37,108,54,135,0,108,0,54,ok,ok
252,attack-f1/252,0,6912,6696,3.1250,6264,6156,1.7241,12447,9470,8023,135,5297,39,108,81,54,0,135,0,27,ok,ok
1721,attack-f1/1721,0,6912,6696,3.1250,6183,6021,2.6201,12474,9471,8157,162,5782,40,108,81,135,0,54,0,54,ok,ok
5538,attack-f1/5538,0,6912,6697,3.1105,6156,5967,3.0702,13446,9024,8019,164,5162,52,135,81,270,0,81,0,27,ok,ok
3757,attack-f1/3757,0,6912,6697,3.1105,6237,6130,1.7156,13176,9344,8005,188,5469,48,81,54,269,0,162,0,27,ok,ok
1769,attack-f1/1769,0,6912,6697,3.1105,6156,5941,3.4925,13095,9748,8207,161,5240,47,108,108,107,0,0,0,27,ok,ok
112,attack-f1/112,0,6912,6697,3.1105,6291,6184,1.7008,12933,9556,8286,164,5534,43,107,27,53,0,108,27,108,ok,ok
4815,attack-f1/4815,0,6912,6699,3.0816,6048,5967,1.3393,11772,9273,8128,217,5809,31,108,54,81,27,162,0,27,ok,ok
8010,attack-f1/8010,0,6912,6700,3.0671,6102,5942,2.6221,12231,9227,7915,162,4973,38,134,80,135,0,54,0,81,ok,ok
2943,attack-f1/2943,0,6912,6721,2.7633,6345,6156,2.9787,12798,9177,7906,163,5072,43,108,108,108,0,28,27,0,ok,ok
7832,attack-f1/7832,0,6912,6722,2.7488,6237,6155,1.3147,13203,8936,7827,108,4751,45,108,81,162,0,81,0,0,ok,ok
7597,attack-f1/7597,0,6912,6722,2.7488,6264,6128,2.1711,13581,8532,7750,218,4677,46,81,81,108,27,81,0,27,ok,ok
6301,attack-f1/6301,0,6912,6722,2.7488,6183,5993,3.0729,12501,9554,7962,111,4914,42,108,135,162,0,27,0,27,ok,ok
2120,attack-f1/2120,0,6912,6722,2.7488,6237,6182,0.8818,13419,9345,8180,216,5372,49,54,0,270,0,81,27,27,ok,ok
1938,attack-f1/1938,0,6912,6722,2.7488,6426,6317,1.6962,12852,10123,8149,164,5018,44,81,54,108,0,54,0,27,ok,ok
1852,attack-f1/1852,0,6912,6722,2.7488,6480,6290,2.9321,13176,9775,8209,136,5431,47,108,81,162,27,0,0,0,ok,ok
9869,attack-f1/9869,0,6912,6723,2.7344,6129,5940,3.0837,12366,9443,8157,81,5379,39,135,54,54,54,54,27,54,ok,ok
9755,attack-f1/9755,0,6912,6723,2.7344,6561,6372,2.8807,12636,10012,8126,135,5184,41,108,135,162,0,0,0,0,ok,ok
9707,attack-f1/9707,0,6912,6723,2.7344,6210,6048,2.6087,12123,10006,7962,137,4887,35,162,108,54,0,81,0,81,ok,ok
1 idx seed attempt naive_instrs cost_instrs save_instrs_pct naive_chip cost_chip save_chip_pct naive_counted_ops nodes needed unneeded_derived option_a consts rw_identity rw_xor_cancel rw_sum_cancel rw_or_idem rw_rotl_merge rw_rotr_merge rw_mul_shared difftest verify
2 8556 attack-f1/8556 1 6912 6588 4.6875 6372 6102 4.2373 11556 9758 8044 244 5346 30 135 135 81 0 54 0 27 ok ok
3 4259 attack-f1/4259 0 6912 6588 4.6875 6021 5778 4.0359 11529 9299 8073 189 5913 30 162 216 27 0 135 0 0 ok ok
4 1206 attack-f1/1206 0 6912 6615 4.2969 6318 6102 3.4188 12690 9014 7773 190 4834 42 108 108 240 0 81 0 0 ok ok
5 3491 attack-f1/3491 0 6912 6616 4.2824 6075 5833 3.9835 12339 9391 6651 0 0 41 323 54 108 27 0 0 27 ok ok
6 6812 attack-f1/6812 0 6912 6641 3.9207 6102 5966 2.2288 11988 9660 8261 135 5642 40 160 54 243 27 135 0 26 ok ok
7 8087 attack-f1/8087 0 6912 6642 3.9062 6210 5940 4.3478 12447 9553 7909 110 4832 41 215 135 189 27 27 0 0 ok ok
8 7292 attack-f1/7292 0 6912 6643 3.8918 6291 6075 3.4335 13797 9215 8024 139 5088 54 189 189 189 0 81 0 27 ok ok
9 9667 attack-f1/9667 0 6912 6669 3.5156 6399 6183 3.3755 12852 9662 8100 136 5081 42 108 108 80 0 27 0 0 ok ok
10 6136 attack-f1/6136 0 6912 6669 3.5156 6345 6156 2.9787 12312 9658 6677 108 0 38 135 135 54 27 54 27 54 ok ok
11 5764 attack-f1/5764 0 6912 6669 3.5156 6102 5994 1.7699 12015 9087 7910 246 4893 34 54 54 54 0 243 0 0 ok ok
12 1125 attack-f1/1125 0 6912 6669 3.5156 6291 6102 3.0043 13122 9801 7969 189 5136 46 54 54 189 0 81 0 0 ok ok
13 9809 attack-f1/9809 0 6912 6670 3.5012 6021 5913 1.7937 12771 8877 7914 190 5190 40 108 54 27 27 135 0 27 ok ok
14 1870 attack-f1/1870 0 6912 6671 3.4867 6264 6050 3.4163 12069 8900 7833 135 5001 36 135 188 108 0 54 0 0 ok ok
15 990 attack-f1/990 0 6912 6696 3.1250 6237 6075 2.5974 12366 9793 8179 135 5047 38 81 81 108 0 81 0 0 ok ok
16 9688 attack-f1/9688 0 6912 6696 3.1250 6210 5994 3.4783 12096 9628 8266 190 5463 35 135 81 135 0 54 0 54 ok ok
17 9661 attack-f1/9661 0 6912 6696 3.1250 6345 6156 2.9787 12663 9661 8045 136 5051 41 81 81 81 0 80 0 27 ok ok
18 9604 attack-f1/9604 1 6912 6696 3.1250 6291 6129 2.5751 12501 9552 7932 138 4857 40 81 108 189 0 54 0 54 ok ok
19 9432 attack-f1/9432 0 6912 6696 3.1250 6345 6129 3.4043 12231 9415 6704 81 0 38 135 108 108 27 54 0 0 ok ok
20 8962 attack-f1/8962 0 6912 6696 3.1250 6237 6102 2.1645 13770 9293 7888 190 4896 51 27 27 162 0 108 27 0 ok ok
21 8309 attack-f1/8309 0 6912 6696 3.1250 6318 6102 3.4188 12393 9035 6704 54 0 36 162 135 54 27 27 0 0 ok ok
22 7054 attack-f1/7054 0 6912 6696 3.1250 6372 6210 2.5424 12906 9607 6704 81 0 41 135 108 54 0 54 0 0 ok ok
23 6586 attack-f1/6586 0 6912 6696 3.1250 6021 5832 3.1390 12663 9419 8025 135 5650 42 81 108 54 0 54 0 0 ok ok
24 5679 attack-f1/5679 0 6912 6696 3.1250 6399 6264 2.1097 11988 9927 8236 162 5591 37 108 108 162 0 54 0 27 ok ok
25 5241 attack-f1/5241 0 6912 6696 3.1250 6291 6129 2.5751 13905 8996 6758 108 0 51 162 135 216 0 0 27 54 ok ok
26 4895 attack-f1/4895 0 6912 6696 3.1250 6345 6183 2.5532 13311 9503 7862 162 4623 45 54 81 108 0 54 0 0 ok ok
27 4814 attack-f1/4814 0 6912 6696 3.1250 6318 6102 3.4188 12258 9605 7990 190 4914 39 81 81 189 0 0 0 27 ok ok
28 4650 attack-f1/4650 0 6912 6696 3.1250 6129 5994 2.2026 13014 9154 7934 163 4858 47 108 108 135 27 108 0 54 ok ok
29 3402 attack-f1/3402 0 6912 6696 3.1250 6318 6210 1.7094 12879 9364 7962 189 4885 41 27 54 108 0 108 0 27 ok ok
30 332 attack-f1/332 0 6912 6696 3.1250 6264 6102 2.5862 12690 9716 7883 135 4942 42 81 81 27 27 107 27 27 ok ok
31 3070 attack-f1/3070 0 6912 6696 3.1250 6453 6237 3.3473 11664 9649 6704 108 0 29 108 108 54 0 27 0 54 ok ok
32 3063 attack-f1/3063 0 6912 6696 3.1250 6156 6048 1.7544 11772 9681 8200 162 5312 34 81 81 108 0 134 27 54 ok ok
33 2657 attack-f1/2657 0 6912 6696 3.1250 5994 5886 1.8018 12069 9009 7691 163 4483 37 108 54 135 0 108 0 54 ok ok
34 252 attack-f1/252 0 6912 6696 3.1250 6264 6156 1.7241 12447 9470 8023 135 5297 39 108 81 54 0 135 0 27 ok ok
35 1721 attack-f1/1721 0 6912 6696 3.1250 6183 6021 2.6201 12474 9471 8157 162 5782 40 108 81 135 0 54 0 54 ok ok
36 5538 attack-f1/5538 0 6912 6697 3.1105 6156 5967 3.0702 13446 9024 8019 164 5162 52 135 81 270 0 81 0 27 ok ok
37 3757 attack-f1/3757 0 6912 6697 3.1105 6237 6130 1.7156 13176 9344 8005 188 5469 48 81 54 269 0 162 0 27 ok ok
38 1769 attack-f1/1769 0 6912 6697 3.1105 6156 5941 3.4925 13095 9748 8207 161 5240 47 108 108 107 0 0 0 27 ok ok
39 112 attack-f1/112 0 6912 6697 3.1105 6291 6184 1.7008 12933 9556 8286 164 5534 43 107 27 53 0 108 27 108 ok ok
40 4815 attack-f1/4815 0 6912 6699 3.0816 6048 5967 1.3393 11772 9273 8128 217 5809 31 108 54 81 27 162 0 27 ok ok
41 8010 attack-f1/8010 0 6912 6700 3.0671 6102 5942 2.6221 12231 9227 7915 162 4973 38 134 80 135 0 54 0 81 ok ok
42 2943 attack-f1/2943 0 6912 6721 2.7633 6345 6156 2.9787 12798 9177 7906 163 5072 43 108 108 108 0 28 27 0 ok ok
43 7832 attack-f1/7832 0 6912 6722 2.7488 6237 6155 1.3147 13203 8936 7827 108 4751 45 108 81 162 0 81 0 0 ok ok
44 7597 attack-f1/7597 0 6912 6722 2.7488 6264 6128 2.1711 13581 8532 7750 218 4677 46 81 81 108 27 81 0 27 ok ok
45 6301 attack-f1/6301 0 6912 6722 2.7488 6183 5993 3.0729 12501 9554 7962 111 4914 42 108 135 162 0 27 0 27 ok ok
46 2120 attack-f1/2120 0 6912 6722 2.7488 6237 6182 0.8818 13419 9345 8180 216 5372 49 54 0 270 0 81 27 27 ok ok
47 1938 attack-f1/1938 0 6912 6722 2.7488 6426 6317 1.6962 12852 10123 8149 164 5018 44 81 54 108 0 54 0 27 ok ok
48 1852 attack-f1/1852 0 6912 6722 2.7488 6480 6290 2.9321 13176 9775 8209 136 5431 47 108 81 162 27 0 0 0 ok ok
49 9869 attack-f1/9869 0 6912 6723 2.7344 6129 5940 3.0837 12366 9443 8157 81 5379 39 135 54 54 54 54 27 54 ok ok
50 9755 attack-f1/9755 0 6912 6723 2.7344 6561 6372 2.8807 12636 10012 8126 135 5184 41 108 135 162 0 0 0 0 ok ok
51 9707 attack-f1/9707 0 6912 6723 2.7344 6210 6048 2.6087 12123 10006 7962 137 4887 35 162 108 54 0 81 0 81 ok ok

View file

@ -0,0 +1,279 @@
#include <stdint.h>
uint32_t shfl(uint32_t v, uint32_t mask) __attribute__((const));
static inline uint32_t rotl_imm(uint32_t x, unsigned n) { return (x << n) | (x >> (32u - n)); }
static inline uint32_t rotr_var(uint32_t x, uint32_t s) { s &= 31u; return s ? ((x >> s) | (x << (32u - s))) : x; }
static inline uint32_t mulhi(uint32_t a, uint32_t b) { return (uint32_t)(((uint64_t)a * (uint64_t)b) >> 32); }
void pass(uint32_t *r, uint32_t sel) {
uint32_t r0 = r[0];
uint32_t r1 = r[1];
uint32_t r2 = r[2];
uint32_t r3 = r[3];
uint32_t r4 = r[4];
uint32_t r5 = r[5];
uint32_t r6 = r[6];
uint32_t r7 = r[7];
r2 = rotl_imm(r2, 10u);
r3 = r3 + r6 + ((((sel >> 31u) & 1u) != 0u) ? 0x6ef59996u : 0xab42dc8du);
r1 = r1 ^ shfl(r0, 1u);
r1 = mulhi(r1, r6);
r4 = r4 - r0;
r1 = rotl_imm(r1, 26u);
r6 = r6 ^ shfl(r0, 1u);
r3 = r3 + r7 + ((((sel >> 5u) & 1u) != 0u) ? 0x45d3d08bu : 0xbdcc8acdu);
r1 = r6 * r3 + r1;
r6 = r6 + r1 + ((((sel >> 16u) & 1u) != 0u) ? 0xfaa09c55u : 0x35ea23dcu);
r0 = r0 ^ r5;
r7 = r7 - r4;
r0 = r0 * r7;
r3 = r3 ^ shfl(r0, 1u);
r7 = r3 * r5 + r7;
r7 = r7 - r1;
r0 = r0 ^ r1;
r1 = r1 + r3 + ((((sel >> 17u) & 1u) != 0u) ? 0x55703eb3u : 0x51b1dbfau);
r6 = r6 ^ r7;
r6 = r6 + r4 + ((((sel >> 4u) & 1u) != 0u) ? 0x3d846fabu : 0x7d55d1d3u);
r1 = rotr_var(r1, r0);
r5 = r5 ^ shfl(r1, 8u);
r1 = r1 ^ shfl(r6, 2u);
r2 = r2 * r7;
r6 = rotr_var(r6, r4);
r0 = r3 * r2 + r0;
r6 = r6 ^ r0;
r6 = r6 * r5;
r5 = rotr_var(r5, r3);
r3 = r5 * r7 + r3;
r2 = r5 * r6 + r2;
r5 = r3 * r2 + r5;
r1 = mulhi(r1, r6);
r2 = r2 ^ r3;
r5 = r5 - r6;
r2 = r2 ^ r6;
r6 = r3 * r7 + r6;
r6 = r6 ^ shfl(r0, 8u);
r4 = r4 * r6;
r1 = r1 ^ r5;
r3 = rotr_var(r3, r4);
r5 = mulhi(r5, r2);
r4 = r4 ^ shfl(r0, 8u);
r5 = rotl_imm(r5, 19u);
r0 = r7 * r2 + r0;
r3 = r3 ^ r4;
r4 = mulhi(r4, r6);
r6 = r4 * r4 + r6;
r1 = r1 ^ r0;
r1 = r1 - r7;
r4 = r4 | r1;
r1 = r5 * r6 + r1;
r5 = r5 * r6;
r4 = r4 ^ r0;
r7 = r5 * r6 + r7;
r7 = r7 ^ r1;
r2 = r2 ^ r5;
r4 = r4 ^ r0;
r2 = rotl_imm(r2, 7u);
r1 = r1 ^ shfl(r6, 1u);
r4 = rotr_var(r4, r7);
r6 = r6 ^ shfl(r5, 16u);
r5 = mulhi(r5, r6);
r2 = mulhi(r2, r4);
r6 = r6 ^ r4;
r5 = mulhi(r5, r2);
r7 = r5 * r5 + r7;
r6 = r6 ^ r4;
r3 = r3 * r5;
r4 = r4 ^ r2;
r6 = r6 * r1;
r3 = r3 ^ r0;
r3 = mulhi(r3, r5);
r6 = rotr_var(r6, r4);
r6 = r6 + r4 + ((((sel >> 31u) & 1u) != 0u) ? 0xb3b344bau : 0xe7169cecu);
r0 = r2 * r3 + r0;
r1 = r1 * r7;
r6 = mulhi(r6, r5);
r5 = r5 * r1;
r3 = r3 - r5;
r2 = rotr_var(r2, r4);
r4 = rotr_var(r4, r0);
r6 = rotr_var(r6, r5);
r5 = r5 | r2;
r7 = r7 + r0 + ((((sel >> 18u) & 1u) != 0u) ? 0x37478b4eu : 0xe94ff297u);
r3 = r3 * r0;
r7 = r7 | r0;
r4 = r5 * r2 + r4;
r0 = rotl_imm(r0, 30u);
r0 = rotl_imm(r0, 31u);
r1 = r1 - r4;
r5 = r5 ^ shfl(r0, 16u);
r5 = r5 * r1;
r3 = r3 ^ shfl(r2, 4u);
r1 = r1 | r4;
r0 = r0 * r3;
r0 = r0 ^ r7;
r0 = rotl_imm(r0, 17u);
r0 = r0 * r5;
r5 = r5 ^ r2;
r5 = r5 ^ shfl(r1, 8u);
r2 = rotl_imm(r2, 10u);
r0 = r7 * r5 + r0;
r6 = r6 * r0;
r1 = r1 * r4;
r0 = r0 ^ shfl(r5, 4u);
r0 = r0 ^ r3;
r0 = r0 + r2 + ((((sel >> 22u) & 1u) != 0u) ? 0xec1522a4u : 0x4d3100e0u);
r7 = mulhi(r7, r2);
r1 = r1 ^ shfl(r7, 16u);
r3 = r3 ^ r2;
r3 = r3 ^ shfl(r2, 8u);
r3 = r3 + r5 + ((((sel >> 31u) & 1u) != 0u) ? 0x4717d483u : 0xbeee4787u);
r3 = r3 + r1 + ((((sel >> 27u) & 1u) != 0u) ? 0xdf7dcaf8u : 0x8da9f412u);
r7 = r7 + r5 + ((((sel >> 1u) & 1u) != 0u) ? 0x412d825fu : 0xeaee5720u);
r5 = r5 + r2 + ((((sel >> 1u) & 1u) != 0u) ? 0x63b09601u : 0x6f8106d6u);
r0 = r0 ^ shfl(r4, 1u);
r1 = r1 ^ shfl(r2, 16u);
r7 = r7 - r5;
r6 = r6 + r1 + ((((sel >> 14u) & 1u) != 0u) ? 0xaf6b5726u : 0xb52b8a97u);
r0 = r3 * r5 + r0;
r7 = r7 ^ shfl(r6, 16u);
r1 = r1 ^ shfl(r0, 4u);
r7 = mulhi(r7, r5);
r0 = r2 * r5 + r0;
r7 = mulhi(r7, r2);
r7 = r7 ^ r6;
r1 = rotr_var(r1, r7);
r5 = r5 - r2;
r0 = mulhi(r0, r7);
r5 = r5 ^ r0;
r2 = mulhi(r2, r1);
r5 = r5 ^ r0;
r4 = rotr_var(r4, r1);
r0 = r0 * r7;
r3 = r3 - r6;
r5 = rotl_imm(r5, 2u);
r5 = r5 + r3 + ((((sel >> 12u) & 1u) != 0u) ? 0xbb2210d3u : 0x09216229u);
r6 = r6 + r1 + ((((sel >> 21u) & 1u) != 0u) ? 0xad65bbd1u : 0xeb49de07u);
r5 = r5 - r7;
r7 = r7 ^ r4;
r5 = r5 - r3;
r2 = r2 ^ shfl(r1, 16u);
r2 = rotl_imm(r2, 27u);
r6 = r6 * r0;
r4 = r4 ^ shfl(r0, 16u);
r1 = r1 ^ shfl(r0, 16u);
r7 = mulhi(r7, r4);
r3 = r3 ^ r0;
r1 = mulhi(r1, r4);
r3 = r3 ^ shfl(r0, 4u);
r5 = r5 ^ r4;
r3 = mulhi(r3, r4);
r4 = mulhi(r4, r6);
r6 = r6 * r2;
r1 = r1 + r0 + ((((sel >> 26u) & 1u) != 0u) ? 0x2e31e926u : 0xf7861795u);
r6 = r6 ^ shfl(r1, 4u);
r4 = r4 ^ r5;
r1 = r1 | r3;
r3 = r3 ^ r6;
r7 = r7 + r1 + ((((sel >> 21u) & 1u) != 0u) ? 0x89256dabu : 0xf8cd4602u);
r3 = rotr_var(r3, r4);
r7 = rotl_imm(r7, 5u);
r0 = r0 - r6;
r1 = r4 * r1 + r1;
r5 = r5 + r1 + ((((sel >> 9u) & 1u) != 0u) ? 0xb1a7abfdu : 0xb5230e29u);
r2 = r2 ^ shfl(r0, 2u);
r0 = r0 | r6;
r7 = rotr_var(r7, r4);
r2 = rotl_imm(r2, 19u);
r4 = r4 + r3 + ((((sel >> 22u) & 1u) != 0u) ? 0xf1e9e601u : 0xe7bded55u);
r2 = r2 * r6;
r1 = rotr_var(r1, r3);
r0 = rotr_var(r0, r7);
r7 = r7 | r2;
r4 = rotl_imm(r4, 6u);
r4 = r4 ^ r7;
r1 = r1 ^ shfl(r0, 2u);
r4 = mulhi(r4, r0);
r4 = rotr_var(r4, r6);
r5 = r5 ^ r1;
r4 = rotr_var(r4, r0);
r6 = r6 * r7;
r6 = r6 | r7;
r1 = r1 - r2;
r5 = r5 + r4 + ((((sel >> 29u) & 1u) != 0u) ? 0xee34d6e4u : 0x2eb286f6u);
r4 = r3 * r7 + r4;
r2 = r2 | r4;
r4 = r4 * r7;
r0 = r0 ^ r2;
r3 = rotl_imm(r3, 20u);
r0 = r0 ^ r2;
r2 = r2 + r6 + ((((sel >> 30u) & 1u) != 0u) ? 0xa1da063cu : 0x90fafe81u);
r6 = r6 - r0;
r6 = r0 * r1 + r6;
r3 = r3 - r2;
r7 = r7 ^ r1;
r2 = r2 | r0;
r7 = r7 + r3 + ((((sel >> 3u) & 1u) != 0u) ? 0x8467eafau : 0x994e43feu);
r1 = mulhi(r1, r6);
r1 = rotr_var(r1, r3);
r6 = r2 * r0 + r6;
r0 = r0 ^ r7;
r7 = r7 ^ shfl(r5, 8u);
r1 = r1 ^ r7;
r1 = r1 * r7;
r5 = mulhi(r5, r2);
r0 = r4 * r1 + r0;
r0 = r4 * r2 + r0;
r6 = rotr_var(r6, r3);
r0 = r0 * r1;
r5 = r5 + r6 + ((((sel >> 25u) & 1u) != 0u) ? 0x355f1374u : 0xcc44b0b7u);
r6 = r6 - r3;
r3 = r3 ^ r0;
r3 = r3 + r7 + ((((sel >> 10u) & 1u) != 0u) ? 0x9b0cb60eu : 0xb496be92u);
r3 = r3 ^ r0;
r7 = r7 - r3;
r7 = r6 * r0 + r7;
r6 = r6 * r3;
r1 = rotr_var(r1, r2);
r1 = r1 + r6 + ((((sel >> 2u) & 1u) != 0u) ? 0xcfeeb3a7u : 0x21156fcfu);
r6 = r6 + r4 + ((((sel >> 9u) & 1u) != 0u) ? 0x71594a7cu : 0xf2a294b0u);
r3 = rotr_var(r3, r4);
r6 = rotl_imm(r6, 3u);
r2 = r2 + r7 + ((((sel >> 26u) & 1u) != 0u) ? 0x38c75136u : 0x6e58b645u);
r4 = r5 * r1 + r4;
r3 = r3 - r0;
r3 = r3 ^ shfl(r1, 1u);
r7 = r2 * r4 + r7;
r7 = r7 + r0 + ((((sel >> 6u) & 1u) != 0u) ? 0x15804358u : 0x1bdd34c6u);
r3 = r5 * r6 + r3;
r7 = r7 ^ r2;
r0 = rotl_imm(r0, 18u);
r4 = rotl_imm(r4, 26u);
r6 = r6 ^ r3;
r1 = r2 * r2 + r1;
r6 = r6 ^ shfl(r0, 8u);
r0 = r0 * r3;
r3 = r3 + r2 + ((((sel >> 12u) & 1u) != 0u) ? 0xc03003e4u : 0xbe75233eu);
r0 = mulhi(r0, r4);
r2 = r2 - r5;
r0 = rotl_imm(r0, 31u);
r5 = r5 ^ shfl(r2, 2u);
r6 = r6 * r2;
r3 = r3 ^ r4;
r5 = r5 - r2;
r5 = r5 * r7;
r3 = r3 + r2 + ((((sel >> 21u) & 1u) != 0u) ? 0x89c0b922u : 0xfdc528feu);
r6 = rotl_imm(r6, 13u);
r2 = r1 * r7 + r2;
r2 = r2 * r5;
r0 = rotl_imm(r0, 12u);
r5 = r5 * r7;
r2 = r2 * r4;
r4 = r4 ^ r6;
r7 = r7 ^ shfl(r0, 4u);
r[0] = r0;
r[1] = r1;
r[2] = r2;
r[3] = r3;
r[4] = r4;
r[5] = r5;
r[6] = r6;
r[7] = r7;
}

View file

@ -0,0 +1,32 @@
attack-f1/0 start=0 instrs=16 lanes=32 nodes=32 proved 0.05s
attack-f1/0 start=8 instrs=16 lanes=32 nodes=26 proved 0.04s
attack-f1/0 start=16 instrs=16 lanes=32 nodes=30 proved 0.04s
attack-f1/0 start=24 instrs=16 lanes=32 nodes=37 proved 0.05s
attack-f1/0 start=32 instrs=16 lanes=32 nodes=38 proved 0.04s
attack-f1/0 start=40 instrs=16 lanes=32 nodes=35 proved 0.05s
attack-f1/0 start=48 instrs=16 lanes=32 nodes=35 proved 0.05s
attack-f1/0 start=56 instrs=16 lanes=32 nodes=36 proved 0.05s
attack-f1/0 start=64 instrs=16 lanes=32 nodes=33 proved 0.05s
attack-f1/0 start=72 instrs=16 lanes=32 nodes=33 proved 0.04s
attack-f1/0 start=80 instrs=16 lanes=32 nodes=30 proved 0.04s
attack-f1/0 start=88 instrs=16 lanes=32 nodes=29 proved 0.05s
attack-f1/0 start=96 instrs=16 lanes=1 nodes=32 proved 0.00s
attack-f1/0 start=104 instrs=16 lanes=1 nodes=32 proved 0.00s
attack-f1/0 start=112 instrs=16 lanes=1 nodes=33 proved 0.04s
attack-f1/0 start=120 instrs=16 lanes=1 nodes=33 proved 0.00s
attack-f1/0 start=128 instrs=16 lanes=1 nodes=30 proved 0.00s
attack-f1/0 start=136 instrs=16 lanes=32 nodes=29 proved 0.04s
attack-f1/0 start=144 instrs=16 lanes=32 nodes=30 proved 0.04s
attack-f1/0 start=152 instrs=16 lanes=32 nodes=28 proved 0.04s
attack-f1/0 start=160 instrs=16 lanes=32 nodes=28 proved 0.06s
attack-f1/0 start=168 instrs=16 lanes=32 nodes=29 proved 0.06s
attack-f1/0 start=176 instrs=16 lanes=32 nodes=29 proved 0.05s
attack-f1/0 start=184 instrs=16 lanes=32 nodes=31 proved 0.07s
attack-f1/0 start=192 instrs=16 lanes=32 nodes=27 proved 0.06s
attack-f1/0 start=200 instrs=16 lanes=32 nodes=28 proved 0.05s
attack-f1/0 start=208 instrs=16 lanes=32 nodes=33 proved 0.04s
attack-f1/0 start=216 instrs=16 lanes=32 nodes=31 proved 0.04s
attack-f1/0 start=224 instrs=16 lanes=32 nodes=30 proved 0.05s
attack-f1/0 start=232 instrs=16 lanes=32 nodes=28 proved 0.04s
attack-f1/0 start=240 instrs=16 lanes=32 nodes=23 proved 0.03s
windows 31 counterexamples 0

View file

@ -0,0 +1,2 @@
attack-f1/0 start=0 instrs=256 lanes=32 nodes=367 unknown 785.86s
windows 1 counterexamples 0

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,177 @@
#!/usr/bin/env python3
"""Attack-pass F1: z3 equivalence of the harness's normal-form DAG against the straight-line shadow block.
Reads the JSON `attack-f1 windows` writes (a list of windows: the instruction slice, the reachable DAG nodes and
the 8 output node ids) and proves, per window, that for every input register file and every `sel` the DAG's
outputs equal the instruction-by-instruction run (the verifier's `step` semantics, verify.rs). A window with a
`shfl` is modelled on all 32 lanes, any other on one lane (every other op is lane-local).
z3check.py FILE.json [--timeout-ms 60000] [--lanes 32]
One line per window: seed, start, instrs, lanes, nodes, result (proved | COUNTEREXAMPLE | unknown), seconds.
Exit 1 on any COUNTEREXAMPLE.
"""
import json
import sys
import time
import z3
W = 32
def rotl(x, n):
n %= 32
return z3.RotateLeft(x, n) if n else x
def rotr_var(x, s):
# the kernels' rotr_var: amount & 31; z3's variable rotate matches rotate_right for 0..31
return z3.RotateRight(x, s & 31)
def mulhi(a, b):
p = z3.ZeroExt(32, a) * z3.ZeroExt(32, b)
return z3.Extract(63, 32, p)
def straight(instrs, regs, sel, lanes):
r = [list(v) for v in regs]
for ins in instrs:
d, a, b = ins["dst"], ins["src"], ins["src2"]
op = ins["op"]
if op == "add":
for l in range(lanes):
c = z3.If(z3.Extract(ins["bit"], ins["bit"], sel[l]) == 1, z3.BitVecVal(ins["imm2"], W), z3.BitVecVal(ins["imm"], W))
r[d][l] = r[d][l] + r[a][l] + c
elif op == "sub":
for l in range(lanes):
r[d][l] = r[d][l] - r[a][l]
elif op == "mul":
for l in range(lanes):
r[d][l] = r[d][l] * r[a][l]
elif op == "mulhi":
for l in range(lanes):
r[d][l] = mulhi(r[d][l], r[a][l])
elif op == "xor":
for l in range(lanes):
r[d][l] = r[d][l] ^ r[a][l]
elif op == "or":
for l in range(lanes):
r[d][l] = r[d][l] | r[a][l]
elif op == "rotl":
for l in range(lanes):
r[d][l] = rotl(r[d][l], ins["rot"])
elif op == "rotr":
for l in range(lanes):
r[d][l] = rotr_var(r[d][l], r[a][l])
elif op == "mad":
for l in range(lanes):
r[d][l] = r[a][l] * r[b][l] + r[d][l]
elif op == "shfl":
src = list(r[a])
m = ins["mask"]
for l in range(lanes):
r[d][l] = r[d][l] ^ src[l ^ m]
else:
raise SystemExit("not an ALU op: " + op)
return r
def dag_eval(nodes, regs, sel, lanes):
byid = {n["id"]: n for n in nodes}
memo = {}
def get(i):
if i in memo:
return memo[i]
n = byid[i]
k = n["k"]
if k == "in":
v = list(regs[n["r"]])
elif k == "csel":
v = [z3.If(z3.Extract(n["bit"], n["bit"], sel[l]) == 1, z3.BitVecVal(n["imm2"], W), z3.BitVecVal(n["imm"], W)) for l in range(lanes)]
elif k == "zero":
v = [z3.BitVecVal(0, W) for _ in range(lanes)]
elif k == "sum":
v = [z3.BitVecVal(0, W) for _ in range(lanes)]
for t, c in n["t"]:
tv = get(t)
for l in range(lanes):
v[l] = v[l] + tv[l] * z3.BitVecVal(c, W)
elif k == "xor":
v = [z3.BitVecVal(0, W) for _ in range(lanes)]
for t, rot, m in n["t"]:
tv = get(t)
for l in range(lanes):
v[l] = v[l] ^ rotl(tv[(l ^ m) % lanes], rot)
elif k == "or":
v = [z3.BitVecVal(0, W) for _ in range(lanes)]
for t in n["t"]:
tv = get(t)
for l in range(lanes):
v[l] = v[l] | tv[l]
elif k == "rotr":
xv, sv = get(n["x"]), get(n["s"])
v = [rotr_var(xv[l], (sv[l] & 31) * n["n"]) for l in range(lanes)]
elif k == "mul":
av, bv = get(n["a"]), get(n["b"])
v = [z3.ZeroExt(32, av[l]) * z3.ZeroExt(32, bv[l]) for l in range(lanes)]
elif k == "lo":
pv = get(n["p"])
v = [z3.Extract(31, 0, pv[l]) for l in range(lanes)]
elif k == "hi":
pv = get(n["p"])
v = [z3.Extract(63, 32, pv[l]) for l in range(lanes)]
else:
raise SystemExit("unknown node kind " + k)
v = [z3.simplify(x) for x in v]
memo[i] = v
return v
return get
def main():
args = sys.argv[1:]
path = args[0]
timeout = 60000
force_lanes = None
if "--timeout-ms" in args:
timeout = int(args[args.index("--timeout-ms") + 1])
if "--lanes" in args:
force_lanes = int(args[args.index("--lanes") + 1])
windows = json.load(open(path))
bad = 0
for w in windows:
has_shfl = any(i["op"] == "shfl" for i in w["instrs"])
lanes = force_lanes or (32 if has_shfl else 1)
regs = [[z3.BitVec(f"r{r}_{l}", W) for l in range(lanes)] for r in range(8)]
sel = [z3.BitVec(f"sel_{l}", W) for l in range(lanes)]
t0 = time.time()
sl = straight(w["instrs"], regs, sel, lanes)
get = dag_eval(w["nodes"], regs, sel, lanes)
s = z3.Solver()
s.set("timeout", timeout)
diffs = []
for r, oid in enumerate(w["outputs"]):
dv = get(oid)
for l in range(lanes):
diffs.append(dv[l] != sl[r][l])
s.add(z3.Or(diffs))
res = s.check()
dt = time.time() - t0
if res == z3.unsat:
verdict = "proved"
elif res == z3.sat:
verdict = "COUNTEREXAMPLE"
bad += 1
else:
verdict = "unknown"
print(f"{w['seed']} start={w['start']} instrs={len(w['instrs'])} lanes={lanes} nodes={len(w['nodes'])} {verdict} {dt:.2f}s", flush=True)
print(f"windows {len(windows)} counterexamples {bad}")
sys.exit(1 if bad else 0)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,336 @@
#!/usr/bin/env node
// F10 (attack pass, 7 October 2026): the ladder's step rule under EXACT signal shares. The real-mining harness
// (latency-ladder.mjs, the ladder lane's) gives three equal CPU miners, so the only shares it can cast are 0, 33, 67
// and 100 percent, and a random miner's share in any one window scatters by several points, so no real-mining run can
// put 8,900 to 8,999 bps in the weakest of seven windows and hold it there. This driver removes the miner from the
// question the rule is asked: a 3-node network on the ladder fork with skip_proof_of_work, ONE producer that takes
// node 0's template, writes the ladder bits it wants into the header version (bit 15 up, bit 14 down, both or neither
// none) and submits it, one block per DAA score on a linear chain, so every window of W DAA holds exactly W blue
// blocks, one of each residue modulo W. A schedule names, per DAA range, the direction and how many residues carry no
// signal: with W = 100, 11 residues give 8,900 bps in every window whatever the window's alignment, 10 give 9,000.
// The three nodes read the chain and decide the rung on their own (processes::latency_ladder); the driver records
// every node's template (rung, weakest up, weakest down) at every poll, restarts a node mid-window on request, and at
// the end re-tallies the chain in JavaScript (an independent copy of the rule) and compares it to what the nodes did.
//
// node ladder-exact.mjs --case <name> --schedule "<from>:<up|down|none>:<nones>[,...]" --expect-steps "<epoch>:<rung>[,...]"
// [--window 100] [--epochs 44] [--rate 8] [--secs 1500] [--restart "<node>@<daa>[,...]"]
//
// Ports IGNEUM_F10_BASE (29900) and up, devnet suffix IGNEUM_F10_SUFFIX (990), data IGNEUM_LADDER_TMP
// (/tmp/igneum-fast-time-attack-f10). Binaries: IGNEUM_LADDER_BIN (the box's ladder lane layout, read-only).
import { spawn } from 'node:child_process';
import { mkdirSync, rmSync, writeFileSync, readFileSync, openSync, existsSync } from 'node:fs';
import { dirname } from 'node:path';
const HERE = dirname(new URL(import.meta.url).pathname) + '/';
function findRoot(from) { let d = from; for (let i = 0; i < 6; i++) { if (existsSync(`${d}tools/finality-attacks/lib/rpc.mjs`)) return d; d = dirname(d.replace(/\/$/, '')) + '/'; } throw new Error('no tree with tools/finality-attacks/lib/rpc.mjs above ' + from); }
const ROOT = process.env.IGNEUM_ROOT ? process.env.IGNEUM_ROOT.replace(/\/?$/, '/') : findRoot(HERE);
const { connectRpc } = await import(`${ROOT}tools/finality-attacks/lib/rpc.mjs`);
const { devAddress } = await import(`${ROOT}tools/harness/lib/address.mjs`);
const FILE = `${HERE}override-60x.json`;
const BIN = process.env.IGNEUM_LADDER_BIN || '/srv/builds/igneum-wt-ladder/vendor/igneum-node-ladder/target/release';
const IGNEUMD = process.env.IGNEUMD || `${BIN}/igneumd`;
const TMP = process.env.IGNEUM_LADDER_TMP || '/tmp/igneum-fast-time-attack-f10';
const BASE = +(process.env.IGNEUM_F10_BASE || 29900), SUFFIX = +(process.env.IGNEUM_F10_SUFFIX || 990);
const WINDOWS = 7, THRESHOLD = 9000;
const args = process.argv.slice(2);
const flag = (name, dflt) => { const i = args.indexOf(`--${name}`); return i >= 0 ? +args[i + 1] : dflt; };
const sflag = (name, dflt = null) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : dflt; };
const CASE = sflag('case', 'exact');
const WINDOW = flag('window', 100);
const EPOCHS = flag('epochs', 44);
const RATE = flag('rate', 8);
const SECS = flag('secs', 1500);
const SCHEDULE = (sflag('schedule') || '0:up:0').split(',').map(s => { const [from, dir, nones] = s.trim().split(':'); return { from: +from, dir, nones: +(nones || 0) }; }).sort((a, b) => a.from - b.from);
const EXPECT_STEPS = (sflag('expect-steps', '') || '').split(',').filter(Boolean).map(s => { const [e, r] = s.split(':'); return { epoch: +e, rung: +r }; });
const RESTARTS = (sflag('restart', '') || '').split(',').filter(Boolean).map(s => { const [n, d] = s.split('@'); return { node: +n, daa: +d, done: false }; });
if (!SCHEDULE.every(s => ['up', 'down', 'none'].includes(s.dir) && s.nones >= 0 && s.nones <= WINDOW)) { console.error('usage: --schedule "from:up|down|none:nones,..."'); process.exit(2); }
if (!existsSync(IGNEUMD)) { console.error(`missing ${IGNEUMD}`); process.exit(2); }
const started = [];
const log = (...a) => console.log(new Date().toISOString().slice(11, 23), ...a);
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
rmSync(TMP, { recursive: true, force: true }); mkdirSync(TMP, { recursive: true });
const baseText = readFileSync(FILE, 'utf8');
const field = (name) => { const m = new RegExp(`"${name}":\\s*([0-9]+)`).exec(baseText); return m ? +m[1] : undefined; };
const EPOCH = field('pow_epoch_blocks');
const LEAD = field('pow_epoch_lead');
const LADDER = JSON.parse(/"latency_ladder":\s*(\[[^\]]*\])/.exec(baseText)[1]);
let FIRST_FULL_EPOCH = 0;
while (FIRST_FULL_EPOCH * EPOCH - LEAD - 1 < WINDOWS * WINDOW) FIRST_FULL_EPOCH++;
function mergeOverrideText(text, fields) {
let out = text;
for (const k of Object.keys(fields)) out = out.replace(new RegExp(`\\s*"${k}":\\s*[^,}\\n]+,?`), '');
const extra = Object.entries(fields).map(([k, v]) => `"${k}": ${typeof v === 'string' && !/^\d+$/.test(v) ? JSON.stringify(v) : v}`).join(', ');
return out.replace(/,?\s*}\s*$/, `,\n ${extra}\n}\n`);
}
const override = `${TMP}/override.json`;
writeFileSync(override, mergeOverrideText(baseText, {
skip_proof_of_work: true,
program_class_v3_activation_daa: '0', program_class_v4_activation_daa: '0', program_class_v4_signal_window_daa: '0',
latency_ladder_activation_daa: '0', latency_ladder_window_daa: String(WINDOW),
}));
log(`case ${CASE}: schedule ${SCHEDULE.map(s => `${s.from}:${s.dir}:${s.nones}`).join(' ')} (W ${WINDOW}, ${WINDOWS} windows, threshold ${THRESHOLD} bps); expect steps ${EXPECT_STEPS.map(s => `e${s.epoch}->r${s.rung}`).join(' ') || 'none'}; restarts ${RESTARTS.map(r => `n${r.node}@${r.daa}`).join(' ') || 'none'}; ${EPOCH} DAA per epoch, lead ${LEAD}, first epoch with seven full windows ${FIRST_FULL_EPOCH}; ladder ${LADDER.map(r => r.admissible ? r.reps : `[${r.reps}]`).join(', ')}; ${RATE} blocks/s, up to ${EPOCHS} epochs or ${SECS} s`);
// the schedule: the signal a block at DAA score d carries. The residues 0 .. nones-1 (mod W) carry none; a none block
// alternates between no bits and both bits (both bits = no signal, igneum.rs ladder_signal_of), so the chain shows
// the rule reads both forms as none.
function segmentAt(d) { let seg = SCHEDULE[0]; for (const s of SCHEDULE) if (d >= s.from) seg = s; return seg; }
function signalAt(d) { const seg = segmentAt(d); if (seg.dir === 'none' || (d % WINDOW) < seg.nones) return 'none'; return seg.dir; }
function bitsFor(sig, d) { return sig === 'up' ? 0x8000 : sig === 'down' ? 0x4000 : (d % 2 ? 0xc000 : 0x0000); }
const bitsOf = (v) => ((v & 0xc000) === 0x8000) ? 'up' : ((v & 0xc000) === 0x4000) ? 'down' : 'none';
class Node {
constructor(i, connect = []) {
this.i = i; this.grpcPort = BASE + i * 10; this.p2pPort = BASE + i * 10 + 1; this.jsonPort = BASE + i * 10 + 2;
this.connect = connect; this.dir = `${TMP}/n${i}`; this.logFile = `${this.dir}/node.log`; this.starts = 0;
}
async start() {
mkdirSync(this.dir, { recursive: true });
const a = ['--devnet', `--devnet-suffix=${SUFFIX}`, '--nodnsseed', '--disable-upnp', '--nologfiles', '--enable-unsynced-mining', '--utxoindex',
`--appdir=${this.dir}`, `--rpclisten=127.0.0.1:${this.grpcPort}`, `--rpclisten-json=127.0.0.1:${this.jsonPort}`,
`--listen=127.0.0.1:${this.p2pPort}`, `--override-params-file=${override}`, '--loglevel=info', '--yes'];
if (this.connect.length) a.push(`--connect=${this.connect.join(',')}`); else a.push('--outpeers=0');
const out = openSync(this.logFile, 'a');
this.proc = spawn(IGNEUMD, a, { stdio: ['ignore', out, out], env: { ...process.env, IGNEUM_LADDER_SIGNAL: 'none' } });
started.push(this.proc);
this.starts++;
writeFileSync(`${TMP}/n${this.i}.pid`, String(this.proc.pid));
await sleep(1500);
this.rpc = await connectRpc(`ws://127.0.0.1:${this.jsonPort}`);
log(`n${this.i} up (start ${this.starts}) pid ${this.proc.pid} json ${this.jsonPort} p2p ${this.p2pPort}`);
return this;
}
async stop() {
const p = this.proc; if (!p) return;
try { this.rpc && this.rpc.close(); } catch { }
try { p.kill('SIGINT'); } catch { }
for (let i = 0; i < 100 && p.exitCode == null && p.signalCode == null; i++) await sleep(100);
if (p.exitCode == null && p.signalCode == null) { try { p.kill('SIGKILL'); } catch { } await sleep(500); }
const idx = started.indexOf(p); if (idx >= 0) started.splice(idx, 1);
this.proc = null;
}
grepLog(re) { try { return readFileSync(this.logFile, 'utf8').split('\n').filter(l => re.test(l)); } catch { return []; } }
}
async function stopAll() {
for (const p of started.slice().reverse()) { try { p.kill('SIGINT'); } catch { } }
await sleep(1500);
for (const p of started) { try { p.kill('SIGKILL'); } catch { } }
}
process.on('SIGINT', async () => { await stopAll(); process.exit(130); });
process.on('unhandledRejection', async (e) => { log(`FAILED: ${e?.stack || e}`); await stopAll(); process.exit(3); });
const STEP_LINE = /Latency ladder step by miner signal: epoch (\d+) moves to rung (\d+) \((\d+) shadow passes, from rung (\d+)\): (up|down) in each of (\d+) consecutive windows of (\d+) DAA .*weakest up (\d+) bps, weakest down (\d+) bps/;
const parseStep = (l) => { const m = STEP_LINE.exec(l); if (!m) return null; const ts = Date.parse(l.slice(0, 29).replace(' ', 'T')); return { ts, epoch: +m[1], rung: +m[2], reps: +m[3], from: +m[4], dir: m[5], weakest_up: +m[8], weakest_down: +m[9] }; };
const t0 = Date.now();
const since = () => ((Date.now() - t0) / 1000).toFixed(1);
const n0 = await new Node(0).start();
const n1 = await new Node(1, [`127.0.0.1:${n0.p2pPort}`]).start();
const n2 = await new Node(2, [`127.0.0.1:${n0.p2pPort}`]).start();
const nodes = [n0, n1, n2];
for (const n of nodes) log(`n${n.i}: ${n.grepLog(/Latency ladder active/).map(l => l.replace(/^.*?(Latency ladder active)/, '$1'))[0] || '(no ladder line)'}`);
log(`n0 digest: ${n0.grepLog(/Consensus params digest/).map(l => l.replace(/^.*?digest: /, '').slice(0, 16)).join(' ')}`);
const pay = devAddress('fast-time-attack-f10');
// the producer
const produced = []; // { daa, sig, version }
let rejected = 0, submitErrors = [];
let lastDaa = -1, firstReport = true;
async function produceOne() {
let t;
try { t = await n0.rpc.call('getBlockTemplate', { payAddress: pay, extraData: [] }); } catch (e) { submitErrors.push(`template: ${e.message}`); return false; }
const h = t.block.header; const daa = +h.daaScore;
if (daa === lastDaa) return false; // the virtual has not moved past the last block yet
const sig = signalAt(daa);
h.version = (h.version & 0x3fff) | bitsFor(sig, daa);
let r;
try { r = await n0.rpc.call('submitBlock', { block: t.block, allowNonDAABlocks: false }); } catch (e) { submitErrors.push(`submit daa ${daa}: ${e.message}`); return false; }
const rs = JSON.stringify(r);
if (firstReport) { log(`first submit at daa ${daa} sig ${sig} version 0x${h.version.toString(16)}: ${rs.slice(0, 200)}`); firstReport = false; }
if (/reject/i.test(rs)) { rejected++; submitErrors.push(`daa ${daa}: ${rs.slice(0, 200)}`); }
produced.push({ daa, sig, version: h.version });
lastDaa = daa;
return true;
}
// the observer: every node's template at every poll
const perNode = nodes.map(() => new Map()); // epoch -> first-seen info
const polls = []; let disagreements = [];
let lastEpochN0 = -1, lastReport = 0, endAt = null;
async function observe() {
const seen = await Promise.all(nodes.map(async n => {
if (!n.rpc) return null;
try {
const t = await n.rpc.call('getBlockTemplate', { payAddress: pay, extraData: [] });
const pe = t.powEpoch || t.pow_epoch || {};
return { daa: +(pe.virtualDaaScore ?? t.block?.header?.daaScore), epoch: pe.epochIndex, step: pe.latencyLadderStep, reps: pe.latencyLadderReps, next_step: pe.nextLatencyLadderStep, next_reps: pe.nextLatencyLadderReps, up: pe.latencyLadderUpBps, up_weakest: pe.latencyLadderUpWeakestBps, down: pe.latencyLadderDownBps, down_weakest: pe.latencyLadderDownWeakestBps, step_epoch: pe.latencyLadderStepEpoch ?? null, cls: pe.programClass };
} catch (e) { return { error: e.message }; }
}));
seen.forEach((s, i) => { if (s && s.epoch != null && !perNode[i].has(s.epoch)) perNode[i].set(s.epoch, { ...s, at: +since(), start: nodes[i].starts }); });
const ok = seen.filter(s => s && s.epoch != null);
if (ok.length >= 2) { const eps = new Set(ok.map(s => s.epoch)); if (eps.size === 1) { const steps = new Set(ok.map(s => s.step)); if (steps.size > 1) disagreements.push({ at: +since(), epoch: [...eps][0], steps: seen.map(s => s && s.step) }); } }
const s0 = seen[0];
if (s0 && s0.epoch != null && s0.epoch !== lastEpochN0) {
log(`epoch ${lastEpochN0} -> ${s0.epoch} at daa ${s0.daa}, ${since()} s: n0 rung ${s0.step} (${s0.reps}) next ${s0.next_step} (${s0.next_reps}) up ${s0.up} weakest ${s0.up_weakest} down ${s0.down} weakest ${s0.down_weakest} step epoch ${s0.step_epoch ?? 'none'} | n1 rung ${seen[1]?.step ?? '?'} e${seen[1]?.epoch ?? '?'} | n2 rung ${seen[2]?.step ?? '?'} e${seen[2]?.epoch ?? '?'}`);
lastEpochN0 = s0.epoch;
}
if (Date.now() - lastReport > 20000) {
lastReport = Date.now();
const counts = await Promise.all(nodes.map(async n => { try { const d = await n.rpc.call('getBlockDagInfo'); return `${d.blockCount}/${String(d.sink).slice(0, 8)}`; } catch { return '?'; } }));
log(`t=${since()} s produced ${produced.length} daa ${s0?.daa} epoch ${s0?.epoch} rungs ${seen.map(s => s?.step ?? '?').join('/')} blocks/sink ${counts.join(' ')} disagreements ${disagreements.length} rejected ${rejected}`);
polls.push({ t: +since(), daa: s0?.daa, epoch: s0?.epoch, rungs: seen.map(s => s?.step ?? null), nodes: counts });
}
return s0;
}
const restartsDone = [];
while (Date.now() - t0 < SECS * 1000) {
const did = await produceOne();
await sleep(did ? Math.max(5, 1000 / RATE) : 25);
if (produced.length % 5 === 0 || !did) {
const s0 = await observe();
for (const r of RESTARTS) {
if (!r.done && lastDaa >= r.daa) {
r.done = true;
const n = nodes[r.node];
log(`RESTART n${r.node} at daa ${lastDaa} (${since()} s): SIGINT, wait, start again on the same data dir`);
await n.stop();
const stoppedAt = Date.now();
await n.start();
restartsDone.push({ node: r.node, daa: lastDaa, at: +since(), stopped_ms: Date.now() - stoppedAt, wall: new Date(stoppedAt).toISOString() });
}
}
if (s0 && s0.epoch != null && s0.epoch >= EPOCHS) { endAt = +since(); break; }
}
}
if (endAt == null) endAt = +since();
log(`production ended at ${endAt} s: ${produced.length} blocks, last daa ${lastDaa}; settling 4 s`);
await sleep(4000);
await observe();
// the chain, from node 0
const dag = await Promise.all(nodes.map(async n => { try { return await n.rpc.call('getBlockDagInfo'); } catch (e) { return { error: e.message }; } }));
const genesis = dag[0].pruningPointHash;
async function allBlocks(n) {
const out = []; let low = genesis; const seen = new Set();
for (let round = 0; round < 2000; round++) {
const r = await n.rpc.call('getBlocks', { lowHash: low, includeBlocks: true, includeTransactions: false });
const blocks = r.blocks || [];
let added = 0;
for (const b of blocks) { const h = b.verboseData?.hash || b.header?.hash; if (seen.has(h)) continue; seen.add(h); out.push({ hash: h, daa: +b.header.daaScore, version: +b.header.version, chain: !!b.verboseData?.isChainBlock }); added++; }
if (!blocks.length || added === 0) break;
low = (r.blockHashes || []).at(-1) || blocks.at(-1).verboseData?.hash; if (!low) break;
}
return out.sort((a, b) => a.daa - b.daa);
}
let blocks = [];
try { blocks = await allBlocks(n0); } catch (e) { log(`getBlocks: ${e.message}`); }
const chainBlocks = blocks.filter(b => b.chain);
const daaCounts = new Map(); for (const b of blocks) daaCounts.set(b.daa, (daaCounts.get(b.daa) || 0) + 1);
const linear = [...daaCounts.entries()].every(([d, c]) => c === 1 || (d === 0 && c <= 2));
const bitsOnChain = blocks.reduce((m, b) => { const k = bitsOf(b.version); m[k] = (m[k] || 0) + 1; return m; }, {});
const bothBits = blocks.filter(b => (b.version & 0xc000) === 0xc000).length;
const lowBytes = new Set(blocks.filter(b => b.daa > 0).map(b => b.version & 0xff));
const objectBytes = new Set(blocks.map(b => (b.version >> 8) & 0x3f));
// the oracle: the rule re-tallied in JavaScript from the chain (every block of a linear chain is blue and on the chain)
const byDaa = new Map(blocks.map(b => [b.daa, b]));
const shareBps = (s, t) => t === 0 ? 0 : Math.floor(s * 10000 / t);
const admissible = (r) => r < LADDER.length && LADDER[r].admissible;
const oracle = []; // per epoch
let prev = { step: 0, since_daa: 0 };
const maxEpoch = Math.max(0, ...perNode[0].keys());
for (let e = 0; e <= maxEpoch; e++) {
let seedDaa;
if (e === 0) seedDaa = 0; else { const below = e * EPOCH - LEAD; const cands = chainBlocks.filter(b => b.daa < below); seedDaa = cands.length ? cands.at(-1).daa : 0; }
const span = WINDOW * WINDOWS, start = Math.max(0, seedDaa - span), full = seedDaa >= span;
const win = Array.from({ length: WINDOWS }, () => ({ t: 0, up: 0, down: 0 }));
for (const b of blocks) {
if (!(b.daa > start && b.daa <= seedDaa)) continue;
const back = seedDaa - b.daa; const k = WINDOWS - 1 - Math.min(Math.floor(back / WINDOW), WINDOWS - 1);
win[k].t++; const s = bitsOf(b.version); if (s === 'up') win[k].up++; else if (s === 'down') win[k].down++;
}
const upS = win.map(w => shareBps(w.up, w.t)), downS = win.map(w => shareBps(w.down, w.t));
const firstCounted = seedDaa - span + 1;
let state = prev, reason = 'stands';
if (!full) reason = 'windows not full';
else if (firstCounted < prev.since_daa) reason = `cool-down (oldest window begins ${firstCounted}, step took effect ${prev.since_daa})`;
else if (upS.every(s => s >= THRESHOLD) && admissible(prev.step + 1)) { state = { step: prev.step + 1, since_daa: e * EPOCH }; reason = 'up'; }
else if (upS.every(s => s >= THRESHOLD)) reason = 'up signalled but the rung above is inadmissible';
else if (downS.every(s => s >= THRESHOLD) && prev.step > 0) { state = { step: prev.step - 1, since_daa: e * EPOCH }; reason = 'down'; }
else if (downS.every(s => s >= THRESHOLD)) reason = 'down signalled at rung 0: the floor';
oracle.push({ epoch: e, seed_daa: seedDaa, full, first_counted_daa: firstCounted, windows: win.map(w => w.t), up_bps: upS, down_bps: downS, weakest_up: Math.min(...upS), weakest_down: Math.min(...downS), step: state.step, stepped: state.step !== prev.step, reason });
prev = state;
}
const oracleSteps = oracle.filter(o => o.stepped).map(o => ({ epoch: o.epoch, rung: o.step }));
// the nodes' step lines
const stepLines = nodes.map(n => n.grepLog(STEP_LINE).map(parseStep).filter(Boolean));
const stepKey = (s) => `${s.epoch}:${s.rung}:${s.from}:${s.weakest_up}:${s.weakest_down}`;
const stepSets = stepLines.map(ls => [...new Set(ls.map(stepKey))].sort());
const nodeSteps = [...new Set(stepLines.flat().map(s => `${s.epoch}:${s.rung}`))].sort((a, b) => +a.split(':')[0] - +b.split(':')[0]).map(s => ({ epoch: +s.split(':')[0], rung: +s.split(':')[1] }));
const sameList = (a, b) => a.length === b.length && a.every((x, i) => x.epoch === b[i].epoch && x.rung === b[i].rung);
// per epoch: the rung every node reported in its template (first seen), and the node-reported weakest against the oracle
const epochRows = [];
for (let e = 0; e <= maxEpoch; e++) {
const rungs = perNode.map(m => m.get(e)?.step ?? null);
const o = oracle[e];
const n0i = perNode[0].get(e);
epochRows.push({ epoch: e, seed_daa: o?.seed_daa, rungs, n0_up_weakest: n0i?.up_weakest ?? null, n0_down_weakest: n0i?.down_weakest ?? null, oracle_up_weakest: o?.weakest_up, oracle_down_weakest: o?.weakest_down, oracle_step: o?.step, oracle_reason: o?.reason, stepped: o?.stepped });
}
const rungAgreementRows = epochRows.filter(r => r.rungs.filter(x => x != null).length >= 2);
// the restarted nodes: every step line they logged before the restart appears again after it, identical
const restartChecks = restartsDone.map(r => {
const ls = stepLines[r.node]; const wall = Date.parse(r.wall);
const before = ls.filter(s => s.ts < wall), after = ls.filter(s => s.ts >= wall);
const missing = before.filter(b => !after.some(a => stepKey(a) === stepKey(b)));
return { ...r, lines_before: before.length, lines_after: after.length, recomputed_identically: missing.length === 0 && (before.length === 0 || after.length > 0), missing: missing.map(stepKey) };
});
const checks = {
chain_is_linear_one_block_per_daa: linear && blocks.length > 0,
zero_rejected_submits: rejected === 0 && submitErrors.length === 0,
// under skip_proof_of_work every node logs 'PoW rejected <hash> by igneum-lottery-v2-bound (daa N, nonce 0x0)' at INFO
// for every block and accepts it anyway (smoke run, 7 Oct 2026 08:14Z: 180 produced, 180/180/180 on the three nodes);
// the chain-side fact is the block count on every node
// (the first exact-89 run, 08:48Z: 'ban' matched the finality parameter line 'ban 120', and blockCount excludes genesis)
zero_rejected_by_nodes: nodes.every(n => n.grepLog(/Rejected block|rejected block|invalid block/i).filter(l => !/PoW rejected .* by igneum-lottery/.test(l)).length === 0),
every_produced_block_on_every_node: dag.every(d => +d.blockCount === produced.length),
sinks_agree: new Set(dag.map(d => String(d.sink))).size === 1,
block_counts_agree: new Set(dag.map(d => String(d.blockCount))).size === 1,
every_block_version_2_object_0: [...lowBytes].every(v => v === 2) && [...objectBytes].every(v => v === 0),
ran_the_epochs: maxEpoch >= EPOCHS,
nodes_never_disagree_on_the_rung_at_the_same_epoch: disagreements.length === 0 && rungAgreementRows.every(r => new Set(r.rungs.filter(x => x != null)).size === 1),
every_node_reported_every_epoch_after_the_first_full: rungAgreementRows.filter(r => r.epoch >= FIRST_FULL_EPOCH).length >= maxEpoch - FIRST_FULL_EPOCH - 1,
// the template's weakest is the LIVE tally anchored at the sink (consensus/mod.rs get_pow_epoch_info, tally_ladder(sink)),
// read at the first template of the epoch (sink = seed + lead); under the residue construction its seven full windows
// carry the same shares as the seed-anchored ones, so it is compared from the first epoch with seven full windows on
// (the first exact-89 run, 08:48Z: epoch 11's partial oldest bucket read 49 of 59 at the sink against 39 of 49 at the seed)
// and only at epochs with no schedule boundary inside the seven windows plus the lead (the second down run, 09:11Z:
// epoch 13's newest window read 40 up blocks in (679, 779] at the sink against 50 in (669, 769] at the seed, the
// boundary at 720 inside both; the step lines' own weakest matched the oracle at every step)
node_weakest_equals_oracle_weakest: epochRows.filter(r => r.n0_up_weakest != null && r.epoch >= FIRST_FULL_EPOCH && !SCHEDULE.some(seg => seg.from > 0 && seg.from > r.seed_daa - WINDOW * WINDOWS && seg.from <= r.seed_daa + LEAD)).every(r => r.n0_up_weakest === r.oracle_up_weakest && r.n0_down_weakest === r.oracle_down_weakest),
// the decision's own weakest, from the step lines, against the oracle at the stepped epochs
step_line_weakest_equals_oracle_weakest: stepLines.flat().every(s => { const o = oracle[s.epoch]; return o && o.weakest_up === s.weakest_up && o.weakest_down === s.weakest_down; }),
node_rung_equals_oracle_rung_every_epoch: epochRows.every(r => r.rungs.every(x => x == null || x === r.oracle_step)),
step_lines_identical_on_every_node: stepSets.every(s => JSON.stringify(s) === JSON.stringify(stepSets[0])),
steps_equal_the_oracle: sameList(nodeSteps, oracleSteps),
steps_equal_the_expectation: sameList(nodeSteps, EXPECT_STEPS),
no_step_under_9000_in_its_direction: stepLines.flat().every(s => (s.dir === 'up' ? s.weakest_up : s.weakest_down) >= THRESHOLD) && oracle.filter(o => o.stepped).every(o => (o.reason === 'up' ? o.weakest_up : o.weakest_down) >= THRESHOLD),
every_step_moves_one_rung: stepLines.flat().every(s => Math.abs(s.rung - s.from) === 1) && stepLines.flat().every(s => s.rung >= 0),
restarted_nodes_recomputed_the_same_steps: restartChecks.every(r => r.recomputed_identically),
};
const pass = Object.values(checks).every(Boolean);
const summary = {
pass, case: CASE, schedule: SCHEDULE, expect_steps: EXPECT_STEPS, restarts: restartChecks, checks, window: WINDOW, windows: WINDOWS, threshold_bps: THRESHOLD, epoch_blocks: EPOCH, lead: LEAD, first_full_epoch: FIRST_FULL_EPOCH, ladder: LADDER,
node: IGNEUMD, rate: RATE, produced: produced.length, run_ended_at_s: endAt, last_daa: lastDaa, max_epoch_seen: maxEpoch,
blocks: { total: blocks.length, chain: chainBlocks.length, linear, bits: bitsOnChain, both_bits_blocks: bothBits, low_bytes: [...lowBytes], object_bytes: [...objectBytes] },
rejected_submits: rejected, submit_errors: submitErrors.slice(0, 20), disagreements, dag: dag.map(d => ({ blocks: d.blockCount, sink: String(d.sink || '?').slice(0, 16) })),
epochs: epochRows, oracle, node_steps: nodeSteps, oracle_steps: oracleSteps, step_lines: stepLines, polls,
};
writeFileSync(`${TMP}/summary.json`, JSON.stringify(summary, null, 2));
log(`SUMMARY ${pass ? 'PASS' : 'FAIL'} (case ${CASE}): steps ${nodeSteps.map(s => `e${s.epoch}->r${s.rung}`).join(' ') || 'none'} (oracle ${oracleSteps.map(s => `e${s.epoch}->r${s.rung}`).join(' ') || 'none'}, expected ${EXPECT_STEPS.map(s => `e${s.epoch}->r${s.rung}`).join(' ') || 'none'}); ${blocks.length} blocks (${linear ? 'linear' : 'NOT linear'}) bits ${JSON.stringify(bitsOnChain)} both-bits ${bothBits}; rejected ${rejected}; disagreements ${disagreements.length}; sinks ${dag.map(d => String(d.sink || '?').slice(0, 8)).join(' ')} at ${dag.map(d => d.blockCount).join('/')}; restarts ${restartChecks.map(r => `n${r.node}@${r.daa}:${r.recomputed_identically ? 'same' : 'DIFFERENT'}`).join(' ') || 'none'}`);
for (const r of epochRows.filter(r => r.epoch >= FIRST_FULL_EPOCH - 1)) log(`EPOCH ${r.epoch} seed ${r.seed_daa}: rungs ${r.rungs.join('/')} n0 weakest up ${r.n0_up_weakest} down ${r.n0_down_weakest} | oracle up ${r.oracle_up_weakest} down ${r.oracle_down_weakest} rung ${r.oracle_step} ${r.stepped ? 'STEP' : ''} (${r.oracle_reason})`);
for (const [k, v] of Object.entries(checks)) if (!v) log(`FAILED CHECK ${k}`);
log(`summary: ${TMP}/summary.json`);
await stopAll();
process.exit(pass ? 0 : 1);

View file

@ -0,0 +1,289 @@
#!/usr/bin/env node
// ATTACK-PASS COPY (F10, 7 October 2026) of infra/fast-time/latency-ladder.mjs from branch ladder at 7003f9f5, taken
// verbatim except: (1) ROOT is found by walking up from this file to the tree that holds tools/finality-attacks/lib/rpc.mjs
// (the file lives at tools/attack/f10-ladder/ on the Mac and at attack-f10/ on the box), the two lib imports are dynamic;
// (2) the override file is this directory's copy of the ladder branch's override-60x.json (the attack-pass tree's copy
// lacks the latency_ladder fields); (3) ports, devnet suffix and data dir come from IGNEUM_F10_BASE / IGNEUM_F10_SUFFIX /
// IGNEUM_LADDER_TMP with the F10 defaults 29900 / 990 / /tmp/igneum-fast-time-attack-f10, so this network never collides
// with the ladder lane's (29720 / 972) or F7's (29800 / 980); (4) the binaries default to the box's ladder lane layout,
// read-only. The checks, the cases and the known-failed case are the original's, unchanged.
//
// The latency ladder's fast-time gate (docs/design/latency-ladder.md section 9; the class-v4-signal.mjs shape): a 3-node
// network on override-60x.json, class v4 from genesis (v3 and the v4 floor at 0, the class window 0: class signalling off,
// so the ladder opens the header's high byte on its own), the ladder active from DAA 0 with one window of --window DAA
// (default 60, one epoch; seven windows = 420 DAA, so the first epoch whose seed block has seven full windows below it is
// epoch 8 at DAA 480), each node's ladder signal set by IGNEUM_LADDER_SIGNAL (--signal a,b,c of up|down|none), one real CPU
// miner per node. Ports 29720 and up, network igneum-devnet-972, data /tmp/igneum-fast-time-ladder.
//
// The cases and the known-failed case:
// --signal up,up,none --expect no-step two of three miners signal up: about 67 percent, rung 0 must hold (run 10 epochs)
// --signal up,up,up --expect step all three: rung 1 (35 shadow passes) from epoch 8, the first with seven full
// windows, every miner's rung-1 program id equal to the CLI's --shadow-reps 35 id and
// unequal to the rung-0 id, and NO second step inside the next two epochs
// --signal up,up,none --expect step the known-failed case: the harness must report FAIL (no step happened)
//
// node infra/fast-time/latency-ladder.mjs --signal a,b,c --expect step|no-step [--window 60] [--secs 900] [--epochs 10]
// IGNEUMD, IGNEUM_MINER, IGNEUM_POW name the binaries (defaults: the ladder fork worktree's target/release and
// igneum-pow/target/release/igneum-pow, the layout on igneum-build-1 under /srv/builds/igneum-wt-ladder).
import { spawn, spawnSync } from 'node:child_process';
import { mkdirSync, rmSync, writeFileSync, readFileSync, openSync, existsSync } from 'node:fs';
import { dirname } from 'node:path';
const HERE = dirname(new URL(import.meta.url).pathname) + '/';
function findRoot(from) { let d = from; for (let i = 0; i < 6; i++) { if (existsSync(`${d}tools/finality-attacks/lib/rpc.mjs`)) return d; d = dirname(d.replace(/\/$/, '')) + '/'; } throw new Error('no tree with tools/finality-attacks/lib/rpc.mjs above ' + from); }
const ROOT = process.env.IGNEUM_ROOT ? process.env.IGNEUM_ROOT.replace(/\/?$/, '/') : findRoot(HERE);
const { connectRpc } = await import(`${ROOT}tools/finality-attacks/lib/rpc.mjs`);
const { devAddress } = await import(`${ROOT}tools/harness/lib/address.mjs`);
const FILE = `${HERE}override-60x.json`;
const BIN = process.env.IGNEUM_LADDER_BIN || '/srv/builds/igneum-wt-ladder/vendor/igneum-node-ladder/target/release';
const IGNEUMD = process.env.IGNEUMD || `${BIN}/igneumd`;
const CPU_MINER = process.env.IGNEUM_MINER || `${BIN}/igneum-miner`;
const IGNEUM_POW = process.env.IGNEUM_POW || '/srv/builds/igneum-wt-ladder/igneum-pow/target/release/igneum-pow';
const TMP = process.env.IGNEUM_LADDER_TMP || '/tmp/igneum-fast-time-attack-f10';
const BASE = +(process.env.IGNEUM_F10_BASE || 29900), SUFFIX = +(process.env.IGNEUM_F10_SUFFIX || 990);
const NEVER = '18446744073709551615';
const RUNG0 = 27, RUNG1 = 35, WINDOWS = 7, THRESHOLD = 9000;
const args = process.argv.slice(2);
const flag = (name, dflt) => { const i = args.indexOf(`--${name}`); return i >= 0 ? +args[i + 1] : dflt; };
const sflag = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : null; };
const GENESIS_BITS = flag('genesis-bits', 0x1f010000);
const SECS = flag('secs', 900);
const EPOCHS = flag('epochs', 10);
const WINDOW = flag('window', 60);
const SIGNAL = (sflag('signal') || 'up,up,up').split(',').map(s => s.trim().toLowerCase());
const EXPECT = sflag('expect') || 'step';
if (!['step', 'no-step'].includes(EXPECT) || SIGNAL.length !== 3 || !SIGNAL.every(s => ['up', 'down', 'none'].includes(s))) { console.error('usage: --signal a,b,c (up|down|none) --expect step|no-step'); process.exit(2); }
const started = [];
const log = (...a) => console.log(new Date().toISOString().slice(11, 23), ...a);
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
for (const b of [IGNEUMD, CPU_MINER]) if (!existsSync(b)) { console.error(`missing ${b}`); process.exit(2); }
rmSync(TMP, { recursive: true, force: true }); mkdirSync(TMP, { recursive: true });
const baseText = readFileSync(FILE, 'utf8');
const field = (name) => { const m = new RegExp(`"${name}":\\s*([0-9]+)`).exec(baseText); return m ? +m[1] : undefined; };
const EPOCH = field('pow_epoch_blocks');
const LEAD = field('pow_epoch_lead');
const DAY_MS = field('pow_day_ms');
// the first epoch whose seed block (the last chain block below L*e - lead) can have DAA >= 7 x WINDOW: L*e - lead - 1 >= 7W
let FIRST_STEP_EPOCH = 0;
while (FIRST_STEP_EPOCH * EPOCH - LEAD - 1 < WINDOWS * WINDOW) FIRST_STEP_EPOCH++;
function mergeOverrideText(text, fields) {
let out = text;
for (const k of Object.keys(fields)) out = out.replace(new RegExp(`\\s*"${k}":\\s*[^,}\\n]+,?`), '');
const extra = Object.entries(fields).map(([k, v]) => `"${k}": ${typeof v === 'string' && !/^\d+$/.test(v) ? JSON.stringify(v) : v}`).join(', ');
return out.replace(/,?\s*}\s*$/, `,\n ${extra}\n}\n`);
}
const override = `${TMP}/override.json`;
writeFileSync(override, mergeOverrideText(baseText, {
genesis_bits: GENESIS_BITS, skip_proof_of_work: false,
program_class_v3_activation_daa: '0', program_class_v4_activation_daa: '0', program_class_v4_signal_window_daa: '0',
latency_ladder_activation_daa: '0', latency_ladder_window_daa: String(WINDOW),
}));
log(`signals ${SIGNAL.join('/')}, expect ${EXPECT}; class v4 from genesis, the ladder active from DAA 0, window ${WINDOW} DAA x ${WINDOWS} (the first epoch that can step is ${FIRST_STEP_EPOCH}, DAA ${FIRST_STEP_EPOCH * EPOCH}); ${EPOCH} DAA per epoch, lead ${LEAD}; run ${SECS} s or ${EPOCHS} epochs`);
class Node {
constructor(i, connect = []) {
this.i = i; this.grpcPort = BASE + i * 10; this.p2pPort = BASE + i * 10 + 1; this.jsonPort = BASE + i * 10 + 2;
this.connect = connect; this.dir = `${TMP}/n${i}`; this.logFile = `${this.dir}/node.log`;
}
get grpc() { return `grpc://127.0.0.1:${this.grpcPort}`; }
async start() {
mkdirSync(this.dir, { recursive: true });
const a = ['--devnet', `--devnet-suffix=${SUFFIX}`, '--nodnsseed', '--disable-upnp', '--nologfiles', '--enable-unsynced-mining', '--utxoindex',
`--appdir=${this.dir}`, `--rpclisten=127.0.0.1:${this.grpcPort}`, `--rpclisten-json=127.0.0.1:${this.jsonPort}`,
`--listen=127.0.0.1:${this.p2pPort}`, `--override-params-file=${override}`, '--loglevel=info', '--yes'];
if (this.connect.length) a.push(`--connect=${this.connect.join(',')}`); else a.push('--outpeers=0');
const out = openSync(this.logFile, 'a');
// the node's own ladder signal: what its templates carry in bits 15 and 14
this.proc = spawn(IGNEUMD, a, { stdio: ['ignore', out, out], env: { ...process.env, IGNEUM_LADDER_SIGNAL: SIGNAL[this.i] } });
started.push(this.proc);
await sleep(1200);
this.rpc = await connectRpc(`ws://127.0.0.1:${this.jsonPort}`);
log(`n${this.i} up pid ${this.proc.pid} json ${this.jsonPort} p2p ${this.p2pPort}, signals ${SIGNAL[this.i]}`);
return this;
}
grepLog(re) { try { return readFileSync(this.logFile, 'utf8').split('\n').filter(l => re.test(l)); } catch { return []; } }
}
function miner(bin, argv, name, env = {}) {
const out = openSync(`${TMP}/${name}.log`, 'a');
const p = spawn(bin, argv, { stdio: ['ignore', out, out], env: { ...process.env, ...env } });
started.push(p);
return p;
}
async function stopAll() {
for (const p of started.reverse()) { try { p.kill('SIGINT'); } catch { } }
await sleep(1500);
for (const p of started) { try { p.kill('SIGKILL'); } catch { } }
}
process.on('SIGINT', async () => { await stopAll(); process.exit(130); });
process.on('unhandledRejection', async (e) => { log(`FAILED: ${e?.stack || e}`); await stopAll(); process.exit(3); });
const minerLog = (i) => { try { return readFileSync(`${TMP}/cpu${i}.log`, 'utf8').split('\n'); } catch { return []; } };
const STEP_LINE = /Latency ladder step by miner signal: epoch (\d+) moves to rung (\d+) \((\d+) shadow passes, from rung (\d+)\): (up|down) in each of (\d+) consecutive windows of (\d+) DAA .*weakest up (\d+) bps, weakest down (\d+) bps/;
const LADDER_LINE = /Latency ladder from the override file/;
const ACTIVE_LINE = /Latency ladder active: rungs/;
const OWN_LINE = /Latency ladder signal from IGNEUM_LADDER_SIGNAL: this node signals (\w+)/;
const t0 = Date.now();
const since = () => ((Date.now() - t0) / 1000).toFixed(1);
const n0 = await new Node(0).start();
const n1 = await new Node(1, [`127.0.0.1:${n0.p2pPort}`]).start();
const n2 = await new Node(2, [`127.0.0.1:${n0.p2pPort}`]).start();
const nodes = [n0, n1, n2];
for (const n of nodes) log(`n${n.i}: ${n.grepLog(LADDER_LINE).map(l => l.replace(/^.*?(Latency ladder from)/, '$1'))[0] || '(no ladder line)'} | ${n.grepLog(OWN_LINE).map(l => l.replace(/^.*?(this node signals)/, '$1'))[0] || '(no signal line)'}`);
log(`n0 digest: ${n0.grepLog(/Consensus params digest/).map(l => l.replace(/^.*?digest: /, '').slice(0, 16)).join(' ')}`);
nodes.forEach((n, i) => miner(CPU_MINER, ['mine', n.grpc, '1', String(SECS), `cpu${i}`, '--engine', 'igneum-pow', '--payout-label', `cpu${i}`, '--status-secs', '30', '--no-vote'], `cpu${i}`, { IGNEUM_POW_DAY_MS: String(DAY_MS) }));
const pay = devAddress('fast-time-ladder');
const epochs = new Map();
let firstStep = null, lastEpoch = -1, lastReport = 0, lastDaa = 0, endAt = null;
const samples = [];
while (Date.now() - t0 < SECS * 1000) {
await sleep(1000);
let daa = null, epoch = null, cls = null, reps = null, nextReps = null, step = null, nextStep = null, up = null, upWeak = null, down = null, sig = null, stepEpoch = null, eraSeed = null;
try {
const t = await n0.rpc.call('getBlockTemplate', { payAddress: pay, extraData: [] });
const pe = t.powEpoch || t.pow_epoch || {};
daa = pe.virtualDaaScore ?? t.block?.header?.daaScore; epoch = pe.epochIndex; cls = pe.programClass; eraSeed = pe.eraSeed;
reps = pe.latencyLadderReps; nextReps = pe.nextLatencyLadderReps; step = pe.latencyLadderStep; nextStep = pe.nextLatencyLadderStep;
up = pe.latencyLadderUpBps; upWeak = pe.latencyLadderUpWeakestBps; down = pe.latencyLadderDownBps; sig = pe.latencyLadderSignal; stepEpoch = pe.latencyLadderStepEpoch;
} catch (e) { log(`template: ${e.message}`); }
if (epoch != null && epoch !== lastEpoch) {
epochs.set(epoch, { class: cls, reps, step, firstSeenDaa: daa, at: +since(), eraSeed: eraSeed == null ? null : String(eraSeed), up_bps: up, up_weakest_bps: upWeak, down_bps: down, step_epoch: stepEpoch ?? null });
log(`epoch ${lastEpoch} -> ${epoch} at daa ${daa}, ${since()} s: template class ${cls} rung ${step} (${reps} passes), next rung ${nextStep} (${nextReps}), up ${up} bps (weakest of ${WINDOWS}: ${upWeak}), down ${down} bps, this node signals ${sig}, step took effect at epoch ${stepEpoch ?? 'none'}`);
if (firstStep == null && step > 0) { firstStep = { epoch, daa, step, reps, at: +since() }; log(`LADDER STEP: the template is rung ${step} (${reps} shadow passes) from epoch ${epoch} (daa ${daa}) at ${since()} s wall`); }
lastEpoch = epoch;
}
lastDaa = daa ?? lastDaa;
if (Date.now() - lastReport > 15000) {
lastReport = Date.now();
const counts = await Promise.all(nodes.map(async n => { try { const d = await n.rpc.call('getBlockDagInfo'); return `${d.blockCount}/${String(d.sink).slice(0, 8)}`; } catch { return '?'; } }));
log(`t=${since()} s daa ${daa} epoch ${epoch} rung ${step} (${reps}) up ${up} bps weakest ${upWeak} blocks/sink per node ${counts.join(' ')}`);
samples.push({ t: +since(), daa, epoch, step, reps, up_bps: up, up_weakest_bps: upWeak, nodes: counts });
}
// the end: two epochs after a step (to show no second step), or --epochs epochs when no step is expected
if (firstStep != null && daa != null && daa >= (firstStep.epoch + 2) * EPOCH + LEAD) { endAt = +since(); break; }
if (firstStep == null && daa != null && daa >= EPOCHS * EPOCH) { endAt = +since(); break; }
}
await sleep(3000);
const dag = await Promise.all(nodes.map(async n => { try { return await n.rpc.call('getBlockDagInfo'); } catch (e) { return { error: e.message }; } }));
const genesis = dag[0].pruningPointHash;
async function allBlocks(n) {
const out = []; let low = genesis; const seen = new Set();
for (let round = 0; round < 500; round++) {
const r = await n.rpc.call('getBlocks', { lowHash: low, includeBlocks: true, includeTransactions: false });
const blocks = r.blocks || [];
let added = 0;
for (const b of blocks) { const h = b.verboseData?.hash || b.header?.hash; if (seen.has(h)) continue; seen.add(h); out.push({ hash: h, daa: +b.header.daaScore, version: +b.header.version, chain: !!b.verboseData?.isChainBlock }); added++; }
if (!blocks.length || added === 0) break;
low = (r.blockHashes || []).at(-1) || blocks.at(-1).verboseData?.hash; if (!low) break;
}
return out;
}
let blocks = [];
try { blocks = await allBlocks(n0); } catch (e) { log(`getBlocks: ${e.message}`); }
const BOUNDARY = firstStep ? firstStep.epoch * EPOCH : Infinity;
const before = blocks.filter(b => b.daa < BOUNDARY), after = blocks.filter(b => b.daa >= BOUNDARY);
// the ladder bits on the chain: bit 15 up, bit 14 down; the object byte (bits 8 to 13) must be 0 (class signalling off)
const bitsOf = (v) => (v & 0x8000) ? 'up' : (v & 0x4000) ? 'down' : 'none';
const ladderBits = blocks.reduce((m, b) => { const k = bitsOf(b.version); m[k] = (m[k] || 0) + 1; return m; }, {});
const upShareOnChain = blocks.length ? Math.round(10000 * (blocks.filter(b => bitsOf(b.version) === 'up').length) / blocks.length) : 0;
const objectBytes = blocks.reduce((m, b) => { const v = (b.version >> 8) & 0x3f; m[v] = (m[v] || 0) + 1; return m; }, {});
// genesis carries header version 0 (genesis.rs), every mined block the block version 2 in its low byte
const lowBytes = new Set(blocks.filter(b => b.daa > 0).map(b => b.version & 0xff));
const programs = new Map();
for (const i of [0, 1, 2]) for (const l of minerLog(i)) {
const m = /epoch seed ([0-9a-f]{64}) day (\d+) \(daa (\d+)\): program and 256 MiB cache ready in ([\d.]+) ms; class (v\d) program id ([0-9a-f]{16})/.exec(l);
if (!m) continue;
const k = m[1]; const e = programs.get(k) || { seed: k.slice(0, 16), epoch: Math.floor(+m[3] / EPOCH), class: m[5], id: m[6], miners: new Set() };
if (e.id !== m[6] || e.class !== m[5]) e.disagree = true;
e.miners.add(i); programs.set(k, e);
}
const programRows = [...programs.values()].sort((a, b) => a.epoch - b.epoch).map(p => ({ epoch: p.epoch, class: p.class, program_id: p.id, seed: p.seed, miners: p.miners.size, disagree: !!p.disagree }));
function cliId(seedHex, eraHex, reps) {
if (!existsSync(IGNEUM_POW)) return null;
const r = spawnSync(IGNEUM_POW, ['show', '--epoch-hex', seedHex, '--program-class', 'v4', '--era-hex', eraHex, '--shadow-reps', String(reps)], { encoding: 'utf8' });
const m = /program id ([0-9a-f]{16})/.exec(r.stdout || '');
return m ? m[1] : null;
}
const idRows = [];
for (const [k, e] of programs) {
const ep = epochs.get(e.epoch);
if (!ep || ep.eraSeed == null) continue;
const reps = ep.reps ?? 0;
idRows.push({ epoch: e.epoch, seed: e.seed, reps, miners_id: e.id, miners: e.miners.size, cli_rung0: cliId(k, ep.eraSeed, 0), cli_at_reps: cliId(k, ep.eraSeed, reps) });
}
const steppedRows = idRows.filter(r => r.reps !== RUNG0 && r.reps !== 0);
const accepted = [0, 1, 2].map(i => minerLog(i).filter(l => /ACCEPTED block/.test(l)).length);
const rejectedMiner = [0, 1, 2].map(i => minerLog(i).filter(l => /rejected nonce=|submit error/.test(l)));
const rejectedNode = nodes.map(n => n.grepLog(/PoW rejected|Rejected block|rejected block/i));
const stepLines = nodes.map(n => n.grepLog(STEP_LINE).map(l => l.replace(/^.*?(Latency ladder step by miner signal)/, '$1')));
const firstStepLine = stepLines.map(ls => ls[0] || null);
const stepEpochs = firstStepLine.map(l => { const m = l && STEP_LINE.exec(l); return m ? +m[1] : null; });
const stepRungs = firstStepLine.map(l => { const m = l && STEP_LINE.exec(l); return m ? +m[2] : null; });
const stepWeakestUp = firstStepLine.map(l => { const m = l && STEP_LINE.exec(l); return m ? +m[8] : null; });
const sinks = dag.map(d => String(d.sink || '?').slice(0, 16));
const counts = dag.map(d => d.blockCount ?? '?');
const maxEpochSeen = Math.max(-1, ...epochs.keys());
const repsSeen = [...epochs.values()].map(e => e.reps);
const afterStep = firstStep ? [...epochs.entries()].filter(([e]) => e > firstStep.epoch).map(([, v]) => v.step) : [];
const common = {
zero_rejected_by_miners: rejectedMiner.every(r => r.length === 0),
zero_rejected_by_nodes: rejectedNode.every(r => r.length === 0),
sinks_agree: new Set(sinks).size === 1,
block_counts_agree: new Set(counts.map(String)).size === 1,
miners_agree_on_every_program: programRows.every(p => !p.disagree),
ladder_line_on_every_node: nodes.every(n => n.grepLog(LADDER_LINE).length > 0 && n.grepLog(ACTIVE_LINE).length > 0),
every_node_signals_its_bits: nodes.every((n, i) => n.grepLog(OWN_LINE).some(l => OWN_LINE.exec(l)[1] === SIGNAL[i])),
every_epoch_class_v4: [...epochs.values()].every(e => e.class === 4),
rung0_ids_equal_the_cli_rung0_id: idRows.filter(r => r.reps === RUNG0).length > 0 && idRows.filter(r => r.reps === RUNG0).every(r => r.cli_rung0 != null && r.cli_rung0 === r.miners_id),
};
// every block carries block version 2, an object byte of 0 (class signalling off) and the ladder bits of one of the three
// nodes; genesis, made before any node, is the one bit-less block when every node signals (the first run of the known-failed
// case, 22:26Z, failed this check on genesis's version 0 in the low-byte test, a harness fault, not a chain one)
const noneNodes = SIGNAL.filter(s => s === 'none').length;
common.chain_carries_the_bits = blocks.length > 0 && [...lowBytes].every(v => v === 2) && Object.keys(objectBytes).every(v => +v === 0)
&& Object.keys(ladderBits).every(k => SIGNAL.includes(k) || k === 'none') && (noneNodes > 0 || (ladderBits.none || 0) === 1);
let checks;
if (EXPECT === 'step') {
checks = {
...common,
template_stepped_to_rung_1: firstStep != null && firstStep.step === 1 && firstStep.reps === RUNG1,
stepped_at_the_first_full_window_epoch: firstStep != null && firstStep.epoch === FIRST_STEP_EPOCH,
step_line_on_every_node_same_epoch: stepEpochs.every(e => e != null) && new Set(stepEpochs).size === 1 && stepEpochs[0] === (firstStep && firstStep.epoch) && stepRungs.every(r => r === 1),
weakest_up_at_or_above_threshold: stepWeakestUp.every(s => s != null && s >= THRESHOLD),
no_second_step_inside_seven_windows: firstStep != null && afterStep.length >= 2 && afterStep.every(s => s === 1) && stepLines.every(ls => ls.length === 1),
blocks_on_both_sides: before.length > 0 && after.length > 0,
rung1_ids_equal_the_cli_rung1_id: steppedRows.length > 0 && steppedRows.every(r => r.reps === RUNG1 && r.cli_at_reps != null && r.cli_at_reps === r.miners_id && r.miners === 3),
rung1_ids_differ_from_the_same_seed_rung0_id: steppedRows.length > 0 && steppedRows.every(r => r.cli_rung0 != null && r.cli_rung0 !== r.miners_id),
};
} else {
checks = {
...common,
template_never_above_rung_0: firstStep == null && repsSeen.every(r => r === RUNG0 || r === 0),
no_step_line_on_any_node: stepLines.every(ls => ls.length === 0),
ran_the_epochs: maxEpochSeen >= EPOCHS - 1,
passed_the_first_full_window_epoch: maxEpochSeen >= FIRST_STEP_EPOCH,
up_share_under_threshold_on_chain: upShareOnChain < THRESHOLD,
};
}
const pass = Object.values(checks).every(Boolean);
const summary = {
pass, expect: EXPECT, signals: SIGNAL, checks, window: WINDOW, windows: WINDOWS, threshold_bps: THRESHOLD, epoch_blocks: EPOCH, lead: LEAD, first_step_epoch: FIRST_STEP_EPOCH,
node: IGNEUMD, miner: CPU_MINER, pow: IGNEUM_POW, template_step: firstStep, run_ended_at_s: endAt, final_daa: lastDaa, max_epoch_seen: maxEpochSeen,
epochs: Object.fromEntries([...epochs.entries()].map(([k, v]) => [k, v])),
blocks: { total: blocks.length, before_boundary: before.length, after_boundary: after.length, ladder_bits: ladderBits, object_bytes: objectBytes, up_share_bps_on_chain: upShareOnChain },
programs: programRows, program_id_rows: idRows, accepted_per_miner: accepted,
rejected_by_miners: rejectedMiner.map(r => r.length), rejected_by_nodes: rejectedNode.map(r => r.length),
sinks, block_counts: counts, step_lines: stepLines, samples,
};
writeFileSync(`${TMP}/summary.json`, JSON.stringify(summary, null, 2));
log(`SUMMARY ${pass ? 'PASS' : 'FAIL'} (expect ${EXPECT}, signals ${SIGNAL.join('/')}): ${firstStep ? `rung ${firstStep.step} (${firstStep.reps} passes) from epoch ${firstStep.epoch} at DAA ${firstStep.daa}` : 'no step'}; epochs seen ${[...epochs.entries()].map(([e, v]) => `e${e}:r${v.step}:${v.up_weakest_bps}bps`).join(' ')}; chain bits ${JSON.stringify(ladderBits)} (${upShareOnChain} bps up); blocks ${before.length} / ${after.length}; rejected miners ${rejectedMiner.map(r => r.length).join('/')} nodes ${rejectedNode.map(r => r.length).join('/')}; sinks ${sinks.join(' ')} at ${counts.join('/')}`);
for (const r of idRows) log(`PROGRAM ID epoch ${r.epoch} seed ${r.seed} reps ${r.reps}: miners ${r.miners_id} (${r.miners} of 3) cli at reps ${r.cli_at_reps} cli rung 0 ${r.cli_rung0}`);
for (const [k, v] of Object.entries(checks)) if (!v) log(`FAILED CHECK ${k}`);
log(`summary: ${TMP}/summary.json`);
await stopAll();
process.exit(pass ? 0 : 1);

View file

@ -0,0 +1,77 @@
{
"timestamp_deviation_tolerance": 132,
"past_median_time_window_size": 27,
"difficulty_window_size": 661,
"min_difficulty_window_size": 150,
"difficulty_rule": "igneum-dual",
"coinbase_payload_script_public_key_max_len": 150,
"max_coinbase_payload_len": 16384,
"max_tx_inputs": 1000,
"max_tx_outputs": 1000,
"max_signature_script_len": 250000,
"max_script_public_key_len": 10000,
"mass_per_tx_byte": 1,
"mass_per_script_pub_key_byte": 10,
"mass_per_sig_op": 1000,
"block_mass_limits": { "compute": 500000, "storage": 500000, "transient": 1000000 },
"block_lane_limits": { "lanes_per_block": 50, "gas_per_lane": 1000000000 },
"storage_mass_parameter": 1000000000000,
"deflationary_phase_daa_score": 0,
"pre_deflationary_phase_base_subsidy": 50000000000,
"skip_proof_of_work": false,
"max_block_level": 250,
"pruning_proof_m": 1000,
"blockrate": {
"target_time_per_block": 1000,
"ghostdag_k": 18,
"past_median_time_sample_rate": 10,
"difficulty_sample_rate": 4,
"max_block_parents": 10,
"mergeset_size_limit": 180,
"merge_depth": 60,
"finality_depth": 720,
"pruning_depth": 13838,
"coinbase_maturity": 2
},
"pre_crescendo_target_time_per_block": 1000,
"crescendo_activation": 0,
"genesis_bits": 487587840,
"finality": {
"checkpoint_interval": 30,
"checkpoint_depth": 20,
"weight_window": 120,
"dust": 5,
"presence_window": 1,
"aggregators": 8,
"equivocation_ban": 120,
"min_daa": 120,
"aggregator_fallback": 1,
"certificate_fold": 3
},
"pow_epoch_blocks": 60,
"pow_epoch_lead": 10,
"pow_day_ms": 1440000,
"difficulty_v2_activation_daa": 18446744073709551615,
"difficulty_v3_activation_daa": 18446744073709551615,
"finality_daa_rule_activation_daa": 18446744073709551615,
"proving_v0_activation_daa": 18446744073709551615,
"finality_v3_activation_daa": 18446744073709551615,
"program_class_v3_activation_daa": 18446744073709551615,
"program_class_v4_activation_daa": 18446744073709551615,
"program_class_v4_signal_window_daa": 120,
"latency_ladder": [{"reps": 27, "admissible": true}, {"reps": 35, "admissible": true}, {"reps": 53, "admissible": true}, {"reps": 88, "admissible": false}, {"reps": 173, "admissible": false}, {"reps": 267, "admissible": false}],
"latency_ladder_activation_daa": 18446744073709551615,
"latency_ladder_window_daa": 120,
"proving_v1_fresh_rule_daa": 18446744073709551615,
"exec_restart_number": 18446744073709551615,
"exec_restart_hash": "",
"exec_restart_state_root": "",
"exec_restart_trust_daa": 18446744073709551615,
"pow_genesis_dataset_log2": 28,
"proving_v1_activation_daa": 18446744073709551615,
"proving_v1_segment_blocks": 8,
"proving_v1_unproven_daa": 10,
"proving_v1_aggregator_share_bps": 1000,
"fees_v1_activation_daa": 0,
"fees": {"pgas": {"version": 1, "cycles_per_pgas": 1000, "intrinsic_pgas_per_tx": 300, "modexp_base": 10, "modexp_per_byte_numer": 1, "modexp_per_byte_denom": 10}, "block_proving_gas_limit": 120000, "shard_proving_gas_budget": 30000, "min_execution_base_fee_wei": 100000000000, "min_proving_base_fee_wei": 10000000000000, "initial_execution_base_fee_wei": 100000000000, "initial_proving_base_fee_wei": 10000000000000, "base_fee_change_denominator": 8}
}

View file

@ -0,0 +1,17 @@
#!/bin/bash
# F10 queue on igneum-build-1: the five runs in order, each its own shared-lock chunk (run-one.sh), under one nohup;
# the known-failed and known-pass cases of the ladder lane's harness first (the harness is trusted only once both
# fire), then the exact-share cases. Pid in runs/queue.pid, the queue log in runs/queue.log, QUEUE-END at the end.
D=/srv/builds/igneum-wt-attack/attack-f10
mkdir -p "$D/runs"
nohup bash -c "
cd $D
bash run-one.sh baseline-fail latency-ladder.mjs --signal up,up,none --expect step --epochs 10 --secs 1500; echo \"queue: baseline-fail rc=\$?\"
bash run-one.sh baseline-pass latency-ladder.mjs --signal up,up,up --expect step --secs 1500; echo \"queue: baseline-pass rc=\$?\"
bash run-one.sh exact-89 ladder-exact.mjs --case eighty-nine --window 100 --schedule 0:up:11,1200:up:10 --expect-steps 31:1,43:2 --epochs 45 --rate 8 --secs 1500; echo \"queue: exact-89 rc=\$?\"
bash run-one.sh exact-down ladder-exact.mjs --case down --window 100 --schedule 0:up:0,720:down:11,1500:down:10 --expect-steps 12:1,36:0 --epochs 50 --rate 8 --restart 2@1000,1@2300,2@2700 --secs 1500; echo \"queue: exact-down rc=\$?\"
bash run-one.sh exact-floor ladder-exact.mjs --case floor --window 100 --schedule 0:down:0 --epochs 20 --rate 8 --secs 600; echo \"queue: exact-floor rc=\$?\"
echo \"QUEUE-END \$(date -u +%FT%TZ)\"
" > "$D/runs/queue.log" 2>&1 &
echo $! > "$D/runs/queue.pid"
echo "queue started pid $(cat "$D/runs/queue.pid")"

View file

@ -0,0 +1,15 @@
#!/bin/bash
# F10 queue 2 on igneum-build-1: waits for queue 1's own QUEUE-END marker, then re-runs the two exact-share cases on the
# fixed driver (the first exact-89 run of 08:48Z tripped three harness faults: blockCount excludes genesis, the 'ban'
# grep matched the finality parameter line, the template's weakest is the sink-anchored live tally). exact-floor of
# queue 1 already runs the fixed file. Pid in runs/queue2.pid, log runs/queue2.log, QUEUE2-END at the end.
D=/srv/builds/igneum-wt-attack/attack-f10
nohup bash -c "
cd $D
while ! grep -q QUEUE-END $D/runs/queue.log 2>/dev/null; do sleep 10; done
bash run-one.sh exact-89b ladder-exact.mjs --case eighty-nine --window 100 --schedule 0:up:11,1200:up:10 --expect-steps 31:1,43:2 --epochs 45 --rate 8 --secs 1500; echo \"queue2: exact-89b rc=\$?\"
bash run-one.sh exact-downb ladder-exact.mjs --case down --window 100 --schedule 0:up:0,720:down:11,1500:down:10 --expect-steps 12:1,36:0 --epochs 50 --rate 8 --restart 2@1000,1@2300,2@2700 --secs 1500; echo \"queue2: exact-downb rc=\$?\"
echo \"QUEUE2-END \$(date -u +%FT%TZ)\"
" > "$D/runs/queue2.log" 2>&1 &
echo $! > "$D/runs/queue2.pid"
echo "queue 2 started pid $(cat "$D/runs/queue2.pid")"

View file

@ -0,0 +1,20 @@
#!/bin/bash
# F10 box runner (igneum-build-1): one harness run under nohup, on the F10 cores, holding the box's measure file SHARED
# for the length of the run (every run is capped under 30 minutes by its own --secs), with a pid file and a log in the
# F10 scratch dir. Usage, on the box:
# run-box.sh <run name> <harness .mjs> [args...]
# The run's log is runs/<name>.log, its pid runs/<name>.pid, its summary copied to runs/<name>.json and its node logs to
# runs/<name>-n{0,1,2}.log when it ends. Never touches another lane's directory, port or network.
set -u
D=/srv/builds/igneum-wt-attack/attack-f10
NAME="$1"; shift
HARNESS="$1"; shift
mkdir -p "$D/runs"
export IGNEUM_ROOT=/srv/builds/igneum-wt-attack/
export IGNEUM_LADDER_TMP="/tmp/igneum-fast-time-attack-f10"
export IGNEUM_F10_BASE=29900 IGNEUM_F10_SUFFIX=990
# the inner command: the run, then the copies, then an END line keyed to this run's name (every wait keys on it)
INNER="cd $D && nice -n 10 taskset -c 38-39,86-87 node $D/$HARNESS $* ; rc=\$? ; cp $IGNEUM_LADDER_TMP/summary.json $D/runs/$NAME.json 2>/dev/null ; for i in 0 1 2; do cp $IGNEUM_LADDER_TMP/n\$i/node.log $D/runs/$NAME-n\$i.log 2>/dev/null; done ; for i in 0 1 2; do cp $IGNEUM_LADDER_TMP/cpu\$i.log $D/runs/$NAME-cpu\$i.log 2>/dev/null; done ; echo \"F10-END $NAME rc=\$rc \$(date -u +%FT%TZ)\""
nohup flock -s /srv/builds/_locks/measure -c "$INNER" > "$D/runs/$NAME.log" 2>&1 &
echo $! > "$D/runs/$NAME.pid"
echo "started $NAME pid $(cat "$D/runs/$NAME.pid") log $D/runs/$NAME.log"

View file

@ -0,0 +1,19 @@
#!/bin/bash
# F10 foreground single run on igneum-build-1: holds the box measure file SHARED for this run only (each run is capped
# under 30 minutes by its own --secs), on the F10 cores, with per-run log, summary and node-log copies and an END
# marker keyed to the run's name. Usage: run-one.sh <name> <harness .mjs> [args...]
set -u
D=/srv/builds/igneum-wt-attack/attack-f10
NAME="$1"; shift
HARNESS="$1"; shift
mkdir -p "$D/runs"
export IGNEUM_ROOT=/srv/builds/igneum-wt-attack/
export IGNEUM_LADDER_TMP="/tmp/igneum-fast-time-attack-f10"
export IGNEUM_F10_BASE=29900 IGNEUM_F10_SUFFIX=990
echo "F10-START $NAME $(date -u +%FT%TZ) $HARNESS $*" > "$D/runs/$NAME.log"
flock -s /srv/builds/_locks/measure -c "cd $D && nice -n 10 taskset -c 38-39,86-87 node $D/$HARNESS $*" >> "$D/runs/$NAME.log" 2>&1
rc=$?
cp "$IGNEUM_LADDER_TMP/summary.json" "$D/runs/$NAME.json" 2>/dev/null
for i in 0 1 2; do cp "$IGNEUM_LADDER_TMP/n$i/node.log" "$D/runs/$NAME-n$i.log" 2>/dev/null; cp "$IGNEUM_LADDER_TMP/cpu$i.log" "$D/runs/$NAME-cpu$i.log" 2>/dev/null; done
echo "F10-END $NAME rc=$rc $(date -u +%FT%TZ)" >> "$D/runs/$NAME.log"
exit $rc

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,86 @@
F10-START baseline-fail 2026-10-07T08:16:33Z latency-ladder.mjs --signal up,up,none --expect step --epochs 10 --secs 1500
08:25:51.376 signals up/up/none, expect step; class v4 from genesis, the ladder active from DAA 0, window 60 DAA x 7 (the first epoch that can step is 8, DAA 480); 60 DAA per epoch, lead 10; run 1500 s or 10 epochs
08:25:52.614 n0 up pid 1285690 json 29902 p2p 29901, signals up
08:25:53.822 n1 up pid 1285840 json 29912 p2p 29911, signals up
08:25:55.033 n2 up pid 1285931 json 29922 p2p 29921, signals none
08:25:55.034 n0: Latency ladder from the override file: rungs 27, 35, 53, [88], [173], [267] shadow passes (brackets: inadmissible, never entered), active from epoch 0 (DAA score 0 rounded up to the epoch boundary at 0), one rung per decision at 90 percent of blue blocks in each of 7 consecutive windows of 60 DAA ending at an epoch's seed block | this node signals up
08:25:55.034 n1: Latency ladder from the override file: rungs 27, 35, 53, [88], [173], [267] shadow passes (brackets: inadmissible, never entered), active from epoch 0 (DAA score 0 rounded up to the epoch boundary at 0), one rung per decision at 90 percent of blue blocks in each of 7 consecutive windows of 60 DAA ending at an epoch's seed block | this node signals up
08:25:55.035 n2: Latency ladder from the override file: rungs 27, 35, 53, [88], [173], [267] shadow passes (brackets: inadmissible, never entered), active from epoch 0 (DAA score 0 rounded up to the epoch boundary at 0), one rung per decision at 90 percent of blue blocks in each of 7 consecutive windows of 60 DAA ending at an epoch's seed block | this node signals none
08:25:55.035 n0 digest: 9cd5b78208d88c86
08:25:56.045 epoch -1 -> 0 at daa 0, 4.7 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 0 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
08:25:56.047 t=4.7 s daa 0 epoch 0 rung 0 (27) up 0 bps weakest 0 blocks/sink per node 0/234e082d 0/234e082d 0/234e082d
08:26:11.079 t=19.7 s daa 3 epoch 0 rung 0 (27) up 5000 bps weakest 0 blocks/sink per node 3/2ce6d047 3/2ce6d047 3/2ce6d047
08:26:26.090 t=34.7 s daa 7 epoch 0 rung 0 (27) up 6666 bps weakest 0 blocks/sink per node 7/e35b9207 7/e35b9207 7/e35b9207
08:26:41.100 t=49.7 s daa 9 epoch 0 rung 0 (27) up 7500 bps weakest 0 blocks/sink per node 9/35cf800a 9/35cf800a 9/35cf800a
08:26:56.114 t=64.7 s daa 13 epoch 0 rung 0 (27) up 7500 bps weakest 0 blocks/sink per node 13/d9974e7e 13/d9974e7e 13/d9974e7e
08:27:11.125 t=79.7 s daa 26 epoch 0 rung 0 (27) up 6800 bps weakest 0 blocks/sink per node 26/431fa088 26/431fa088 26/431fa088
08:27:26.136 t=94.8 s daa 45 epoch 0 rung 0 (27) up 6590 bps weakest 0 blocks/sink per node 45/3e4772da 45/3e4772da 45/3e4772da
08:27:41.144 epoch 0 -> 1 at daa 60, 109.8 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 6610 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
08:27:41.144 t=109.8 s daa 60 epoch 1 rung 0 (27) up 6610 bps weakest 0 blocks/sink per node 60/f7b69587 60/f7b69587 60/f7b69587
08:27:56.157 t=124.8 s daa 77 epoch 1 rung 0 (27) up 6000 bps weakest 0 blocks/sink per node 77/de946972 77/de946972 77/de946972
08:28:11.167 t=139.8 s daa 92 epoch 1 rung 0 (27) up 6440 bps weakest 0 blocks/sink per node 92/31fdd824 92/31fdd824 92/31fdd824
08:28:26.180 t=154.8 s daa 106 epoch 1 rung 0 (27) up 6000 bps weakest 0 blocks/sink per node 106/ce48457c 106/ce48457c 106/ce48457c
08:28:41.190 epoch 1 -> 2 at daa 120, 169.8 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 6166 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
08:28:41.191 t=169.8 s daa 120 epoch 2 rung 0 (27) up 6166 bps weakest 0 blocks/sink per node 120/60fcfcf5 120/60fcfcf5 120/60fcfcf5
08:28:56.204 t=184.8 s daa 134 epoch 2 rung 0 (27) up 6333 bps weakest 0 blocks/sink per node 134/eddb214c 134/eddb214c 134/eddb214c
08:29:11.216 t=199.8 s daa 156 epoch 2 rung 0 (27) up 6000 bps weakest 0 blocks/sink per node 156/02076a52 156/02076a52 156/02076a52
08:29:26.227 t=214.8 s daa 167 epoch 2 rung 0 (27) up 6500 bps weakest 0 blocks/sink per node 167/f0a4e1ac 167/f0a4e1ac 167/f0a4e1ac
08:29:41.240 t=229.9 s daa 175 epoch 2 rung 0 (27) up 6666 bps weakest 0 blocks/sink per node 175/e3cea22c 175/e3cea22c 175/e3cea22c
08:29:50.254 epoch 2 -> 3 at daa 181, 238.9 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 6500 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
08:29:56.257 t=244.9 s daa 188 epoch 3 rung 0 (27) up 6500 bps weakest 0 blocks/sink per node 188/013bc365 188/013bc365 188/013bc365
08:30:11.267 t=259.9 s daa 208 epoch 3 rung 0 (27) up 7000 bps weakest 0 blocks/sink per node 208/76eb278e 208/76eb278e 208/76eb278e
08:30:26.281 t=274.9 s daa 219 epoch 3 rung 0 (27) up 7166 bps weakest 0 blocks/sink per node 219/ec530140 219/ec530140 219/ec530140
08:30:41.294 t=289.9 s daa 232 epoch 3 rung 0 (27) up 6666 bps weakest 0 blocks/sink per node 232/326f9f1b 232/326f9f1b 232/326f9f1b
08:30:48.300 epoch 3 -> 4 at daa 240, 296.9 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 5833 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
08:30:56.305 t=304.9 s daa 246 epoch 4 rung 0 (27) up 5666 bps weakest 0 blocks/sink per node 246/b83c1108 246/b83c1108 246/b83c1108
08:31:11.315 t=319.9 s daa 257 epoch 4 rung 0 (27) up 5166 bps weakest 0 blocks/sink per node 257/b6a39dad 257/b6a39dad 257/b6a39dad
08:31:26.333 t=335.0 s daa 270 epoch 4 rung 0 (27) up 5000 bps weakest 0 blocks/sink per node 270/008a606a 270/008a606a 270/008a606a
08:31:41.352 t=350.0 s daa 290 epoch 4 rung 0 (27) up 5500 bps weakest 0 blocks/sink per node 290/fe5c94ce 290/fe5c94ce 290/fe5c94ce
08:31:52.362 epoch 4 -> 5 at daa 300, 361.0 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 6500 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
08:31:56.367 t=365.0 s daa 301 epoch 5 rung 0 (27) up 6666 bps weakest 0 blocks/sink per node 301/9d58bfd0 301/9d58bfd0 301/9d58bfd0
08:32:11.385 t=380.0 s daa 321 epoch 5 rung 0 (27) up 8166 bps weakest 0 blocks/sink per node 321/c72b7c01 321/c72b7c01 321/c72b7c01
08:32:26.398 t=395.0 s daa 340 epoch 5 rung 0 (27) up 7833 bps weakest 0 blocks/sink per node 340/e8bf8d9c 340/e8bf8d9c 340/e8bf8d9c
08:32:41.410 t=410.0 s daa 356 epoch 5 rung 0 (27) up 6666 bps weakest 0 blocks/sink per node 356/00645f4b 356/00645f4b 356/00645f4b
08:32:44.412 epoch 5 -> 6 at daa 360, 413.0 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 6833 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
08:32:56.425 t=425.0 s daa 374 epoch 6 rung 0 (27) up 6610 bps weakest 5000 blocks/sink per node 374/d412bee1 374/d412bee1 374/d412bee1
08:33:11.439 t=440.1 s daa 384 epoch 6 rung 0 (27) up 6500 bps weakest 5000 blocks/sink per node 384/6746169e 384/6746169e 384/6746169e
08:33:26.451 t=455.1 s daa 404 epoch 6 rung 0 (27) up 5833 bps weakest 5333 blocks/sink per node 404/faf0d12e 404/faf0d12e 404/faf0d12e
08:33:41.465 t=470.1 s daa 417 epoch 6 rung 0 (27) up 6333 bps weakest 6000 blocks/sink per node 417/47fa9eb5 417/47fa9eb5 417/47fa9eb5
08:33:43.466 epoch 6 -> 7 at daa 420, 472.1 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 6000 bps (weakest of 7: 5833), down 0 bps, this node signals 1, step took effect at epoch 0
08:33:56.480 t=485.1 s daa 428 epoch 7 rung 0 (27) up 6000 bps weakest 5500 blocks/sink per node 428/6c05dfc6 428/6c05dfc6 428/6c05dfc6
08:34:11.494 t=500.1 s daa 442 epoch 7 rung 0 (27) up 6000 bps weakest 5000 blocks/sink per node 442/93a972b3 442/93a972b3 442/93a972b3
08:34:26.509 t=515.1 s daa 463 epoch 7 rung 0 (27) up 5833 bps weakest 5500 blocks/sink per node 463/e67477c8 463/e67477c8 463/e67477c8
08:34:36.519 epoch 7 -> 8 at daa 480, 525.1 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 6333 bps (weakest of 7: 5833), down 0 bps, this node signals 1, step took effect at epoch 0
08:34:41.524 t=530.1 s daa 487 epoch 8 rung 0 (27) up 6333 bps weakest 5666 blocks/sink per node 487/7ba8eb08 487/7ba8eb08 487/7ba8eb08
08:34:56.540 t=545.2 s daa 503 epoch 8 rung 0 (27) up 6500 bps weakest 5000 blocks/sink per node 503/2ba5d515 503/2ba5d515 503/2ba5d515
08:35:11.556 t=560.2 s daa 513 epoch 8 rung 0 (27) up 6779 bps weakest 5000 blocks/sink per node 513/cacafd09 513/cacafd09 513/cacafd09
08:35:26.572 t=575.2 s daa 524 epoch 8 rung 0 (27) up 6833 bps weakest 5333 blocks/sink per node 524/7dffb8ff 524/7dffb8ff 524/7dffb8ff
08:35:41.595 t=590.2 s daa 536 epoch 8 rung 0 (27) up 7166 bps weakest 6166 blocks/sink per node 536/b0cee90c 536/b0cee90c 536/b0cee90c
08:35:44.598 epoch 8 -> 9 at daa 541, 593.2 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 7166 bps (weakest of 7: 5833), down 0 bps, this node signals 1, step took effect at epoch 0
08:35:56.612 t=605.2 s daa 555 epoch 9 rung 0 (27) up 6333 bps weakest 5166 blocks/sink per node 555/7cee781c 555/7cee781c 555/7cee781c
08:36:11.629 t=620.3 s daa 569 epoch 9 rung 0 (27) up 6333 bps weakest 5000 blocks/sink per node 569/84b5661b 569/84b5661b 569/84b5661b
08:36:26.648 t=635.3 s daa 583 epoch 9 rung 0 (27) up 6333 bps weakest 5500 blocks/sink per node 583/7ebb276a 583/7ebb276a 583/7ebb276a
08:36:41.665 t=650.3 s daa 596 epoch 9 rung 0 (27) up 5500 bps weakest 5500 blocks/sink per node 596/1d79b610 596/1d79b610 596/1d79b610
08:36:45.668 epoch 9 -> 10 at daa 601, 654.3 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 5500 bps (weakest of 7: 5500), down 0 bps, this node signals 1, step took effect at epoch 0
08:36:48.929 SUMMARY FAIL (expect step, signals up/up/none): no step; epochs seen e0:r0:0bps e1:r0:0bps e2:r0:0bps e3:r0:0bps e4:r0:0bps e5:r0:0bps e6:r0:0bps e7:r0:5833bps e8:r0:5833bps e9:r0:5833bps e10:r0:5500bps; chain bits {"none":221,"up":385} (6353 bps up); blocks 606 / 0; rejected miners 0/0/0 nodes 0/0/0; sinks 4a7f20ccc84f9b47 4a7f20ccc84f9b47 4a7f20ccc84f9b47 at 605/605/605
08:36:48.929 PROGRAM ID epoch 0 seed 234e082d653dc69d reps 27: miners 26fc8f3decca98ed (3 of 3) cli at reps 26fc8f3decca98ed cli rung 0 26fc8f3decca98ed
08:36:48.929 PROGRAM ID epoch 1 seed 8a90ebffc428b9ab reps 27: miners 3a978116045da3ae (3 of 3) cli at reps 3a978116045da3ae cli rung 0 3a978116045da3ae
08:36:48.929 PROGRAM ID epoch 2 seed 3866c986c34bb33c reps 27: miners d80aca35e09dd260 (3 of 3) cli at reps d80aca35e09dd260 cli rung 0 d80aca35e09dd260
08:36:48.929 PROGRAM ID epoch 3 seed c02a106b5ca7f60d reps 27: miners 91bc369535cd56d7 (3 of 3) cli at reps 91bc369535cd56d7 cli rung 0 91bc369535cd56d7
08:36:48.929 PROGRAM ID epoch 4 seed b52c27cf319bf7f9 reps 27: miners 447a4c87189a8a0a (3 of 3) cli at reps 447a4c87189a8a0a cli rung 0 447a4c87189a8a0a
08:36:48.929 PROGRAM ID epoch 5 seed fe5c94cee5620198 reps 27: miners 35f341c3c84ad66b (3 of 3) cli at reps 35f341c3c84ad66b cli rung 0 35f341c3c84ad66b
08:36:48.929 PROGRAM ID epoch 6 seed 42a032b249a605a0 reps 27: miners d37e87123db5a373 (3 of 3) cli at reps d37e87123db5a373 cli rung 0 d37e87123db5a373
08:36:48.929 PROGRAM ID epoch 7 seed 97bda73bce598a8d reps 27: miners d9f431b37edb5971 (3 of 3) cli at reps d9f431b37edb5971 cli rung 0 d9f431b37edb5971
08:36:48.929 PROGRAM ID epoch 8 seed 858ed61beb59b503 reps 27: miners f11fc44ff40aeee4 (3 of 3) cli at reps f11fc44ff40aeee4 cli rung 0 f11fc44ff40aeee4
08:36:48.929 PROGRAM ID epoch 9 seed 62fb0adc111ba5f7 reps 27: miners 35f29a405af3f58f (3 of 3) cli at reps 35f29a405af3f58f cli rung 0 35f29a405af3f58f
08:36:48.929 PROGRAM ID epoch 10 seed e574fc3a9a210cf3 reps 27: miners de3d5259e9f233cf (3 of 3) cli at reps de3d5259e9f233cf cli rung 0 de3d5259e9f233cf
08:36:48.929 FAILED CHECK template_stepped_to_rung_1
08:36:48.929 FAILED CHECK stepped_at_the_first_full_window_epoch
08:36:48.929 FAILED CHECK step_line_on_every_node_same_epoch
08:36:48.929 FAILED CHECK weakest_up_at_or_above_threshold
08:36:48.929 FAILED CHECK no_second_step_inside_seven_windows
08:36:48.929 FAILED CHECK blocks_on_both_sides
08:36:48.929 FAILED CHECK rung1_ids_equal_the_cli_rung1_id
08:36:48.929 FAILED CHECK rung1_ids_differ_from_the_same_seed_rung0_id
08:36:48.929 summary: /tmp/igneum-fast-time-attack-f10/summary.json
F10-END baseline-fail rc=1 2026-10-07T08:36:50Z

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,80 @@
F10-START baseline-pass 2026-10-07T08:36:50Z latency-ladder.mjs --signal up,up,up --expect step --secs 1500
08:36:50.495 signals up/up/up, expect step; class v4 from genesis, the ladder active from DAA 0, window 60 DAA x 7 (the first epoch that can step is 8, DAA 480); 60 DAA per epoch, lead 10; run 1500 s or 10 epochs
08:36:51.735 n0 up pid 1395636 json 29902 p2p 29901, signals up
08:36:52.943 n1 up pid 1395749 json 29912 p2p 29911, signals up
08:36:54.152 n2 up pid 1395888 json 29922 p2p 29921, signals up
08:36:54.153 n0: Latency ladder from the override file: rungs 27, 35, 53, [88], [173], [267] shadow passes (brackets: inadmissible, never entered), active from epoch 0 (DAA score 0 rounded up to the epoch boundary at 0), one rung per decision at 90 percent of blue blocks in each of 7 consecutive windows of 60 DAA ending at an epoch's seed block | this node signals up
08:36:54.153 n1: Latency ladder from the override file: rungs 27, 35, 53, [88], [173], [267] shadow passes (brackets: inadmissible, never entered), active from epoch 0 (DAA score 0 rounded up to the epoch boundary at 0), one rung per decision at 90 percent of blue blocks in each of 7 consecutive windows of 60 DAA ending at an epoch's seed block | this node signals up
08:36:54.153 n2: Latency ladder from the override file: rungs 27, 35, 53, [88], [173], [267] shadow passes (brackets: inadmissible, never entered), active from epoch 0 (DAA score 0 rounded up to the epoch boundary at 0), one rung per decision at 90 percent of blue blocks in each of 7 consecutive windows of 60 DAA ending at an epoch's seed block | this node signals up
08:36:54.154 n0 digest: 9cd5b78208d88c86
08:36:55.163 epoch -1 -> 0 at daa 0, 4.7 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 0 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
08:36:55.164 t=4.7 s daa 0 epoch 0 rung 0 (27) up 0 bps weakest 0 blocks/sink per node 0/234e082d 0/234e082d 0/234e082d
08:37:10.199 t=19.7 s daa 1 epoch 0 rung 0 (27) up 0 bps weakest 0 blocks/sink per node 1/a8797408 1/a8797408 1/a8797408
08:37:25.212 t=34.7 s daa 3 epoch 0 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 3/99d88d5c 3/99d88d5c 3/99d88d5c
08:37:40.232 t=49.7 s daa 8 epoch 0 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 8/b66102e2 8/b66102e2 8/b66102e2
08:37:55.242 t=64.7 s daa 14 epoch 0 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 14/6ec1b561 14/6ec1b561 14/6ec1b561
08:38:10.255 t=79.8 s daa 25 epoch 0 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 25/0b1878c5 25/0b1878c5 25/0b1878c5
08:38:25.264 t=94.8 s daa 42 epoch 0 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 42/bf305704 42/bf305704 42/bf305704
08:38:40.275 t=109.8 s daa 57 epoch 0 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 57/45b61b8d 57/45b61b8d 57/45b61b8d
08:38:42.277 epoch 0 -> 1 at daa 60, 111.8 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 10000 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
08:38:55.288 t=124.8 s daa 71 epoch 1 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 71/a2718afc 71/a2718afc 71/a2718afc
08:39:10.299 t=139.8 s daa 87 epoch 1 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 87/796afbcc 87/796afbcc 87/796afbcc
08:39:25.314 t=154.8 s daa 110 epoch 1 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 110/5baec92c 110/5baec92c 110/5baec92c
08:39:39.323 epoch 1 -> 2 at daa 120, 168.8 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 10000 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
08:39:40.323 t=169.8 s daa 121 epoch 2 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 121/d5574cb3 122/b546bf0c 121/d5574cb3
08:39:55.332 t=184.8 s daa 139 epoch 2 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 139/043d5aa6 139/043d5aa6 139/043d5aa6
08:40:10.341 t=199.8 s daa 152 epoch 2 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 152/599c675f 152/599c675f 152/599c675f
08:40:25.351 t=214.9 s daa 164 epoch 2 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 164/322b6efe 164/322b6efe 164/322b6efe
08:40:40.360 t=229.9 s daa 178 epoch 2 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 178/e7c0aba8 178/e7c0aba8 178/e7c0aba8
08:40:41.361 epoch 2 -> 3 at daa 180, 230.9 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 10000 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
08:40:55.378 t=244.9 s daa 188 epoch 3 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 188/9481dd3f 188/9481dd3f 188/9481dd3f
08:41:10.398 t=259.9 s daa 200 epoch 3 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 200/5dead8ba 200/5dead8ba 200/5dead8ba
08:41:25.414 t=274.9 s daa 225 epoch 3 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 225/4c41a1d3 225/4c41a1d3 225/4c41a1d3
08:41:38.428 epoch 3 -> 4 at daa 240, 287.9 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 10000 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
08:41:40.431 t=289.9 s daa 241 epoch 4 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 241/e7765769 241/e7765769 241/e7765769
08:41:55.453 t=305.0 s daa 260 epoch 4 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 260/c57aa107 260/c57aa107 260/c57aa107
08:42:10.468 t=320.0 s daa 271 epoch 4 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 271/069e628d 271/069e628d 271/069e628d
08:42:25.480 t=335.0 s daa 282 epoch 4 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 282/9aa9faf7 282/9aa9faf7 282/9aa9faf7
08:42:38.491 epoch 4 -> 5 at daa 300, 348.0 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 10000 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
08:42:40.494 t=350.0 s daa 303 epoch 5 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 303/036e8208 303/036e8208 303/036e8208
08:42:55.507 t=365.0 s daa 315 epoch 5 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 315/c3d254b9 315/c3d254b9 315/c3d254b9
08:43:10.526 t=380.0 s daa 335 epoch 5 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 335/286c5498 335/286c5498 335/286c5498
08:43:25.542 t=395.0 s daa 346 epoch 5 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 346/e0e21741 346/e0e21741 346/e0e21741
08:43:40.557 t=410.1 s daa 357 epoch 5 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 357/ca1c8b8f 357/ca1c8b8f 357/ca1c8b8f
08:43:44.560 epoch 5 -> 6 at daa 360, 414.1 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 10000 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
08:43:55.569 t=425.1 s daa 375 epoch 6 rung 0 (27) up 10000 bps weakest 10000 blocks/sink per node 375/e87525b5 375/e87525b5 375/e87525b5
08:44:10.586 t=440.1 s daa 390 epoch 6 rung 0 (27) up 10000 bps weakest 10000 blocks/sink per node 390/a45c44d8 390/a45c44d8 390/a45c44d8
08:44:25.600 t=455.1 s daa 400 epoch 6 rung 0 (27) up 10000 bps weakest 10000 blocks/sink per node 400/6cce2763 400/6cce2763 400/6cce2763
08:44:40.614 t=470.1 s daa 416 epoch 6 rung 0 (27) up 10000 bps weakest 10000 blocks/sink per node 416/0a2e7eeb 416/0a2e7eeb 416/0a2e7eeb
08:44:44.618 epoch 6 -> 7 at daa 420, 474.1 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 10000 bps (weakest of 7: 10000), down 0 bps, this node signals 1, step took effect at epoch 0
08:44:55.630 t=485.1 s daa 433 epoch 7 rung 0 (27) up 10000 bps weakest 10000 blocks/sink per node 433/ed18ef74 433/ed18ef74 433/ed18ef74
08:45:10.649 t=500.2 s daa 448 epoch 7 rung 0 (27) up 10000 bps weakest 10000 blocks/sink per node 448/db177cde 448/db177cde 448/db177cde
08:45:25.665 t=515.2 s daa 460 epoch 7 rung 0 (27) up 10000 bps weakest 10000 blocks/sink per node 460/a16c30f3 460/a16c30f3 460/a16c30f3
08:45:40.685 t=530.2 s daa 473 epoch 7 rung 0 (27) up 10000 bps weakest 10000 blocks/sink per node 473/9e318d4e 473/9e318d4e 473/9e318d4e
08:45:48.692 epoch 7 -> 8 at daa 480, 538.2 s: template class 4 rung 1 (35 passes), next rung 1 (35), up 10000 bps (weakest of 7: 10000), down 0 bps, this node signals 1, step took effect at epoch 8
08:45:48.692 LADDER STEP: the template is rung 1 (35 shadow passes) from epoch 8 (daa 480) at 538.2 s wall
08:45:55.702 t=545.2 s daa 484 epoch 8 rung 1 (35) up 10000 bps weakest 10000 blocks/sink per node 484/3c3bde14 484/3c3bde14 484/3c3bde14
08:46:10.718 t=560.2 s daa 496 epoch 8 rung 1 (35) up 10000 bps weakest 10000 blocks/sink per node 496/a3e22ead 496/a3e22ead 496/a3e22ead
08:46:25.736 t=575.2 s daa 509 epoch 8 rung 1 (35) up 10000 bps weakest 10000 blocks/sink per node 509/a6894f37 509/a6894f37 509/a6894f37
08:46:40.751 t=590.3 s daa 530 epoch 8 rung 1 (35) up 10000 bps weakest 10000 blocks/sink per node 530/864b08a8 530/864b08a8 530/864b08a8
08:46:52.764 epoch 8 -> 9 at daa 541, 602.3 s: template class 4 rung 1 (35 passes), next rung 1 (35), up 10000 bps (weakest of 7: 10000), down 0 bps, this node signals 1, step took effect at epoch 8
08:46:55.768 t=605.3 s daa 545 epoch 9 rung 1 (35) up 10000 bps weakest 10000 blocks/sink per node 545/8c57227f 545/8c57227f 545/8c57227f
08:47:10.783 t=620.3 s daa 558 epoch 9 rung 1 (35) up 10000 bps weakest 10000 blocks/sink per node 558/5e55e59f 558/5e55e59f 558/5e55e59f
08:47:25.812 t=635.3 s daa 579 epoch 9 rung 1 (35) up 10000 bps weakest 10000 blocks/sink per node 579/8b12070c 579/8b12070c 579/8b12070c
08:47:40.830 t=650.3 s daa 596 epoch 9 rung 1 (35) up 10000 bps weakest 10000 blocks/sink per node 596/44c62365 596/44c62365 596/44c62365
08:47:45.835 epoch 9 -> 10 at daa 600, 655.3 s: template class 4 rung 1 (35 passes), next rung 1 (35), up 10000 bps (weakest of 7: 10000), down 0 bps, this node signals 1, step took effect at epoch 8
08:47:55.848 t=665.4 s daa 611 epoch 10 rung 1 (35) up 10000 bps weakest 10000 blocks/sink per node 611/79e66905 611/79e66905 611/79e66905
08:47:59.168 SUMMARY PASS (expect step, signals up/up/up): rung 1 (35 passes) from epoch 8 at DAA 480; epochs seen e0:r0:0bps e1:r0:0bps e2:r0:0bps e3:r0:0bps e4:r0:0bps e5:r0:0bps e6:r0:0bps e7:r0:10000bps e8:r1:10000bps e9:r1:10000bps e10:r1:10000bps; chain bits {"none":1,"up":612} (9984 bps up); blocks 481 / 132; rejected miners 0/0/0 nodes 0/0/0; sinks 41e819449a6ca5dc 41e819449a6ca5dc 41e819449a6ca5dc at 612/612/612
08:47:59.168 PROGRAM ID epoch 0 seed 234e082d653dc69d reps 27: miners 26fc8f3decca98ed (3 of 3) cli at reps 26fc8f3decca98ed cli rung 0 26fc8f3decca98ed
08:47:59.168 PROGRAM ID epoch 1 seed 7fbb09450059b438 reps 27: miners 3b62266974d2e42a (3 of 3) cli at reps 3b62266974d2e42a cli rung 0 3b62266974d2e42a
08:47:59.168 PROGRAM ID epoch 2 seed 5baec92c296b962b reps 27: miners 8f59392326c19dca (3 of 3) cli at reps 8f59392326c19dca cli rung 0 8f59392326c19dca
08:47:59.168 PROGRAM ID epoch 3 seed 3e7ebed4bc730a58 reps 27: miners 034f77fe48cebd85 (3 of 3) cli at reps 034f77fe48cebd85 cli rung 0 034f77fe48cebd85
08:47:59.168 PROGRAM ID epoch 4 seed 74eefce7802b43c8 reps 27: miners 50d23e98cbc5ed4e (3 of 3) cli at reps 50d23e98cbc5ed4e cli rung 0 50d23e98cbc5ed4e
08:47:59.168 PROGRAM ID epoch 5 seed 1979464e20cdeebd reps 27: miners 1e1e4940f5342624 (3 of 3) cli at reps 1e1e4940f5342624 cli rung 0 1e1e4940f5342624
08:47:59.168 PROGRAM ID epoch 6 seed 16b3a8b0bdc0d904 reps 27: miners 2b6aeae806ef68cc (3 of 3) cli at reps 2b6aeae806ef68cc cli rung 0 2b6aeae806ef68cc
08:47:59.168 PROGRAM ID epoch 7 seed c00c2c27d84a045e reps 27: miners c67e2382ad740b46 (3 of 3) cli at reps c67e2382ad740b46 cli rung 0 c67e2382ad740b46
08:47:59.168 PROGRAM ID epoch 8 seed 2439d34623c0a379 reps 35: miners 218fa530b4c599b0 (3 of 3) cli at reps 218fa530b4c599b0 cli rung 0 5c5a326a31a4795d
08:47:59.168 PROGRAM ID epoch 9 seed 864b08a80e5eccfa reps 35: miners 8f30ce6666b4ea8f (3 of 3) cli at reps 8f30ce6666b4ea8f cli rung 0 c73f3c63daac3748
08:47:59.168 PROGRAM ID epoch 10 seed e205f78b1fdf728e reps 35: miners e2ea0a1ea8b4ca44 (3 of 3) cli at reps e2ea0a1ea8b4ca44 cli rung 0 626455372164a1b5
08:47:59.168 summary: /tmp/igneum-fast-time-attack-f10/summary.json
F10-END baseline-pass rc=0 2026-10-07T08:48:00Z

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,117 @@
F10-START exact-89 2026-10-07T08:48:00Z ladder-exact.mjs --case eighty-nine --window 100 --schedule 0:up:11,1200:up:10 --expect-steps 31:1,43:2 --epochs 45 --rate 8 --secs 1500
08:48:00.787 case eighty-nine: schedule 0:up:11 1200:up:10 (W 100, 7 windows, threshold 9000 bps); expect steps e31->r1 e43->r2; restarts none; 60 DAA per epoch, lead 10, first epoch with seven full windows 12; ladder 27, 35, 53, [88], [173], [267]; 8 blocks/s, up to 45 epochs or 1500 s
08:48:02.337 n0 up (start 1) pid 1517960 json 29902 p2p 29901
08:48:03.847 n1 up (start 1) pid 1518410 json 29912 p2p 29911
08:48:05.356 n2 up (start 1) pid 1518683 json 29922 p2p 29921
08:48:05.357 n0: Latency ladder active: rungs 27, 35, 53, [88], [173], [267] shadow passes, this node signals none (header version bits 15 and 14)
08:48:05.357 n1: Latency ladder active: rungs 27, 35, 53, [88], [173], [267] shadow passes, this node signals none (header version bits 15 and 14)
08:48:05.357 n2: Latency ladder active: rungs 27, 35, 53, [88], [173], [267] shadow passes, this node signals none (header version bits 15 and 14)
08:48:05.357 n0 digest: 5d6d9f3d3f504b84
08:48:05.859 first submit at daa 0 sig none version 0x2: {"report":{"type":"success"}}
08:48:06.563 epoch -1 -> 0 at daa 5, 5.8 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e0 | n2 rung 0 e0
08:48:06.564 t=5.8 s produced 5 daa 5 epoch 0 rungs 0/0/0 blocks/sink 5/599aa716 5/599aa716 5/599aa716 disagreements 0 rejected 0
08:48:14.132 epoch 0 -> 1 at daa 60, 13.3 s: n0 rung 0 (27) next 0 (27) up 8305 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e1 | n2 rung 0 e1
08:48:22.426 epoch 1 -> 2 at daa 120, 21.6 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e2 | n2 rung 0 e2
08:48:27.262 t=26.5 s produced 155 daa 155 epoch 2 rungs 0/0/0 blocks/sink 155/f4a2228f 155/f4a2228f 155/f4a2228f disagreements 0 rejected 0
08:48:30.775 epoch 2 -> 3 at daa 180, 30.0 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e3 | n2 rung 0 e3
08:48:39.154 epoch 3 -> 4 at daa 240, 38.4 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e4 | n2 rung 0 e4
08:48:47.495 epoch 4 -> 5 at daa 300, 46.7 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e5 | n2 rung 0 e5
08:48:47.496 t=46.7 s produced 300 daa 300 epoch 5 rungs 0/0/0 blocks/sink 300/4d46f194 300/4d46f194 300/4d46f194 disagreements 0 rejected 0
08:48:55.846 epoch 5 -> 6 at daa 360, 55.1 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e6 | n2 rung 0 e6
08:49:04.310 epoch 6 -> 7 at daa 420, 63.5 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e7 | n2 rung 0 e7
08:49:07.794 t=67.0 s produced 445 daa 445 epoch 7 rungs 0/0/0 blocks/sink 445/05fee8ed 445/05fee8ed 445/05fee8ed disagreements 0 rejected 0
08:49:12.619 epoch 7 -> 8 at daa 480, 71.8 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e8 | n2 rung 0 e8
08:49:21.021 epoch 8 -> 9 at daa 540, 80.2 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e9 | n2 rung 0 e9
08:49:28.071 t=87.3 s produced 590 daa 590 epoch 9 rungs 0/0/0 blocks/sink 590/657210b2 590/657210b2 590/657210b2 disagreements 0 rejected 0
08:49:29.473 epoch 9 -> 10 at daa 600, 88.7 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e10 | n2 rung 0 e10
08:49:37.842 epoch 10 -> 11 at daa 660, 97.1 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 8305 down 0 weakest 0 step epoch 0 | n1 rung 0 e11 | n2 rung 0 e11
08:49:46.193 epoch 11 -> 12 at daa 720, 105.4 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e12 | n2 rung 0 e12
08:49:48.270 t=107.5 s produced 735 daa 735 epoch 12 rungs 0/0/0 blocks/sink 735/3ba279d4 735/3ba279d4 735/3ba279d4 disagreements 0 rejected 0
08:49:54.513 epoch 12 -> 13 at daa 780, 113.7 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e13 | n2 rung 0 e13
08:50:02.803 epoch 13 -> 14 at daa 840, 122.0 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e14 | n2 rung 0 e14
08:50:08.371 t=127.6 s produced 880 daa 880 epoch 14 rungs 0/0/0 blocks/sink 880/3a265302 880/3a265302 880/3a265302 disagreements 0 rejected 0
08:50:11.166 epoch 14 -> 15 at daa 900, 130.4 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e15 | n2 rung 0 e15
08:50:19.466 epoch 15 -> 16 at daa 960, 138.7 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e16 | n2 rung 0 e16
08:50:27.896 epoch 16 -> 17 at daa 1020, 147.1 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e17 | n2 rung 0 e17
08:50:28.588 t=147.8 s produced 1025 daa 1025 epoch 17 rungs 0/0/0 blocks/sink 1025/8379e14f 1025/8379e14f 1025/8379e14f disagreements 0 rejected 0
08:50:36.237 epoch 17 -> 18 at daa 1080, 155.4 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e18 | n2 rung 0 e18
08:50:44.599 epoch 18 -> 19 at daa 1140, 163.8 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e19 | n2 rung 0 e19
08:50:48.822 t=168.0 s produced 1170 daa 1170 epoch 19 rungs 0/0/0 blocks/sink 1170/a99e9bc1 1170/a99e9bc1 1170/a99e9bc1 disagreements 0 rejected 0
08:50:52.982 epoch 19 -> 20 at daa 1200, 172.2 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e20 | n2 rung 0 e20
08:51:01.329 epoch 20 -> 21 at daa 1260, 180.5 s: n0 rung 0 (27) next 0 (27) up 9000 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e21 | n2 rung 0 e21
08:51:08.940 t=188.2 s produced 1315 daa 1315 epoch 21 rungs 0/0/0 blocks/sink 1315/46219009 1315/46219009 1315/46219009 disagreements 0 rejected 0
08:51:09.635 epoch 21 -> 22 at daa 1320, 188.8 s: n0 rung 0 (27) next 0 (27) up 9000 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e22 | n2 rung 0 e22
08:51:18.006 epoch 22 -> 23 at daa 1380, 197.2 s: n0 rung 0 (27) next 0 (27) up 9000 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e23 | n2 rung 0 e23
08:51:26.342 epoch 23 -> 24 at daa 1440, 205.6 s: n0 rung 0 (27) next 0 (27) up 9000 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e24 | n2 rung 0 e24
08:51:29.056 t=208.3 s produced 1460 daa 1460 epoch 24 rungs 0/0/0 blocks/sink 1460/029f2383 1460/029f2383 1460/029f2383 disagreements 0 rejected 0
08:51:34.490 epoch 24 -> 25 at daa 1500, 213.7 s: n0 rung 0 (27) next 0 (27) up 9000 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e25 | n2 rung 0 e25
08:51:42.687 epoch 25 -> 26 at daa 1560, 221.9 s: n0 rung 0 (27) next 0 (27) up 9000 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e26 | n2 rung 0 e26
08:51:49.663 t=228.9 s produced 1610 daa 1610 epoch 26 rungs 0/0/0 blocks/sink 1610/918a926c 1610/918a926c 1610/918a926c disagreements 0 rejected 0
08:51:51.041 epoch 26 -> 27 at daa 1620, 230.3 s: n0 rung 0 (27) next 0 (27) up 9000 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e27 | n2 rung 0 e27
08:51:59.386 epoch 27 -> 28 at daa 1680, 238.6 s: n0 rung 0 (27) next 0 (27) up 9000 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e28 | n2 rung 0 e28
08:52:07.775 epoch 28 -> 29 at daa 1740, 247.0 s: n0 rung 0 (27) next 0 (27) up 9000 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e29 | n2 rung 0 e29
08:52:09.878 t=249.1 s produced 1755 daa 1755 epoch 29 rungs 0/0/0 blocks/sink 1755/fae8b0f3 1755/fae8b0f3 1755/fae8b0f3 disagreements 0 rejected 0
08:52:16.189 epoch 29 -> 30 at daa 1800, 255.4 s: n0 rung 0 (27) next 0 (27) up 9000 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e30 | n2 rung 0 e30
08:52:24.638 epoch 30 -> 31 at daa 1860, 263.8 s: n0 rung 1 (35) next 1 (35) up 9000 weakest 9000 down 0 weakest 0 step epoch 31 | n1 rung 1 e31 | n2 rung 1 e31
08:52:30.237 t=269.4 s produced 1900 daa 1900 epoch 31 rungs 1/1/1 blocks/sink 1900/f91b0889 1900/f91b0889 1900/f91b0889 disagreements 0 rejected 0
08:52:33.028 epoch 31 -> 32 at daa 1920, 272.2 s: n0 rung 1 (35) next 1 (35) up 9000 weakest 9000 down 0 weakest 0 step epoch 31 | n1 rung 1 e32 | n2 rung 1 e32
08:52:41.451 epoch 32 -> 33 at daa 1980, 280.7 s: n0 rung 1 (35) next 1 (35) up 9000 weakest 9000 down 0 weakest 0 step epoch 31 | n1 rung 1 e33 | n2 rung 1 e33
08:52:49.666 epoch 33 -> 34 at daa 2040, 288.9 s: n0 rung 1 (35) next 1 (35) up 9000 weakest 9000 down 0 weakest 0 step epoch 31 | n1 rung 1 e34 | n2 rung 1 e34
08:52:50.345 t=289.6 s produced 2045 daa 2045 epoch 34 rungs 1/1/1 blocks/sink 2045/60c07f5c 2045/60c07f5c 2045/60c07f5c disagreements 0 rejected 0
08:52:57.887 epoch 34 -> 35 at daa 2100, 297.1 s: n0 rung 1 (35) next 1 (35) up 9000 weakest 9000 down 0 weakest 0 step epoch 31 | n1 rung 1 e35 | n2 rung 1 e35
08:53:06.142 epoch 35 -> 36 at daa 2160, 305.4 s: n0 rung 1 (35) next 1 (35) up 9000 weakest 9000 down 0 weakest 0 step epoch 31 | n1 rung 1 e36 | n2 rung 1 e36
08:53:10.968 t=310.2 s produced 2195 daa 2195 epoch 36 rungs 1/1/1 blocks/sink 2195/7b1e57df 2195/7b1e57df 2195/7b1e57df disagreements 0 rejected 0
08:53:14.372 epoch 36 -> 37 at daa 2220, 313.6 s: n0 rung 1 (35) next 1 (35) up 9000 weakest 9000 down 0 weakest 0 step epoch 31 | n1 rung 1 e37 | n2 rung 1 e37
08:53:22.593 epoch 37 -> 38 at daa 2280, 321.8 s: n0 rung 1 (35) next 1 (35) up 9000 weakest 9000 down 0 weakest 0 step epoch 31 | n1 rung 1 e38 | n2 rung 1 e38
08:53:30.854 epoch 38 -> 39 at daa 2340, 330.1 s: n0 rung 1 (35) next 1 (35) up 9000 weakest 9000 down 0 weakest 0 step epoch 31 | n1 rung 1 e39 | n2 rung 1 e39
08:53:31.542 t=330.8 s produced 2345 daa 2345 epoch 39 rungs 1/1/1 blocks/sink 2345/42b4944e 2345/42b4944e 2345/42b4944e disagreements 0 rejected 0
08:53:39.082 epoch 39 -> 40 at daa 2400, 338.3 s: n0 rung 1 (35) next 1 (35) up 9000 weakest 9000 down 0 weakest 0 step epoch 31 | n1 rung 1 e40 | n2 rung 1 e40
08:53:47.348 epoch 40 -> 41 at daa 2460, 346.6 s: n0 rung 1 (35) next 1 (35) up 9000 weakest 9000 down 0 weakest 0 step epoch 31 | n1 rung 1 e41 | n2 rung 1 e41
08:53:52.215 t=351.4 s produced 2495 daa 2495 epoch 41 rungs 1/1/1 blocks/sink 2495/63304b1d 2495/63304b1d 2495/63304b1d disagreements 0 rejected 0
08:53:55.727 epoch 41 -> 42 at daa 2520, 354.9 s: n0 rung 1 (35) next 1 (35) up 9000 weakest 9000 down 0 weakest 0 step epoch 31 | n1 rung 1 e42 | n2 rung 1 e42
08:54:04.166 epoch 42 -> 43 at daa 2580, 363.4 s: n0 rung 2 (53) next 2 (53) up 9000 weakest 9000 down 0 weakest 0 step epoch 43 | n1 rung 2 e43 | n2 rung 2 e43
08:54:12.634 epoch 43 -> 44 at daa 2640, 371.8 s: n0 rung 2 (53) next 2 (53) up 9000 weakest 9000 down 0 weakest 0 step epoch 43 | n1 rung 2 e44 | n2 rung 2 e44
08:54:12.635 t=371.8 s produced 2640 daa 2640 epoch 44 rungs 2/2/2 blocks/sink 2640/694617ae 2640/694617ae 2640/694617ae disagreements 0 rejected 0
08:54:21.056 epoch 44 -> 45 at daa 2700, 380.3 s: n0 rung 2 (53) next 2 (53) up 9000 weakest 9000 down 0 weakest 0 step epoch 43 | n1 rung 2 e45 | n2 rung 2 e45
08:54:21.056 production ended at 380.3 s: 2700 blocks, last daa 2699; settling 4 s
08:54:25.455 SUMMARY FAIL (case eighty-nine): steps e31->r1 e43->r2 (oracle e31->r1 e43->r2, expected e31->r1 e43->r2); 2701 blocks (linear) bits {"none":283,"up":2418} both-bits 135; rejected 0; disagreements 0; sinks e2b40e9f e2b40e9f e2b40e9f at 2700/2700/2700; restarts none
08:54:25.455 EPOCH 11 seed 649: rungs 0/0/0 n0 weakest up 8305 down 0 | oracle up 7959 down 0 rung 0 (windows not full)
08:54:25.455 EPOCH 12 seed 709: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
08:54:25.455 EPOCH 13 seed 769: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
08:54:25.455 EPOCH 14 seed 829: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
08:54:25.455 EPOCH 15 seed 889: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
08:54:25.455 EPOCH 16 seed 949: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
08:54:25.455 EPOCH 17 seed 1009: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
08:54:25.455 EPOCH 18 seed 1069: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
08:54:25.455 EPOCH 19 seed 1129: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
08:54:25.455 EPOCH 20 seed 1189: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
08:54:25.455 EPOCH 21 seed 1249: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
08:54:25.455 EPOCH 22 seed 1309: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
08:54:25.455 EPOCH 23 seed 1369: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
08:54:25.455 EPOCH 24 seed 1429: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
08:54:25.455 EPOCH 25 seed 1489: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
08:54:25.455 EPOCH 26 seed 1549: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
08:54:25.455 EPOCH 27 seed 1609: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
08:54:25.455 EPOCH 28 seed 1669: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
08:54:25.455 EPOCH 29 seed 1729: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
08:54:25.455 EPOCH 30 seed 1789: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
08:54:25.455 EPOCH 31 seed 1849: rungs 1/1/1 n0 weakest up 9000 down 0 | oracle up 9000 down 0 rung 1 STEP (up)
08:54:25.455 EPOCH 32 seed 1909: rungs 1/1/1 n0 weakest up 9000 down 0 | oracle up 9000 down 0 rung 1 (cool-down (oldest window begins 1210, step took effect 1860))
08:54:25.455 EPOCH 33 seed 1969: rungs 1/1/1 n0 weakest up 9000 down 0 | oracle up 9000 down 0 rung 1 (cool-down (oldest window begins 1270, step took effect 1860))
08:54:25.455 EPOCH 34 seed 2029: rungs 1/1/1 n0 weakest up 9000 down 0 | oracle up 9000 down 0 rung 1 (cool-down (oldest window begins 1330, step took effect 1860))
08:54:25.455 EPOCH 35 seed 2089: rungs 1/1/1 n0 weakest up 9000 down 0 | oracle up 9000 down 0 rung 1 (cool-down (oldest window begins 1390, step took effect 1860))
08:54:25.455 EPOCH 36 seed 2149: rungs 1/1/1 n0 weakest up 9000 down 0 | oracle up 9000 down 0 rung 1 (cool-down (oldest window begins 1450, step took effect 1860))
08:54:25.455 EPOCH 37 seed 2209: rungs 1/1/1 n0 weakest up 9000 down 0 | oracle up 9000 down 0 rung 1 (cool-down (oldest window begins 1510, step took effect 1860))
08:54:25.456 EPOCH 38 seed 2269: rungs 1/1/1 n0 weakest up 9000 down 0 | oracle up 9000 down 0 rung 1 (cool-down (oldest window begins 1570, step took effect 1860))
08:54:25.456 EPOCH 39 seed 2329: rungs 1/1/1 n0 weakest up 9000 down 0 | oracle up 9000 down 0 rung 1 (cool-down (oldest window begins 1630, step took effect 1860))
08:54:25.456 EPOCH 40 seed 2389: rungs 1/1/1 n0 weakest up 9000 down 0 | oracle up 9000 down 0 rung 1 (cool-down (oldest window begins 1690, step took effect 1860))
08:54:25.456 EPOCH 41 seed 2449: rungs 1/1/1 n0 weakest up 9000 down 0 | oracle up 9000 down 0 rung 1 (cool-down (oldest window begins 1750, step took effect 1860))
08:54:25.456 EPOCH 42 seed 2509: rungs 1/1/1 n0 weakest up 9000 down 0 | oracle up 9000 down 0 rung 1 (cool-down (oldest window begins 1810, step took effect 1860))
08:54:25.456 EPOCH 43 seed 2569: rungs 2/2/2 n0 weakest up 9000 down 0 | oracle up 9000 down 0 rung 2 STEP (up)
08:54:25.456 EPOCH 44 seed 2629: rungs 2/2/2 n0 weakest up 9000 down 0 | oracle up 9000 down 0 rung 2 (cool-down (oldest window begins 1930, step took effect 2580))
08:54:25.456 EPOCH 45 seed 2689: rungs 2/2/2 n0 weakest up 9000 down 0 | oracle up 9000 down 0 rung 2 (cool-down (oldest window begins 1990, step took effect 2580))
08:54:25.456 FAILED CHECK zero_rejected_by_nodes
08:54:25.456 FAILED CHECK every_produced_block_on_every_node
08:54:25.456 FAILED CHECK node_weakest_equals_oracle_weakest
08:54:25.456 summary: /tmp/igneum-fast-time-attack-f10/summary.json
F10-END exact-89 rc=1 2026-10-07T08:54:27Z

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,114 @@
F10-START exact-89b 2026-10-07T09:04:45Z ladder-exact.mjs --case eighty-nine --window 100 --schedule 0:up:11,1200:up:10 --expect-steps 31:1,43:2 --epochs 45 --rate 8 --secs 1500
09:04:45.943 case eighty-nine: schedule 0:up:11 1200:up:10 (W 100, 7 windows, threshold 9000 bps); expect steps e31->r1 e43->r2; restarts none; 60 DAA per epoch, lead 10, first epoch with seven full windows 12; ladder 27, 35, 53, [88], [173], [267]; 8 blocks/s, up to 45 epochs or 1500 s
09:04:47.482 n0 up (start 1) pid 1733646 json 29902 p2p 29901
09:04:48.992 n1 up (start 1) pid 1734009 json 29912 p2p 29911
09:04:50.498 n2 up (start 1) pid 1734404 json 29922 p2p 29921
09:04:50.498 n0: Latency ladder active: rungs 27, 35, 53, [88], [173], [267] shadow passes, this node signals none (header version bits 15 and 14)
09:04:50.498 n1: Latency ladder active: rungs 27, 35, 53, [88], [173], [267] shadow passes, this node signals none (header version bits 15 and 14)
09:04:50.498 n2: Latency ladder active: rungs 27, 35, 53, [88], [173], [267] shadow passes, this node signals none (header version bits 15 and 14)
09:04:50.499 n0 digest: 5d6d9f3d3f504b84
09:04:50.795 first submit at daa 0 sig none version 0x2: {"report":{"type":"success"}}
09:04:51.467 epoch -1 -> 0 at daa 5, 5.5 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e0 | n2 rung 0 e0
09:04:51.468 t=5.5 s produced 5 daa 5 epoch 0 rungs 0/0/0 blocks/sink 5/2247299a 5/2247299a 5/2247299a disagreements 0 rejected 0
09:04:58.926 epoch 0 -> 1 at daa 60, 13.0 s: n0 rung 0 (27) next 0 (27) up 8305 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e1 | n2 rung 0 e1
09:05:07.060 epoch 1 -> 2 at daa 120, 21.1 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e2 | n2 rung 0 e2
09:05:11.900 t=26.0 s produced 155 daa 155 epoch 2 rungs 0/0/0 blocks/sink 155/4ce8ae3d 155/4ce8ae3d 155/4ce8ae3d disagreements 0 rejected 0
09:05:15.283 epoch 2 -> 3 at daa 180, 29.3 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e3 | n2 rung 0 e3
09:05:23.503 epoch 3 -> 4 at daa 240, 37.6 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e4 | n2 rung 0 e4
09:05:31.834 epoch 4 -> 5 at daa 300, 45.9 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e5 | n2 rung 0 e5
09:05:32.534 t=46.6 s produced 305 daa 305 epoch 5 rungs 0/0/0 blocks/sink 305/798bbafe 305/798bbafe 305/798bbafe disagreements 0 rejected 0
09:05:40.103 epoch 5 -> 6 at daa 360, 54.2 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e6 | n2 rung 0 e6
09:05:48.442 epoch 6 -> 7 at daa 420, 62.5 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e7 | n2 rung 0 e7
09:05:52.658 t=66.7 s produced 450 daa 450 epoch 7 rungs 0/0/0 blocks/sink 450/2db1f75b 450/2db1f75b 450/2db1f75b disagreements 0 rejected 0
09:05:56.910 epoch 7 -> 8 at daa 480, 71.0 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e8 | n2 rung 0 e8
09:06:05.298 epoch 8 -> 9 at daa 540, 79.4 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e9 | n2 rung 0 e9
09:06:13.066 t=87.1 s produced 595 daa 595 epoch 9 rungs 0/0/0 blocks/sink 595/9792612f 595/9792612f 595/9792612f disagreements 0 rejected 0
09:06:13.773 epoch 9 -> 10 at daa 600, 87.8 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e10 | n2 rung 0 e10
09:06:22.152 epoch 10 -> 11 at daa 660, 96.2 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 8305 down 0 weakest 0 step epoch 0 | n1 rung 0 e11 | n2 rung 0 e11
09:06:30.617 epoch 11 -> 12 at daa 720, 104.7 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e12 | n2 rung 0 e12
09:06:33.408 t=107.5 s produced 740 daa 740 epoch 12 rungs 0/0/0 blocks/sink 740/4c04fa65 740/4c04fa65 740/4c04fa65 disagreements 0 rejected 0
09:06:39.063 epoch 12 -> 13 at daa 780, 113.1 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e13 | n2 rung 0 e13
09:06:47.400 epoch 13 -> 14 at daa 840, 121.5 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e14 | n2 rung 0 e14
09:06:53.725 t=127.8 s produced 885 daa 885 epoch 14 rungs 0/0/0 blocks/sink 885/60492262 885/60492262 885/60492262 disagreements 0 rejected 0
09:06:55.823 epoch 14 -> 15 at daa 900, 129.9 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e15 | n2 rung 0 e15
09:07:04.212 epoch 15 -> 16 at daa 960, 138.3 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e16 | n2 rung 0 e16
09:07:12.684 epoch 16 -> 17 at daa 1020, 146.7 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e17 | n2 rung 0 e17
09:07:14.085 t=148.1 s produced 1030 daa 1030 epoch 17 rungs 0/0/0 blocks/sink 1030/95636d8d 1030/95636d8d 1030/95636d8d disagreements 0 rejected 0
09:07:21.077 epoch 17 -> 18 at daa 1080, 155.1 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e18 | n2 rung 0 e18
09:07:29.518 epoch 18 -> 19 at daa 1140, 163.6 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e19 | n2 rung 0 e19
09:07:34.466 t=168.5 s produced 1175 daa 1175 epoch 19 rungs 0/0/0 blocks/sink 1175/21de585f 1175/21de585f 1175/21de585f disagreements 0 rejected 0
09:07:37.983 epoch 19 -> 20 at daa 1200, 172.0 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e20 | n2 rung 0 e20
09:07:46.434 epoch 20 -> 21 at daa 1260, 180.5 s: n0 rung 0 (27) next 0 (27) up 9000 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e21 | n2 rung 0 e21
09:07:54.868 epoch 21 -> 22 at daa 1320, 188.9 s: n0 rung 0 (27) next 0 (27) up 9000 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e22 | n2 rung 0 e22
09:07:54.869 t=188.9 s produced 1320 daa 1320 epoch 22 rungs 0/0/0 blocks/sink 1320/09b95972 1320/09b95972 1320/09b95972 disagreements 0 rejected 0
09:08:03.311 epoch 22 -> 23 at daa 1380, 197.4 s: n0 rung 0 (27) next 0 (27) up 9000 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e23 | n2 rung 0 e23
09:08:11.770 epoch 23 -> 24 at daa 1440, 205.8 s: n0 rung 0 (27) next 0 (27) up 9000 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e24 | n2 rung 0 e24
09:08:15.275 t=209.3 s produced 1465 daa 1465 epoch 24 rungs 0/0/0 blocks/sink 1465/081bd210 1465/081bd210 1465/081bd210 disagreements 0 rejected 0
09:08:20.221 epoch 24 -> 25 at daa 1500, 214.3 s: n0 rung 0 (27) next 0 (27) up 9000 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e25 | n2 rung 0 e25
09:08:28.660 epoch 25 -> 26 at daa 1560, 222.7 s: n0 rung 0 (27) next 0 (27) up 9000 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e26 | n2 rung 0 e26
09:08:35.698 t=229.8 s produced 1610 daa 1610 epoch 26 rungs 0/0/0 blocks/sink 1610/ce80ffe8 1610/ce80ffe8 1610/ce80ffe8 disagreements 0 rejected 0
09:08:37.103 epoch 26 -> 27 at daa 1620, 231.2 s: n0 rung 0 (27) next 0 (27) up 9000 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e27 | n2 rung 0 e27
09:08:45.575 epoch 27 -> 28 at daa 1680, 239.6 s: n0 rung 0 (27) next 0 (27) up 9000 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e28 | n2 rung 0 e28
09:08:54.018 epoch 28 -> 29 at daa 1740, 248.1 s: n0 rung 0 (27) next 0 (27) up 9000 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e29 | n2 rung 0 e29
09:08:56.140 t=250.2 s produced 1755 daa 1755 epoch 29 rungs 0/0/0 blocks/sink 1755/281cc438 1755/281cc438 1755/281cc438 disagreements 0 rejected 0
09:09:02.432 epoch 29 -> 30 at daa 1800, 256.5 s: n0 rung 0 (27) next 0 (27) up 9000 weakest 8900 down 0 weakest 0 step epoch 0 | n1 rung 0 e30 | n2 rung 0 e30
09:09:10.806 epoch 30 -> 31 at daa 1860, 264.9 s: n0 rung 1 (35) next 1 (35) up 9000 weakest 9000 down 0 weakest 0 step epoch 31 | n1 rung 1 e31 | n2 rung 1 e31
09:09:16.389 t=270.4 s produced 1900 daa 1900 epoch 31 rungs 1/1/1 blocks/sink 1900/c3a0a558 1900/c3a0a558 1900/c3a0a558 disagreements 0 rejected 0
09:09:19.177 epoch 31 -> 32 at daa 1920, 273.2 s: n0 rung 1 (35) next 1 (35) up 9000 weakest 9000 down 0 weakest 0 step epoch 31 | n1 rung 1 e32 | n2 rung 1 e32
09:09:27.595 epoch 32 -> 33 at daa 1980, 281.7 s: n0 rung 1 (35) next 1 (35) up 9000 weakest 9000 down 0 weakest 0 step epoch 31 | n1 rung 1 e33 | n2 rung 1 e33
09:09:35.975 epoch 33 -> 34 at daa 2040, 290.0 s: n0 rung 1 (35) next 1 (35) up 9000 weakest 9000 down 0 weakest 0 step epoch 31 | n1 rung 1 e34 | n2 rung 1 e34
09:09:36.690 t=290.7 s produced 2045 daa 2045 epoch 34 rungs 1/1/1 blocks/sink 2045/8fd11bad 2045/8fd11bad 2045/8fd11bad disagreements 0 rejected 0
09:09:44.402 epoch 34 -> 35 at daa 2100, 298.5 s: n0 rung 1 (35) next 1 (35) up 9000 weakest 9000 down 0 weakest 0 step epoch 31 | n1 rung 1 e35 | n2 rung 1 e35
09:09:52.835 epoch 35 -> 36 at daa 2160, 306.9 s: n0 rung 1 (35) next 1 (35) up 9000 weakest 9000 down 0 weakest 0 step epoch 31 | n1 rung 1 e36 | n2 rung 1 e36
09:09:57.028 t=311.1 s produced 2190 daa 2190 epoch 36 rungs 1/1/1 blocks/sink 2190/41fab535 2190/41fab535 2190/41fab535 disagreements 0 rejected 0
09:10:01.273 epoch 36 -> 37 at daa 2220, 315.3 s: n0 rung 1 (35) next 1 (35) up 9000 weakest 9000 down 0 weakest 0 step epoch 31 | n1 rung 1 e37 | n2 rung 1 e37
09:10:09.612 epoch 37 -> 38 at daa 2280, 323.7 s: n0 rung 1 (35) next 1 (35) up 9000 weakest 9000 down 0 weakest 0 step epoch 31 | n1 rung 1 e38 | n2 rung 1 e38
09:10:17.263 t=331.3 s produced 2335 daa 2335 epoch 38 rungs 1/1/1 blocks/sink 2335/e39f5f7e 2335/e39f5f7e 2335/e39f5f7e disagreements 0 rejected 0
09:10:17.970 epoch 38 -> 39 at daa 2340, 332.0 s: n0 rung 1 (35) next 1 (35) up 9000 weakest 9000 down 0 weakest 0 step epoch 31 | n1 rung 1 e39 | n2 rung 1 e39
09:10:26.347 epoch 39 -> 40 at daa 2400, 340.4 s: n0 rung 1 (35) next 1 (35) up 9000 weakest 9000 down 0 weakest 0 step epoch 31 | n1 rung 1 e40 | n2 rung 1 e40
09:10:34.684 epoch 40 -> 41 at daa 2460, 348.7 s: n0 rung 1 (35) next 1 (35) up 9000 weakest 9000 down 0 weakest 0 step epoch 31 | n1 rung 1 e41 | n2 rung 1 e41
09:10:37.460 t=351.5 s produced 2480 daa 2480 epoch 41 rungs 1/1/1 blocks/sink 2480/3872275e 2480/3872275e 2480/3872275e disagreements 0 rejected 0
09:10:43.029 epoch 41 -> 42 at daa 2520, 357.1 s: n0 rung 1 (35) next 1 (35) up 9000 weakest 9000 down 0 weakest 0 step epoch 31 | n1 rung 1 e42 | n2 rung 1 e42
09:10:51.392 epoch 42 -> 43 at daa 2580, 365.4 s: n0 rung 2 (53) next 2 (53) up 9000 weakest 9000 down 0 weakest 0 step epoch 43 | n1 rung 2 e43 | n2 rung 2 e43
09:10:57.673 t=371.7 s produced 2625 daa 2625 epoch 43 rungs 2/2/2 blocks/sink 2625/0ffd8e9b 2625/0ffd8e9b 2625/0ffd8e9b disagreements 0 rejected 0
09:10:59.739 epoch 43 -> 44 at daa 2640, 373.8 s: n0 rung 2 (53) next 2 (53) up 9000 weakest 9000 down 0 weakest 0 step epoch 43 | n1 rung 2 e44 | n2 rung 2 e44
09:11:08.141 epoch 44 -> 45 at daa 2700, 382.2 s: n0 rung 2 (53) next 2 (53) up 9000 weakest 9000 down 0 weakest 0 step epoch 43 | n1 rung 2 e45 | n2 rung 2 e45
09:11:08.142 production ended at 382.2 s: 2700 blocks, last daa 2699; settling 4 s
09:11:12.519 SUMMARY PASS (case eighty-nine): steps e31->r1 e43->r2 (oracle e31->r1 e43->r2, expected e31->r1 e43->r2); 2701 blocks (linear) bits {"none":283,"up":2418} both-bits 135; rejected 0; disagreements 0; sinks 1dd776b4 1dd776b4 1dd776b4 at 2700/2700/2700; restarts none
09:11:12.519 EPOCH 11 seed 649: rungs 0/0/0 n0 weakest up 8305 down 0 | oracle up 7959 down 0 rung 0 (windows not full)
09:11:12.519 EPOCH 12 seed 709: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
09:11:12.519 EPOCH 13 seed 769: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
09:11:12.519 EPOCH 14 seed 829: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
09:11:12.519 EPOCH 15 seed 889: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
09:11:12.519 EPOCH 16 seed 949: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
09:11:12.519 EPOCH 17 seed 1009: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
09:11:12.519 EPOCH 18 seed 1069: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
09:11:12.519 EPOCH 19 seed 1129: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
09:11:12.519 EPOCH 20 seed 1189: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
09:11:12.519 EPOCH 21 seed 1249: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
09:11:12.519 EPOCH 22 seed 1309: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
09:11:12.519 EPOCH 23 seed 1369: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
09:11:12.519 EPOCH 24 seed 1429: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
09:11:12.519 EPOCH 25 seed 1489: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
09:11:12.519 EPOCH 26 seed 1549: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
09:11:12.519 EPOCH 27 seed 1609: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
09:11:12.519 EPOCH 28 seed 1669: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
09:11:12.520 EPOCH 29 seed 1729: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
09:11:12.520 EPOCH 30 seed 1789: rungs 0/0/0 n0 weakest up 8900 down 0 | oracle up 8900 down 0 rung 0 (stands)
09:11:12.520 EPOCH 31 seed 1849: rungs 1/1/1 n0 weakest up 9000 down 0 | oracle up 9000 down 0 rung 1 STEP (up)
09:11:12.520 EPOCH 32 seed 1909: rungs 1/1/1 n0 weakest up 9000 down 0 | oracle up 9000 down 0 rung 1 (cool-down (oldest window begins 1210, step took effect 1860))
09:11:12.520 EPOCH 33 seed 1969: rungs 1/1/1 n0 weakest up 9000 down 0 | oracle up 9000 down 0 rung 1 (cool-down (oldest window begins 1270, step took effect 1860))
09:11:12.520 EPOCH 34 seed 2029: rungs 1/1/1 n0 weakest up 9000 down 0 | oracle up 9000 down 0 rung 1 (cool-down (oldest window begins 1330, step took effect 1860))
09:11:12.520 EPOCH 35 seed 2089: rungs 1/1/1 n0 weakest up 9000 down 0 | oracle up 9000 down 0 rung 1 (cool-down (oldest window begins 1390, step took effect 1860))
09:11:12.520 EPOCH 36 seed 2149: rungs 1/1/1 n0 weakest up 9000 down 0 | oracle up 9000 down 0 rung 1 (cool-down (oldest window begins 1450, step took effect 1860))
09:11:12.520 EPOCH 37 seed 2209: rungs 1/1/1 n0 weakest up 9000 down 0 | oracle up 9000 down 0 rung 1 (cool-down (oldest window begins 1510, step took effect 1860))
09:11:12.520 EPOCH 38 seed 2269: rungs 1/1/1 n0 weakest up 9000 down 0 | oracle up 9000 down 0 rung 1 (cool-down (oldest window begins 1570, step took effect 1860))
09:11:12.520 EPOCH 39 seed 2329: rungs 1/1/1 n0 weakest up 9000 down 0 | oracle up 9000 down 0 rung 1 (cool-down (oldest window begins 1630, step took effect 1860))
09:11:12.520 EPOCH 40 seed 2389: rungs 1/1/1 n0 weakest up 9000 down 0 | oracle up 9000 down 0 rung 1 (cool-down (oldest window begins 1690, step took effect 1860))
09:11:12.520 EPOCH 41 seed 2449: rungs 1/1/1 n0 weakest up 9000 down 0 | oracle up 9000 down 0 rung 1 (cool-down (oldest window begins 1750, step took effect 1860))
09:11:12.520 EPOCH 42 seed 2509: rungs 1/1/1 n0 weakest up 9000 down 0 | oracle up 9000 down 0 rung 1 (cool-down (oldest window begins 1810, step took effect 1860))
09:11:12.520 EPOCH 43 seed 2569: rungs 2/2/2 n0 weakest up 9000 down 0 | oracle up 9000 down 0 rung 2 STEP (up)
09:11:12.520 EPOCH 44 seed 2629: rungs 2/2/2 n0 weakest up 9000 down 0 | oracle up 9000 down 0 rung 2 (cool-down (oldest window begins 1930, step took effect 2580))
09:11:12.520 EPOCH 45 seed 2689: rungs 2/2/2 n0 weakest up 9000 down 0 | oracle up 9000 down 0 rung 2 (cool-down (oldest window begins 1990, step took effect 2580))
09:11:12.520 summary: /tmp/igneum-fast-time-attack-f10/summary.json
F10-END exact-89b rc=0 2026-10-07T09:11:14Z

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,136 @@
F10-START exact-down 2026-10-07T08:54:27Z ladder-exact.mjs --case down --window 100 --schedule 0:up:0,720:down:11,1500:down:10 --expect-steps 12:1,36:0 --epochs 50 --rate 8 --restart 2@1000,1@2300,2@2700 --secs 1500
08:54:27.122 case down: schedule 0:up:0 720:down:11 1500:down:10 (W 100, 7 windows, threshold 9000 bps); expect steps e12->r1 e36->r0; restarts n2@1000 n1@2300 n2@2700; 60 DAA per epoch, lead 10, first epoch with seven full windows 12; ladder 27, 35, 53, [88], [173], [267]; 8 blocks/s, up to 50 epochs or 1500 s
08:54:28.662 n0 up (start 1) pid 1588524 json 29902 p2p 29901
08:54:30.172 n1 up (start 1) pid 1588618 json 29912 p2p 29911
08:54:31.679 n2 up (start 1) pid 1588818 json 29922 p2p 29921
08:54:31.679 n0: Latency ladder active: rungs 27, 35, 53, [88], [173], [267] shadow passes, this node signals none (header version bits 15 and 14)
08:54:31.679 n1: Latency ladder active: rungs 27, 35, 53, [88], [173], [267] shadow passes, this node signals none (header version bits 15 and 14)
08:54:31.679 n2: Latency ladder active: rungs 27, 35, 53, [88], [173], [267] shadow passes, this node signals none (header version bits 15 and 14)
08:54:31.680 n0 digest: 5d6d9f3d3f504b84
08:54:32.101 first submit at daa 0 sig up version 0x8002: {"report":{"type":"success"}}
08:54:32.772 epoch -1 -> 0 at daa 5, 5.6 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e0 | n2 rung 0 e0
08:54:32.773 t=5.7 s produced 5 daa 5 epoch 0 rungs 0/0/0 blocks/sink 5/0b9168f8 0/edc4fa84 0/edc4fa84 disagreements 0 rejected 0
08:54:40.426 epoch 0 -> 1 at daa 60, 13.3 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e1 | n2 rung 0 e1
08:54:48.795 epoch 1 -> 2 at daa 120, 21.7 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e2 | n2 rung 0 e2
08:54:53.034 t=25.9 s produced 150 daa 150 epoch 2 rungs 0/0/0 blocks/sink 150/d58da1d5 150/d58da1d5 150/d58da1d5 disagreements 0 rejected 0
08:54:57.209 epoch 2 -> 3 at daa 180, 30.1 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e3 | n2 rung 0 e3
08:55:05.596 epoch 3 -> 4 at daa 240, 38.5 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e4 | n2 rung 0 e4
08:55:13.357 t=46.2 s produced 295 daa 295 epoch 4 rungs 0/0/0 blocks/sink 295/69ad307f 295/69ad307f 295/69ad307f disagreements 0 rejected 0
08:55:14.060 epoch 4 -> 5 at daa 300, 46.9 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e5 | n2 rung 0 e5
08:55:22.377 epoch 5 -> 6 at daa 360, 55.3 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e6 | n2 rung 0 e6
08:55:30.489 epoch 6 -> 7 at daa 420, 63.4 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e7 | n2 rung 0 e7
08:55:33.884 t=66.8 s produced 445 daa 445 epoch 7 rungs 0/0/0 blocks/sink 445/c222e235 445/c222e235 445/c222e235 disagreements 0 rejected 0
08:55:38.676 epoch 7 -> 8 at daa 480, 71.6 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e8 | n2 rung 0 e8
08:55:46.904 epoch 8 -> 9 at daa 540, 79.8 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e9 | n2 rung 0 e9
08:55:54.421 t=87.3 s produced 595 daa 595 epoch 9 rungs 0/0/0 blocks/sink 595/506255d0 595/506255d0 595/506255d0 disagreements 0 rejected 0
08:55:55.107 epoch 9 -> 10 at daa 600, 88.0 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e10 | n2 rung 0 e10
08:56:03.367 epoch 10 -> 11 at daa 660, 96.2 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 10000 down 0 weakest 0 step epoch 0 | n1 rung 0 e11 | n2 rung 0 e11
08:56:11.569 epoch 11 -> 12 at daa 720, 104.4 s: n0 rung 1 (35) next 1 (35) up 10000 weakest 10000 down 0 weakest 0 step epoch 12 | n1 rung 1 e12 | n2 rung 1 e12
08:56:15.003 t=107.9 s produced 745 daa 745 epoch 12 rungs 1/1/1 blocks/sink 745/cd496a16 745/cd496a16 745/cd496a16 disagreements 0 rejected 0
08:56:19.802 epoch 12 -> 13 at daa 780, 112.7 s: n0 rung 1 (35) next 1 (35) up 4000 weakest 4000 down 6000 weakest 0 step epoch 12 | n1 rung 1 e13 | n2 rung 1 e13
08:56:28.025 epoch 13 -> 14 at daa 840, 120.9 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 0 step epoch 12 | n1 rung 1 e14 | n2 rung 1 e14
08:56:35.194 t=128.1 s produced 890 daa 890 epoch 14 rungs 1/1/1 blocks/sink 890/d920ca2f 890/d920ca2f 890/d920ca2f disagreements 0 rejected 0
08:56:36.567 epoch 14 -> 15 at daa 900, 129.4 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 0 step epoch 12 | n1 rung 1 e15 | n2 rung 1 e15
08:56:44.845 epoch 15 -> 16 at daa 960, 137.7 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 0 step epoch 12 | n1 rung 1 e16 | n2 rung 1 e16
08:56:51.046 RESTART n2 at daa 1004 (143.9 s): SIGINT, wait, start again on the same data dir
08:56:53.153 n2 up (start 2) pid 1614389 json 29922 p2p 29921
08:56:55.222 epoch 16 -> 17 at daa 1020, 148.1 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 0 step epoch 12 | n1 rung 1 e17 | n2 rung 1 e17
08:56:55.223 t=148.1 s produced 1020 daa 1020 epoch 17 rungs 1/1/1 blocks/sink 1020/044b2dce 1020/044b2dce 1020/044b2dce disagreements 0 rejected 0
08:57:03.446 epoch 17 -> 18 at daa 1080, 156.3 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 0 step epoch 12 | n1 rung 1 e18 | n2 rung 1 e18
08:57:11.881 epoch 18 -> 19 at daa 1140, 164.8 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 0 step epoch 12 | n1 rung 1 e19 | n2 rung 1 e19
08:57:15.404 t=168.3 s produced 1165 daa 1165 epoch 19 rungs 1/1/1 blocks/sink 1165/f9d5e8ab 1165/f9d5e8ab 1165/f9d5e8ab disagreements 0 rejected 0
08:57:20.227 epoch 19 -> 20 at daa 1200, 173.1 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 0 step epoch 12 | n1 rung 1 e20 | n2 rung 1 e20
08:57:28.596 epoch 20 -> 21 at daa 1260, 181.5 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 0 step epoch 12 | n1 rung 1 e21 | n2 rung 1 e21
08:57:35.647 t=188.5 s produced 1310 daa 1310 epoch 21 rungs 1/1/1 blocks/sink 1310/771761fa 1310/771761fa 1310/771761fa disagreements 0 rejected 0
08:57:37.022 epoch 21 -> 22 at daa 1320, 189.9 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 0 step epoch 12 | n1 rung 1 e22 | n2 rung 1 e22
08:57:45.326 epoch 22 -> 23 at daa 1380, 198.2 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 6000 step epoch 12 | n1 rung 1 e23 | n2 rung 1 e23
08:57:53.719 epoch 23 -> 24 at daa 1440, 206.6 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 8900 step epoch 12 | n1 rung 1 e24 | n2 rung 1 e24
08:57:55.820 t=208.7 s produced 1455 daa 1455 epoch 24 rungs 1/1/1 blocks/sink 1455/773ee909 1455/773ee909 1455/773ee909 disagreements 0 rejected 0
08:58:02.067 epoch 24 -> 25 at daa 1500, 214.9 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 8900 step epoch 12 | n1 rung 1 e25 | n2 rung 1 e25
08:58:10.419 epoch 25 -> 26 at daa 1560, 223.3 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 9000 weakest 8900 step epoch 12 | n1 rung 1 e26 | n2 rung 1 e26
08:58:16.015 t=228.9 s produced 1600 daa 1600 epoch 26 rungs 1/1/1 blocks/sink 1600/6e6380fd 1600/6e6380fd 1600/6e6380fd disagreements 0 rejected 0
08:58:18.865 epoch 26 -> 27 at daa 1620, 231.7 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 9000 weakest 8900 step epoch 12 | n1 rung 1 e27 | n2 rung 1 e27
08:58:27.360 epoch 27 -> 28 at daa 1680, 240.2 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 9000 weakest 8900 step epoch 12 | n1 rung 1 e28 | n2 rung 1 e28
08:58:35.688 epoch 28 -> 29 at daa 1740, 248.6 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 9000 weakest 8900 step epoch 12 | n1 rung 1 e29 | n2 rung 1 e29
08:58:36.369 t=249.2 s produced 1745 daa 1745 epoch 29 rungs 1/1/1 blocks/sink 1745/43a86715 1745/43a86715 1745/43a86715 disagreements 0 rejected 0
08:58:43.886 epoch 29 -> 30 at daa 1800, 256.8 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 9000 weakest 8900 step epoch 12 | n1 rung 1 e30 | n2 rung 1 e30
08:58:52.259 epoch 30 -> 31 at daa 1860, 265.1 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 9000 weakest 8900 step epoch 12 | n1 rung 1 e31 | n2 rung 1 e31
08:58:56.424 t=269.3 s produced 1890 daa 1890 epoch 31 rungs 1/1/1 blocks/sink 1890/fb0c9d21 1890/fb0c9d21 1890/fb0c9d21 disagreements 0 rejected 0
08:59:00.624 epoch 31 -> 32 at daa 1920, 273.5 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 9000 weakest 8900 step epoch 12 | n1 rung 1 e32 | n2 rung 1 e32
08:59:09.014 epoch 32 -> 33 at daa 1980, 281.9 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 9000 weakest 8900 step epoch 12 | n1 rung 1 e33 | n2 rung 1 e33
08:59:16.784 t=289.7 s produced 2035 daa 2035 epoch 33 rungs 1/1/1 blocks/sink 2035/d44cfaff 2035/d44cfaff 2035/d44cfaff disagreements 0 rejected 0
08:59:17.469 epoch 33 -> 34 at daa 2040, 290.3 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 9000 weakest 8900 step epoch 12 | n1 rung 1 e34 | n2 rung 1 e34
08:59:25.894 epoch 34 -> 35 at daa 2100, 298.8 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 9000 weakest 8900 step epoch 12 | n1 rung 1 e35 | n2 rung 1 e35
08:59:34.077 epoch 35 -> 36 at daa 2160, 307.0 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e36 | n2 rung 0 e36
08:59:36.836 t=309.7 s produced 2180 daa 2180 epoch 36 rungs 0/0/0 blocks/sink 2180/ccee58bc 2180/ccee58bc 2180/ccee58bc disagreements 0 rejected 0
08:59:42.305 epoch 36 -> 37 at daa 2220, 315.2 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e37 | n2 rung 0 e37
08:59:50.568 epoch 37 -> 38 at daa 2280, 323.4 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e38 | n2 rung 0 e38
08:59:53.981 RESTART n1 at daa 2304 (326.9 s): SIGINT, wait, start again on the same data dir
08:59:56.088 n1 up (start 2) pid 1665876 json 29912 p2p 29911
08:59:57.449 t=330.3 s produced 2315 daa 2315 epoch 38 rungs 0/0/0 blocks/sink 2315/bfafeaad 2315/bfafeaad 2315/bfafeaad disagreements 0 rejected 0
09:00:00.830 epoch 38 -> 39 at daa 2340, 333.7 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e39 | n2 rung 0 e39
09:00:09.091 epoch 39 -> 40 at daa 2400, 342.0 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e40 | n2 rung 0 e40
09:00:17.335 epoch 40 -> 41 at daa 2460, 350.2 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e41 | n2 rung 0 e41
09:00:18.042 t=350.9 s produced 2465 daa 2465 epoch 41 rungs 0/0/0 blocks/sink 2465/043646e4 2465/043646e4 2465/043646e4 disagreements 0 rejected 0
09:00:25.600 epoch 41 -> 42 at daa 2520, 358.5 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e42 | n2 rung 0 e42
09:00:33.979 epoch 42 -> 43 at daa 2580, 366.9 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e43 | n2 rung 0 e43
09:00:38.082 t=371.0 s produced 2610 daa 2610 epoch 43 rungs 0/0/0 blocks/sink 2610/e369ecad 2610/e369ecad 2610/e369ecad disagreements 0 rejected 0
09:00:42.239 epoch 43 -> 44 at daa 2640, 375.1 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e44 | n2 rung 0 e44
09:00:50.505 epoch 44 -> 45 at daa 2700, 383.4 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e45 | n2 rung 0 e45
09:00:51.189 RESTART n2 at daa 2704 (384.1 s): SIGINT, wait, start again on the same data dir
09:00:53.296 n2 up (start 3) pid 1679740 json 29922 p2p 29921
09:00:58.101 t=391.0 s produced 2740 daa 2740 epoch 45 rungs 0/0/0 blocks/sink 2740/96d3bf9c 2740/96d3bf9c 2740/96d3bf9c disagreements 0 rejected 0
09:01:00.886 epoch 45 -> 46 at daa 2760, 393.8 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e46 | n2 rung 0 e46
09:01:09.322 epoch 46 -> 47 at daa 2820, 402.2 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e47 | n2 rung 0 e47
09:01:17.766 epoch 47 -> 48 at daa 2880, 410.6 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e48 | n2 rung 0 e48
09:01:18.467 t=411.3 s produced 2885 daa 2885 epoch 48 rungs 0/0/0 blocks/sink 2885/f9fe981b 2885/f9fe981b 2885/f9fe981b disagreements 0 rejected 0
09:01:26.246 epoch 48 -> 49 at daa 2940, 419.1 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e49 | n2 rung 0 e49
09:01:34.663 epoch 49 -> 50 at daa 3000, 427.5 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e50 | n2 rung 0 e50
09:01:34.663 production ended at 427.5 s: 3000 blocks, last daa 2999; settling 4 s
09:01:38.667 t=431.5 s produced 3000 daa 3000 epoch 50 rungs 0/0/0 blocks/sink 3000/55f10b89 3000/55f10b89 3000/55f10b89 disagreements 0 rejected 0
09:01:39.197 SUMMARY FAIL (case down): steps e12->r1 e36->r0 (oracle e12->r1 e36->r0, expected e12->r1 e36->r0); 3001 blocks (linear) bits {"none":228,"up":720,"down":2053} both-bits 110; rejected 0; disagreements 0; sinks 55f10b89 55f10b89 55f10b89 at 3000/3000/3000; restarts n2@1004:same n1@2304:same n2@2704:same
09:01:39.197 EPOCH 11 seed 649: rungs 0/0/0 n0 weakest up 10000 down 0 | oracle up 10000 down 0 rung 0 (windows not full)
09:01:39.197 EPOCH 12 seed 709: rungs 1/1/1 n0 weakest up 10000 down 0 | oracle up 10000 down 0 rung 1 STEP (up)
09:01:39.197 EPOCH 13 seed 769: rungs 1/1/1 n0 weakest up 4000 down 0 | oracle up 5000 down 0 rung 1 (cool-down (oldest window begins 70, step took effect 720))
09:01:39.197 EPOCH 14 seed 829: rungs 1/1/1 n0 weakest up 0 down 0 | oracle up 0 down 0 rung 1 (cool-down (oldest window begins 130, step took effect 720))
09:01:39.197 EPOCH 15 seed 889: rungs 1/1/1 n0 weakest up 0 down 0 | oracle up 0 down 0 rung 1 (cool-down (oldest window begins 190, step took effect 720))
09:01:39.197 EPOCH 16 seed 949: rungs 1/1/1 n0 weakest up 0 down 0 | oracle up 0 down 0 rung 1 (cool-down (oldest window begins 250, step took effect 720))
09:01:39.197 EPOCH 17 seed 1009: rungs 1/1/1 n0 weakest up 0 down 0 | oracle up 0 down 0 rung 1 (cool-down (oldest window begins 310, step took effect 720))
09:01:39.197 EPOCH 18 seed 1069: rungs 1/1/1 n0 weakest up 0 down 0 | oracle up 0 down 0 rung 1 (cool-down (oldest window begins 370, step took effect 720))
09:01:39.197 EPOCH 19 seed 1129: rungs 1/1/1 n0 weakest up 0 down 0 | oracle up 0 down 0 rung 1 (cool-down (oldest window begins 430, step took effect 720))
09:01:39.197 EPOCH 20 seed 1189: rungs 1/1/1 n0 weakest up 0 down 0 | oracle up 0 down 0 rung 1 (cool-down (oldest window begins 490, step took effect 720))
09:01:39.197 EPOCH 21 seed 1249: rungs 1/1/1 n0 weakest up 0 down 0 | oracle up 0 down 0 rung 1 (cool-down (oldest window begins 550, step took effect 720))
09:01:39.197 EPOCH 22 seed 1309: rungs 1/1/1 n0 weakest up 0 down 0 | oracle up 0 down 0 rung 1 (cool-down (oldest window begins 610, step took effect 720))
09:01:39.197 EPOCH 23 seed 1369: rungs 1/1/1 n0 weakest up 0 down 6000 | oracle up 0 down 5000 rung 1 (cool-down (oldest window begins 670, step took effect 720))
09:01:39.197 EPOCH 24 seed 1429: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:01:39.197 EPOCH 25 seed 1489: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:01:39.197 EPOCH 26 seed 1549: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:01:39.197 EPOCH 27 seed 1609: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:01:39.197 EPOCH 28 seed 1669: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:01:39.197 EPOCH 29 seed 1729: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:01:39.197 EPOCH 30 seed 1789: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:01:39.197 EPOCH 31 seed 1849: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:01:39.197 EPOCH 32 seed 1909: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:01:39.197 EPOCH 33 seed 1969: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:01:39.197 EPOCH 34 seed 2029: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:01:39.197 EPOCH 35 seed 2089: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:01:39.197 EPOCH 36 seed 2149: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 STEP (down)
09:01:39.197 EPOCH 37 seed 2209: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 1510, step took effect 2160))
09:01:39.197 EPOCH 38 seed 2269: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 1570, step took effect 2160))
09:01:39.197 EPOCH 39 seed 2329: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 1630, step took effect 2160))
09:01:39.197 EPOCH 40 seed 2389: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 1690, step took effect 2160))
09:01:39.197 EPOCH 41 seed 2449: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 1750, step took effect 2160))
09:01:39.197 EPOCH 42 seed 2509: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 1810, step took effect 2160))
09:01:39.197 EPOCH 43 seed 2569: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 1870, step took effect 2160))
09:01:39.197 EPOCH 44 seed 2629: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 1930, step took effect 2160))
09:01:39.197 EPOCH 45 seed 2689: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 1990, step took effect 2160))
09:01:39.197 EPOCH 46 seed 2749: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 2050, step took effect 2160))
09:01:39.197 EPOCH 47 seed 2809: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 2110, step took effect 2160))
09:01:39.197 EPOCH 48 seed 2869: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (down signalled at rung 0: the floor)
09:01:39.197 EPOCH 49 seed 2929: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (down signalled at rung 0: the floor)
09:01:39.197 EPOCH 50 seed 2989: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (down signalled at rung 0: the floor)
09:01:39.197 FAILED CHECK zero_rejected_by_nodes
09:01:39.197 FAILED CHECK every_produced_block_on_every_node
09:01:39.197 FAILED CHECK node_weakest_equals_oracle_weakest
09:01:39.197 summary: /tmp/igneum-fast-time-attack-f10/summary.json
F10-END exact-down rc=1 2026-10-07T09:01:40Z

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,133 @@
F10-START exact-downb 2026-10-07T09:11:14Z ladder-exact.mjs --case down --window 100 --schedule 0:up:0,720:down:11,1500:down:10 --expect-steps 12:1,36:0 --epochs 50 --rate 8 --restart 2@1000,1@2300,2@2700 --secs 1500
09:11:14.196 case down: schedule 0:up:0 720:down:11 1500:down:10 (W 100, 7 windows, threshold 9000 bps); expect steps e12->r1 e36->r0; restarts n2@1000 n1@2300 n2@2700; 60 DAA per epoch, lead 10, first epoch with seven full windows 12; ladder 27, 35, 53, [88], [173], [267]; 8 blocks/s, up to 50 epochs or 1500 s
09:11:15.734 n0 up (start 1) pid 1793983 json 29902 p2p 29901
09:11:17.243 n1 up (start 1) pid 1794226 json 29912 p2p 29911
09:11:18.747 n2 up (start 1) pid 1794462 json 29922 p2p 29921
09:11:18.748 n0: Latency ladder active: rungs 27, 35, 53, [88], [173], [267] shadow passes, this node signals none (header version bits 15 and 14)
09:11:18.748 n1: Latency ladder active: rungs 27, 35, 53, [88], [173], [267] shadow passes, this node signals none (header version bits 15 and 14)
09:11:18.748 n2: Latency ladder active: rungs 27, 35, 53, [88], [173], [267] shadow passes, this node signals none (header version bits 15 and 14)
09:11:18.748 n0 digest: 5d6d9f3d3f504b84
09:11:19.033 first submit at daa 0 sig up version 0x8002: {"report":{"type":"success"}}
09:11:19.695 epoch -1 -> 0 at daa 5, 5.5 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e0 | n2 rung 0 e0
09:11:19.696 t=5.5 s produced 5 daa 5 epoch 0 rungs 0/0/0 blocks/sink 5/8900c735 5/8900c735 5/8900c735 disagreements 0 rejected 0
09:11:27.286 epoch 0 -> 1 at daa 60, 13.1 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e1 | n2 rung 0 e1
09:11:35.572 epoch 1 -> 2 at daa 120, 21.4 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e2 | n2 rung 0 e2
09:11:39.733 t=25.5 s produced 150 daa 150 epoch 2 rungs 0/0/0 blocks/sink 150/1444b63b 150/1444b63b 150/1444b63b disagreements 0 rejected 0
09:11:43.866 epoch 2 -> 3 at daa 180, 29.7 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e3 | n2 rung 0 e3
09:11:52.190 epoch 3 -> 4 at daa 240, 38.0 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e4 | n2 rung 0 e4
09:11:59.778 t=45.6 s produced 295 daa 295 epoch 4 rungs 0/0/0 blocks/sink 295/fc1c1c19 295/fc1c1c19 295/fc1c1c19 disagreements 0 rejected 0
09:12:00.742 epoch 4 -> 5 at daa 300, 46.5 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e4 | n2 rung 0 e4
09:12:08.960 epoch 5 -> 6 at daa 360, 54.8 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e6 | n2 rung 0 e6
09:12:17.190 epoch 6 -> 7 at daa 420, 63.0 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e7 | n2 rung 0 e7
09:12:19.913 t=65.7 s produced 440 daa 440 epoch 7 rungs 0/0/0 blocks/sink 440/ad540ee1 440/ad540ee1 440/ad540ee1 disagreements 0 rejected 0
09:12:25.441 epoch 7 -> 8 at daa 480, 71.2 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e8 | n2 rung 0 e8
09:12:33.684 epoch 8 -> 9 at daa 540, 79.5 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e9 | n2 rung 0 e9
09:12:40.577 t=86.4 s produced 590 daa 590 epoch 9 rungs 0/0/0 blocks/sink 590/2c5599ac 590/2c5599ac 590/2c5599ac disagreements 0 rejected 0
09:12:41.921 epoch 9 -> 10 at daa 600, 87.7 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e10 | n2 rung 0 e10
09:12:50.253 epoch 10 -> 11 at daa 660, 96.1 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 10000 down 0 weakest 0 step epoch 0 | n1 rung 0 e11 | n2 rung 0 e11
09:12:58.461 epoch 11 -> 12 at daa 720, 104.3 s: n0 rung 1 (35) next 1 (35) up 10000 weakest 10000 down 0 weakest 0 step epoch 12 | n1 rung 1 e12 | n2 rung 1 e12
09:13:01.223 t=107.0 s produced 740 daa 740 epoch 12 rungs 1/1/1 blocks/sink 740/f1dfe0c4 740/f1dfe0c4 740/f1dfe0c4 disagreements 0 rejected 0
09:13:06.706 epoch 12 -> 13 at daa 780, 112.5 s: n0 rung 1 (35) next 1 (35) up 4000 weakest 4000 down 6000 weakest 0 step epoch 12 | n1 rung 1 e13 | n2 rung 1 e13
09:13:15.066 epoch 13 -> 14 at daa 840, 120.9 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 0 step epoch 12 | n1 rung 1 e14 | n2 rung 1 e14
09:13:21.328 t=127.1 s produced 885 daa 885 epoch 14 rungs 1/1/1 blocks/sink 885/0a0bb93d 885/0a0bb93d 885/0a0bb93d disagreements 0 rejected 0
09:13:23.430 epoch 14 -> 15 at daa 900, 129.2 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 0 step epoch 12 | n1 rung 1 e15 | n2 rung 1 e15
09:13:31.765 epoch 15 -> 16 at daa 960, 137.6 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 0 step epoch 12 | n1 rung 1 e16 | n2 rung 1 e16
09:13:38.015 RESTART n2 at daa 1004 (143.8 s): SIGINT, wait, start again on the same data dir
09:13:40.121 n2 up (start 2) pid 1826731 json 29922 p2p 29921
09:13:41.505 t=147.3 s produced 1015 daa 1015 epoch 16 rungs 1/1/1 blocks/sink 1015/8fc62114 1015/8fc62114 1015/8fc62114 disagreements 0 rejected 0
09:13:42.193 epoch 16 -> 17 at daa 1020, 148.0 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 0 step epoch 12 | n1 rung 1 e17 | n2 rung 1 e17
09:13:50.530 epoch 17 -> 18 at daa 1080, 156.3 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 0 step epoch 12 | n1 rung 1 e18 | n2 rung 1 e18
09:13:58.873 epoch 18 -> 19 at daa 1140, 164.7 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 0 step epoch 12 | n1 rung 1 e19 | n2 rung 1 e19
09:14:01.610 t=167.4 s produced 1160 daa 1160 epoch 19 rungs 1/1/1 blocks/sink 1160/76b32916 1160/76b32916 1160/76b32916 disagreements 0 rejected 0
09:14:07.154 epoch 19 -> 20 at daa 1200, 173.0 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 0 step epoch 12 | n1 rung 1 e20 | n2 rung 1 e20
09:14:15.487 epoch 20 -> 21 at daa 1260, 181.3 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 0 step epoch 12 | n1 rung 1 e21 | n2 rung 1 e21
09:14:21.703 t=187.5 s produced 1305 daa 1305 epoch 21 rungs 1/1/1 blocks/sink 1305/325fdcee 1305/325fdcee 1305/325fdcee disagreements 0 rejected 0
09:14:23.773 epoch 21 -> 22 at daa 1320, 189.6 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 0 step epoch 12 | n1 rung 1 e22 | n2 rung 1 e22
09:14:32.099 epoch 22 -> 23 at daa 1380, 197.9 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 6000 step epoch 12 | n1 rung 1 e23 | n2 rung 1 e23
09:14:40.421 epoch 23 -> 24 at daa 1440, 206.2 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 8900 step epoch 12 | n1 rung 1 e24 | n2 rung 1 e24
09:14:41.800 t=207.6 s produced 1450 daa 1450 epoch 24 rungs 1/1/1 blocks/sink 1450/5b3fd990 1450/5b3fd990 1450/5b3fd990 disagreements 0 rejected 0
09:14:48.727 epoch 24 -> 25 at daa 1500, 214.5 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 8900 step epoch 12 | n1 rung 1 e25 | n2 rung 1 e25
09:14:57.006 epoch 25 -> 26 at daa 1560, 222.8 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 9000 weakest 8900 step epoch 12 | n1 rung 1 e26 | n2 rung 1 e26
09:15:01.828 t=227.6 s produced 1595 daa 1595 epoch 26 rungs 1/1/1 blocks/sink 1595/407201f5 1595/407201f5 1595/407201f5 disagreements 0 rejected 0
09:15:05.250 epoch 26 -> 27 at daa 1620, 231.1 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 9000 weakest 8900 step epoch 12 | n1 rung 1 e27 | n2 rung 1 e27
09:15:13.446 epoch 27 -> 28 at daa 1680, 239.2 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 9000 weakest 8900 step epoch 12 | n1 rung 1 e28 | n2 rung 1 e28
09:15:21.674 epoch 28 -> 29 at daa 1740, 247.5 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 9000 weakest 8900 step epoch 12 | n1 rung 1 e29 | n2 rung 1 e29
09:15:22.347 t=248.2 s produced 1745 daa 1745 epoch 29 rungs 1/1/1 blocks/sink 1745/0de97964 1745/0de97964 1745/0de97964 disagreements 0 rejected 0
09:15:29.901 epoch 29 -> 30 at daa 1800, 255.7 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 9000 weakest 8900 step epoch 12 | n1 rung 1 e30 | n2 rung 1 e30
09:15:38.146 epoch 30 -> 31 at daa 1860, 263.9 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 9000 weakest 8900 step epoch 12 | n1 rung 1 e31 | n2 rung 1 e31
09:15:42.927 t=268.7 s produced 1895 daa 1895 epoch 31 rungs 1/1/1 blocks/sink 1895/6385a25a 1895/6385a25a 1895/6385a25a disagreements 0 rejected 0
09:15:46.332 epoch 31 -> 32 at daa 1920, 272.1 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 9000 weakest 8900 step epoch 12 | n1 rung 1 e32 | n2 rung 1 e32
09:15:54.586 epoch 32 -> 33 at daa 1980, 280.4 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 9000 weakest 8900 step epoch 12 | n1 rung 1 e33 | n2 rung 1 e33
09:16:02.864 epoch 33 -> 34 at daa 2040, 288.7 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 9000 weakest 8900 step epoch 12 | n1 rung 1 e34 | n2 rung 1 e34
09:16:03.539 t=289.3 s produced 2045 daa 2045 epoch 34 rungs 1/1/1 blocks/sink 2045/28b497b7 2045/28b497b7 2045/28b497b7 disagreements 0 rejected 0
09:16:11.142 epoch 34 -> 35 at daa 2100, 296.9 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 9000 weakest 8900 step epoch 12 | n1 rung 1 e35 | n2 rung 1 e35
09:16:19.426 epoch 35 -> 36 at daa 2160, 305.2 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e36 | n2 rung 0 e36
09:16:23.596 t=309.4 s produced 2190 daa 2190 epoch 36 rungs 0/0/0 blocks/sink 2190/97266812 2190/97266812 2190/97266812 disagreements 0 rejected 0
09:16:27.744 epoch 36 -> 37 at daa 2220, 313.5 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e37 | n2 rung 0 e37
09:16:36.066 epoch 37 -> 38 at daa 2280, 321.9 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e38 | n2 rung 0 e38
09:16:39.546 RESTART n1 at daa 2304 (325.3 s): SIGINT, wait, start again on the same data dir
09:16:41.654 n1 up (start 2) pid 1869644 json 29912 p2p 29911
09:16:43.762 t=329.6 s produced 2320 daa 2320 epoch 38 rungs 0/0/0 blocks/sink 2320/92725d34 2320/92725d34 2320/92725d34 disagreements 0 rejected 0
09:16:46.502 epoch 38 -> 39 at daa 2340, 332.3 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e39 | n2 rung 0 e39
09:16:54.834 epoch 39 -> 40 at daa 2400, 340.6 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e40 | n2 rung 0 e40
09:17:03.108 epoch 40 -> 41 at daa 2460, 348.9 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e41 | n2 rung 0 e41
09:17:03.816 t=349.6 s produced 2465 daa 2465 epoch 41 rungs 0/0/0 blocks/sink 2465/d41f64bb 2465/d41f64bb 2465/d41f64bb disagreements 0 rejected 0
09:17:11.391 epoch 41 -> 42 at daa 2520, 357.2 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e42 | n2 rung 0 e42
09:17:19.801 epoch 42 -> 43 at daa 2580, 365.6 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e43 | n2 rung 0 e43
09:17:23.980 t=369.8 s produced 2610 daa 2610 epoch 43 rungs 0/0/0 blocks/sink 2610/faf74a1b 2610/faf74a1b 2610/faf74a1b disagreements 0 rejected 0
09:17:28.154 epoch 43 -> 44 at daa 2640, 374.0 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e44 | n2 rung 0 e44
09:17:36.448 epoch 44 -> 45 at daa 2700, 382.3 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e45 | n2 rung 0 e45
09:17:37.135 RESTART n2 at daa 2704 (382.9 s): SIGINT, wait, start again on the same data dir
09:17:39.242 n2 up (start 3) pid 1882703 json 29922 p2p 29921
09:17:44.076 t=389.9 s produced 2740 daa 2740 epoch 45 rungs 0/0/0 blocks/sink 2740/4a4d4c56 2740/4a4d4c56 2740/4a4d4c56 disagreements 0 rejected 0
09:17:46.860 epoch 45 -> 46 at daa 2760, 392.7 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e46 | n2 rung 0 e46
09:17:55.086 epoch 46 -> 47 at daa 2820, 400.9 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e47 | n2 rung 0 e47
09:18:03.382 epoch 47 -> 48 at daa 2880, 409.2 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e48 | n2 rung 0 e48
09:18:04.085 t=409.9 s produced 2885 daa 2885 epoch 48 rungs 0/0/0 blocks/sink 2885/fdf6a6a9 2885/fdf6a6a9 2885/fdf6a6a9 disagreements 0 rejected 0
09:18:11.732 epoch 48 -> 49 at daa 2940, 417.5 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e49 | n2 rung 0 e49
09:18:20.034 epoch 49 -> 50 at daa 3000, 425.8 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e50 | n2 rung 0 e50
09:18:20.034 production ended at 425.8 s: 3000 blocks, last daa 2999; settling 4 s
09:18:24.573 SUMMARY FAIL (case down): steps e12->r1 e36->r0 (oracle e12->r1 e36->r0, expected e12->r1 e36->r0); 3001 blocks (linear) bits {"none":228,"up":720,"down":2053} both-bits 110; rejected 0; disagreements 0; sinks a087747f a087747f a087747f at 3000/3000/3000; restarts n2@1004:same n1@2304:same n2@2704:same
09:18:24.574 EPOCH 11 seed 649: rungs 0/0/0 n0 weakest up 10000 down 0 | oracle up 10000 down 0 rung 0 (windows not full)
09:18:24.574 EPOCH 12 seed 709: rungs 1/1/1 n0 weakest up 10000 down 0 | oracle up 10000 down 0 rung 1 STEP (up)
09:18:24.574 EPOCH 13 seed 769: rungs 1/1/1 n0 weakest up 4000 down 0 | oracle up 5000 down 0 rung 1 (cool-down (oldest window begins 70, step took effect 720))
09:18:24.574 EPOCH 14 seed 829: rungs 1/1/1 n0 weakest up 0 down 0 | oracle up 0 down 0 rung 1 (cool-down (oldest window begins 130, step took effect 720))
09:18:24.574 EPOCH 15 seed 889: rungs 1/1/1 n0 weakest up 0 down 0 | oracle up 0 down 0 rung 1 (cool-down (oldest window begins 190, step took effect 720))
09:18:24.574 EPOCH 16 seed 949: rungs 1/1/1 n0 weakest up 0 down 0 | oracle up 0 down 0 rung 1 (cool-down (oldest window begins 250, step took effect 720))
09:18:24.574 EPOCH 17 seed 1009: rungs 1/1/1 n0 weakest up 0 down 0 | oracle up 0 down 0 rung 1 (cool-down (oldest window begins 310, step took effect 720))
09:18:24.574 EPOCH 18 seed 1069: rungs 1/1/1 n0 weakest up 0 down 0 | oracle up 0 down 0 rung 1 (cool-down (oldest window begins 370, step took effect 720))
09:18:24.574 EPOCH 19 seed 1129: rungs 1/1/1 n0 weakest up 0 down 0 | oracle up 0 down 0 rung 1 (cool-down (oldest window begins 430, step took effect 720))
09:18:24.574 EPOCH 20 seed 1189: rungs 1/1/1 n0 weakest up 0 down 0 | oracle up 0 down 0 rung 1 (cool-down (oldest window begins 490, step took effect 720))
09:18:24.574 EPOCH 21 seed 1249: rungs 1/1/1 n0 weakest up 0 down 0 | oracle up 0 down 0 rung 1 (cool-down (oldest window begins 550, step took effect 720))
09:18:24.574 EPOCH 22 seed 1309: rungs 1/1/1 n0 weakest up 0 down 0 | oracle up 0 down 0 rung 1 (cool-down (oldest window begins 610, step took effect 720))
09:18:24.574 EPOCH 23 seed 1369: rungs 1/1/1 n0 weakest up 0 down 6000 | oracle up 0 down 5000 rung 1 (cool-down (oldest window begins 670, step took effect 720))
09:18:24.574 EPOCH 24 seed 1429: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:18:24.574 EPOCH 25 seed 1489: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:18:24.574 EPOCH 26 seed 1549: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:18:24.574 EPOCH 27 seed 1609: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:18:24.574 EPOCH 28 seed 1669: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:18:24.574 EPOCH 29 seed 1729: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:18:24.574 EPOCH 30 seed 1789: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:18:24.574 EPOCH 31 seed 1849: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:18:24.574 EPOCH 32 seed 1909: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:18:24.574 EPOCH 33 seed 1969: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:18:24.574 EPOCH 34 seed 2029: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:18:24.574 EPOCH 35 seed 2089: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:18:24.574 EPOCH 36 seed 2149: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 STEP (down)
09:18:24.574 EPOCH 37 seed 2209: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 1510, step took effect 2160))
09:18:24.574 EPOCH 38 seed 2269: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 1570, step took effect 2160))
09:18:24.574 EPOCH 39 seed 2329: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 1630, step took effect 2160))
09:18:24.574 EPOCH 40 seed 2389: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 1690, step took effect 2160))
09:18:24.574 EPOCH 41 seed 2449: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 1750, step took effect 2160))
09:18:24.574 EPOCH 42 seed 2509: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 1810, step took effect 2160))
09:18:24.574 EPOCH 43 seed 2569: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 1870, step took effect 2160))
09:18:24.574 EPOCH 44 seed 2629: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 1930, step took effect 2160))
09:18:24.574 EPOCH 45 seed 2689: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 1990, step took effect 2160))
09:18:24.574 EPOCH 46 seed 2749: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 2050, step took effect 2160))
09:18:24.574 EPOCH 47 seed 2809: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 2110, step took effect 2160))
09:18:24.574 EPOCH 48 seed 2869: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (down signalled at rung 0: the floor)
09:18:24.574 EPOCH 49 seed 2929: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (down signalled at rung 0: the floor)
09:18:24.574 EPOCH 50 seed 2989: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (down signalled at rung 0: the floor)
09:18:24.574 FAILED CHECK node_weakest_equals_oracle_weakest
09:18:24.574 summary: /tmp/igneum-fast-time-attack-f10/summary.json
F10-END exact-downb rc=1 2026-10-07T09:18:26Z

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,132 @@
F10-START exact-downc 2026-10-07T09:19:13Z ladder-exact.mjs --case down --window 100 --schedule 0:up:0,720:down:11,1500:down:10 --expect-steps 12:1,36:0 --epochs 50 --rate 8 --restart 2@1000,1@2300,2@2700 --secs 1500
09:19:13.936 case down: schedule 0:up:0 720:down:11 1500:down:10 (W 100, 7 windows, threshold 9000 bps); expect steps e12->r1 e36->r0; restarts n2@1000 n1@2300 n2@2700; 60 DAA per epoch, lead 10, first epoch with seven full windows 12; ladder 27, 35, 53, [88], [173], [267]; 8 blocks/s, up to 50 epochs or 1500 s
09:19:15.476 n0 up (start 1) pid 1905713 json 29902 p2p 29901
09:19:16.986 n1 up (start 1) pid 1906054 json 29912 p2p 29911
09:19:18.491 n2 up (start 1) pid 1906527 json 29922 p2p 29921
09:19:18.492 n0: Latency ladder active: rungs 27, 35, 53, [88], [173], [267] shadow passes, this node signals none (header version bits 15 and 14)
09:19:18.492 n1: Latency ladder active: rungs 27, 35, 53, [88], [173], [267] shadow passes, this node signals none (header version bits 15 and 14)
09:19:18.492 n2: Latency ladder active: rungs 27, 35, 53, [88], [173], [267] shadow passes, this node signals none (header version bits 15 and 14)
09:19:18.492 n0 digest: 5d6d9f3d3f504b84
09:19:18.799 first submit at daa 0 sig up version 0x8002: {"report":{"type":"success"}}
09:19:19.469 epoch -1 -> 0 at daa 5, 5.5 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e0 | n2 rung 0 e0
09:19:19.470 t=5.5 s produced 5 daa 5 epoch 0 rungs 0/0/0 blocks/sink 5/1a714afa 5/1a714afa 5/1a714afa disagreements 0 rejected 0
09:19:27.076 epoch 0 -> 1 at daa 60, 13.1 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e1 | n2 rung 0 e1
09:19:35.344 epoch 1 -> 2 at daa 120, 21.4 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e2 | n2 rung 0 e2
09:19:39.484 t=25.5 s produced 150 daa 150 epoch 2 rungs 0/0/0 blocks/sink 150/100ffa01 150/100ffa01 150/100ffa01 disagreements 0 rejected 0
09:19:43.624 epoch 2 -> 3 at daa 180, 29.7 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e3 | n2 rung 0 e3
09:19:51.980 epoch 3 -> 4 at daa 240, 38.0 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e4 | n2 rung 0 e4
09:19:59.622 t=45.7 s produced 295 daa 295 epoch 4 rungs 0/0/0 blocks/sink 295/82c6c70b 295/82c6c70b 295/82c6c70b disagreements 0 rejected 0
09:20:00.320 epoch 4 -> 5 at daa 300, 46.4 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e5 | n2 rung 0 e5
09:20:08.625 epoch 5 -> 6 at daa 360, 54.7 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e6 | n2 rung 0 e6
09:20:16.987 epoch 6 -> 7 at daa 420, 63.0 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e7 | n2 rung 0 e7
09:20:19.747 t=65.8 s produced 440 daa 440 epoch 7 rungs 0/0/0 blocks/sink 440/8f7a3395 440/8f7a3395 440/8f7a3395 disagreements 0 rejected 0
09:20:25.301 epoch 7 -> 8 at daa 480, 71.4 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e8 | n2 rung 0 e8
09:20:33.634 epoch 8 -> 9 at daa 540, 79.7 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e9 | n2 rung 0 e9
09:20:39.893 t=86.0 s produced 585 daa 585 epoch 9 rungs 0/0/0 blocks/sink 585/3c11c892 585/3c11c892 585/3c11c892 disagreements 0 rejected 0
09:20:41.970 epoch 9 -> 10 at daa 600, 88.0 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e10 | n2 rung 0 e10
09:20:50.338 epoch 10 -> 11 at daa 660, 96.4 s: n0 rung 0 (27) next 0 (27) up 10000 weakest 10000 down 0 weakest 0 step epoch 0 | n1 rung 0 e11 | n2 rung 0 e11
09:20:58.663 epoch 11 -> 12 at daa 720, 104.7 s: n0 rung 1 (35) next 1 (35) up 10000 weakest 10000 down 0 weakest 0 step epoch 12 | n1 rung 1 e12 | n2 rung 1 e12
09:21:00.056 t=106.1 s produced 730 daa 730 epoch 12 rungs 1/1/1 blocks/sink 730/b2385862 730/b2385862 730/b2385862 disagreements 0 rejected 0
09:21:07.037 epoch 12 -> 13 at daa 780, 113.1 s: n0 rung 1 (35) next 1 (35) up 4000 weakest 4000 down 6000 weakest 0 step epoch 12 | n1 rung 1 e13 | n2 rung 1 e13
09:21:15.376 epoch 13 -> 14 at daa 840, 121.4 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 0 step epoch 12 | n1 rung 1 e14 | n2 rung 1 e14
09:21:20.247 t=126.3 s produced 875 daa 875 epoch 14 rungs 1/1/1 blocks/sink 875/2e21ee57 875/2e21ee57 875/2e21ee57 disagreements 0 rejected 0
09:21:23.684 epoch 14 -> 15 at daa 900, 129.7 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 0 step epoch 12 | n1 rung 1 e15 | n2 rung 1 e15
09:21:31.986 epoch 15 -> 16 at daa 960, 138.0 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 0 step epoch 12 | n1 rung 1 e16 | n2 rung 1 e16
09:21:38.252 RESTART n2 at daa 1004 (144.3 s): SIGINT, wait, start again on the same data dir
09:21:40.360 n2 up (start 2) pid 1945464 json 29922 p2p 29921
09:21:41.063 t=147.1 s produced 1010 daa 1010 epoch 16 rungs 1/1/1 blocks/sink 1010/fd132095 1010/fd132095 1010/fd132095 disagreements 0 rejected 0
09:21:42.440 epoch 16 -> 17 at daa 1020, 148.5 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 0 step epoch 12 | n1 rung 1 e17 | n2 rung 1 e17
09:21:50.800 epoch 17 -> 18 at daa 1080, 156.9 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 0 step epoch 12 | n1 rung 1 e18 | n2 rung 1 e18
09:21:59.099 epoch 18 -> 19 at daa 1140, 165.2 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 0 step epoch 12 | n1 rung 1 e19 | n2 rung 1 e19
09:22:01.182 t=167.2 s produced 1155 daa 1155 epoch 19 rungs 1/1/1 blocks/sink 1155/6d4659e7 1155/6d4659e7 1155/6d4659e7 disagreements 0 rejected 0
09:22:07.356 epoch 19 -> 20 at daa 1200, 173.4 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 0 step epoch 12 | n1 rung 1 e20 | n2 rung 1 e20
09:22:15.657 epoch 20 -> 21 at daa 1260, 181.7 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 0 step epoch 12 | n1 rung 1 e21 | n2 rung 1 e21
09:22:21.215 t=187.3 s produced 1300 daa 1300 epoch 21 rungs 1/1/1 blocks/sink 1300/9867e991 1300/9867e991 1300/9867e991 disagreements 0 rejected 0
09:22:23.942 epoch 21 -> 22 at daa 1320, 190.0 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 0 step epoch 12 | n1 rung 1 e22 | n2 rung 1 e22
09:22:32.200 epoch 22 -> 23 at daa 1380, 198.3 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 6000 step epoch 12 | n1 rung 1 e23 | n2 rung 1 e23
09:22:40.445 epoch 23 -> 24 at daa 1440, 206.5 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 8900 step epoch 12 | n1 rung 1 e24 | n2 rung 1 e24
09:22:41.844 t=207.9 s produced 1450 daa 1450 epoch 24 rungs 1/1/1 blocks/sink 1450/7658d15b 1450/7658d15b 1450/7658d15b disagreements 0 rejected 0
09:22:48.766 epoch 24 -> 25 at daa 1500, 214.8 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 8900 weakest 8900 step epoch 12 | n1 rung 1 e25 | n2 rung 1 e25
09:22:57.091 epoch 25 -> 26 at daa 1560, 223.2 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 9000 weakest 8900 step epoch 12 | n1 rung 1 e26 | n2 rung 1 e26
09:23:01.993 t=228.1 s produced 1595 daa 1595 epoch 26 rungs 1/1/1 blocks/sink 1595/cc5c5c3d 1595/cc5c5c3d 1595/cc5c5c3d disagreements 0 rejected 0
09:23:05.472 epoch 26 -> 27 at daa 1620, 231.5 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 9000 weakest 8900 step epoch 12 | n1 rung 1 e27 | n2 rung 1 e27
09:23:13.640 epoch 27 -> 28 at daa 1680, 239.7 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 9000 weakest 8900 step epoch 12 | n1 rung 1 e28 | n2 rung 1 e28
09:23:21.725 epoch 28 -> 29 at daa 1740, 247.8 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 9000 weakest 8900 step epoch 12 | n1 rung 1 e29 | n2 rung 1 e29
09:23:22.406 t=248.5 s produced 1745 daa 1745 epoch 29 rungs 1/1/1 blocks/sink 1745/c76e8f5c 1745/c76e8f5c 1745/c76e8f5c disagreements 0 rejected 0
09:23:29.938 epoch 29 -> 30 at daa 1800, 256.0 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 9000 weakest 8900 step epoch 12 | n1 rung 1 e30 | n2 rung 1 e30
09:23:38.212 epoch 30 -> 31 at daa 1860, 264.3 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 9000 weakest 8900 step epoch 12 | n1 rung 1 e31 | n2 rung 1 e31
09:23:43.034 t=269.1 s produced 1895 daa 1895 epoch 31 rungs 1/1/1 blocks/sink 1895/65e67486 1895/65e67486 1895/65e67486 disagreements 0 rejected 0
09:23:46.461 epoch 31 -> 32 at daa 1920, 272.5 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 9000 weakest 8900 step epoch 12 | n1 rung 1 e32 | n2 rung 1 e32
09:23:54.813 epoch 32 -> 33 at daa 1980, 280.9 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 9000 weakest 8900 step epoch 12 | n1 rung 1 e33 | n2 rung 1 e33
09:24:03.191 epoch 33 -> 34 at daa 2040, 289.3 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 9000 weakest 8900 step epoch 12 | n1 rung 1 e34 | n2 rung 1 e34
09:24:03.192 t=289.3 s produced 2040 daa 2040 epoch 34 rungs 1/1/1 blocks/sink 2040/9f40166e 2040/9f40166e 2040/9f40166e disagreements 0 rejected 0
09:24:11.362 epoch 34 -> 35 at daa 2100, 297.4 s: n0 rung 1 (35) next 1 (35) up 0 weakest 0 down 9000 weakest 8900 step epoch 12 | n1 rung 1 e35 | n2 rung 1 e35
09:24:19.648 epoch 35 -> 36 at daa 2160, 305.7 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e36 | n2 rung 0 e36
09:24:23.862 t=309.9 s produced 2190 daa 2190 epoch 36 rungs 0/0/0 blocks/sink 2190/8e830918 2190/8e830918 2190/8e830918 disagreements 0 rejected 0
09:24:28.022 epoch 36 -> 37 at daa 2220, 314.1 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e37 | n2 rung 0 e37
09:24:36.331 epoch 37 -> 38 at daa 2280, 322.4 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e38 | n2 rung 0 e38
09:24:39.816 RESTART n1 at daa 2304 (325.9 s): SIGINT, wait, start again on the same data dir
09:24:41.920 n1 up (start 2) pid 2007008 json 29912 p2p 29911
09:24:43.974 t=330.0 s produced 2320 daa 2320 epoch 38 rungs 0/0/0 blocks/sink 2320/540bcb24 2320/540bcb24 2320/540bcb24 disagreements 0 rejected 0
09:24:46.742 epoch 38 -> 39 at daa 2340, 332.8 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e39 | n2 rung 0 e39
09:24:55.064 epoch 39 -> 40 at daa 2400, 341.1 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e40 | n2 rung 0 e40
09:25:03.285 epoch 40 -> 41 at daa 2460, 349.3 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e41 | n2 rung 0 e41
09:25:04.646 t=350.7 s produced 2470 daa 2470 epoch 41 rungs 0/0/0 blocks/sink 2470/11c5d39d 2470/11c5d39d 2470/11c5d39d disagreements 0 rejected 0
09:25:11.662 epoch 41 -> 42 at daa 2520, 357.7 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e42 | n2 rung 0 e42
09:25:19.863 epoch 42 -> 43 at daa 2580, 365.9 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e43 | n2 rung 0 e43
09:25:25.310 t=371.4 s produced 2620 daa 2620 epoch 43 rungs 0/0/0 blocks/sink 2620/5d032c2b 2620/5d032c2b 2620/5d032c2b disagreements 0 rejected 0
09:25:28.020 epoch 43 -> 44 at daa 2640, 374.1 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e44 | n2 rung 0 e44
09:25:36.138 epoch 44 -> 45 at daa 2700, 382.2 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e45 | n2 rung 0 e45
09:25:36.833 RESTART n2 at daa 2704 (382.9 s): SIGINT, wait, start again on the same data dir
09:25:38.939 n2 up (start 3) pid 2026032 json 29922 p2p 29921
09:25:45.759 t=391.8 s produced 2755 daa 2755 epoch 45 rungs 0/0/0 blocks/sink 2755/2689ae48 2755/2689ae48 2755/2689ae48 disagreements 0 rejected 0
09:25:46.452 epoch 45 -> 46 at daa 2760, 392.5 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e46 | n2 rung 0 e46
09:25:54.605 epoch 46 -> 47 at daa 2820, 400.7 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e47 | n2 rung 0 e47
09:26:02.761 epoch 47 -> 48 at daa 2880, 408.8 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e48 | n2 rung 0 e48
09:26:06.171 t=412.2 s produced 2905 daa 2905 epoch 48 rungs 0/0/0 blocks/sink 2905/7488043d 2905/7488043d 2905/7488043d disagreements 0 rejected 0
09:26:10.988 epoch 48 -> 49 at daa 2940, 417.1 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e49 | n2 rung 0 e49
09:26:19.287 epoch 49 -> 50 at daa 3000, 425.4 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 9000 weakest 9000 step epoch 36 | n1 rung 0 e50 | n2 rung 0 e50
09:26:19.287 production ended at 425.4 s: 3000 blocks, last daa 2999; settling 4 s
09:26:23.586 SUMMARY PASS (case down): steps e12->r1 e36->r0 (oracle e12->r1 e36->r0, expected e12->r1 e36->r0); 3001 blocks (linear) bits {"none":228,"up":720,"down":2053} both-bits 110; rejected 0; disagreements 0; sinks 20c6b367 20c6b367 20c6b367 at 3000/3000/3000; restarts n2@1004:same n1@2304:same n2@2704:same
09:26:23.586 EPOCH 11 seed 649: rungs 0/0/0 n0 weakest up 10000 down 0 | oracle up 10000 down 0 rung 0 (windows not full)
09:26:23.586 EPOCH 12 seed 709: rungs 1/1/1 n0 weakest up 10000 down 0 | oracle up 10000 down 0 rung 1 STEP (up)
09:26:23.586 EPOCH 13 seed 769: rungs 1/1/1 n0 weakest up 4000 down 0 | oracle up 5000 down 0 rung 1 (cool-down (oldest window begins 70, step took effect 720))
09:26:23.586 EPOCH 14 seed 829: rungs 1/1/1 n0 weakest up 0 down 0 | oracle up 0 down 0 rung 1 (cool-down (oldest window begins 130, step took effect 720))
09:26:23.586 EPOCH 15 seed 889: rungs 1/1/1 n0 weakest up 0 down 0 | oracle up 0 down 0 rung 1 (cool-down (oldest window begins 190, step took effect 720))
09:26:23.586 EPOCH 16 seed 949: rungs 1/1/1 n0 weakest up 0 down 0 | oracle up 0 down 0 rung 1 (cool-down (oldest window begins 250, step took effect 720))
09:26:23.586 EPOCH 17 seed 1009: rungs 1/1/1 n0 weakest up 0 down 0 | oracle up 0 down 0 rung 1 (cool-down (oldest window begins 310, step took effect 720))
09:26:23.586 EPOCH 18 seed 1069: rungs 1/1/1 n0 weakest up 0 down 0 | oracle up 0 down 0 rung 1 (cool-down (oldest window begins 370, step took effect 720))
09:26:23.586 EPOCH 19 seed 1129: rungs 1/1/1 n0 weakest up 0 down 0 | oracle up 0 down 0 rung 1 (cool-down (oldest window begins 430, step took effect 720))
09:26:23.586 EPOCH 20 seed 1189: rungs 1/1/1 n0 weakest up 0 down 0 | oracle up 0 down 0 rung 1 (cool-down (oldest window begins 490, step took effect 720))
09:26:23.586 EPOCH 21 seed 1249: rungs 1/1/1 n0 weakest up 0 down 0 | oracle up 0 down 0 rung 1 (cool-down (oldest window begins 550, step took effect 720))
09:26:23.586 EPOCH 22 seed 1309: rungs 1/1/1 n0 weakest up 0 down 0 | oracle up 0 down 0 rung 1 (cool-down (oldest window begins 610, step took effect 720))
09:26:23.586 EPOCH 23 seed 1369: rungs 1/1/1 n0 weakest up 0 down 6000 | oracle up 0 down 5000 rung 1 (cool-down (oldest window begins 670, step took effect 720))
09:26:23.586 EPOCH 24 seed 1429: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:26:23.586 EPOCH 25 seed 1489: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:26:23.586 EPOCH 26 seed 1549: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:26:23.586 EPOCH 27 seed 1609: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:26:23.586 EPOCH 28 seed 1669: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:26:23.586 EPOCH 29 seed 1729: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:26:23.586 EPOCH 30 seed 1789: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:26:23.586 EPOCH 31 seed 1849: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:26:23.586 EPOCH 32 seed 1909: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:26:23.586 EPOCH 33 seed 1969: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:26:23.586 EPOCH 34 seed 2029: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:26:23.586 EPOCH 35 seed 2089: rungs 1/1/1 n0 weakest up 0 down 8900 | oracle up 0 down 8900 rung 1 (stands)
09:26:23.586 EPOCH 36 seed 2149: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 STEP (down)
09:26:23.586 EPOCH 37 seed 2209: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 1510, step took effect 2160))
09:26:23.586 EPOCH 38 seed 2269: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 1570, step took effect 2160))
09:26:23.586 EPOCH 39 seed 2329: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 1630, step took effect 2160))
09:26:23.586 EPOCH 40 seed 2389: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 1690, step took effect 2160))
09:26:23.586 EPOCH 41 seed 2449: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 1750, step took effect 2160))
09:26:23.586 EPOCH 42 seed 2509: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 1810, step took effect 2160))
09:26:23.586 EPOCH 43 seed 2569: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 1870, step took effect 2160))
09:26:23.586 EPOCH 44 seed 2629: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 1930, step took effect 2160))
09:26:23.586 EPOCH 45 seed 2689: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 1990, step took effect 2160))
09:26:23.586 EPOCH 46 seed 2749: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 2050, step took effect 2160))
09:26:23.586 EPOCH 47 seed 2809: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (cool-down (oldest window begins 2110, step took effect 2160))
09:26:23.586 EPOCH 48 seed 2869: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (down signalled at rung 0: the floor)
09:26:23.586 EPOCH 49 seed 2929: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (down signalled at rung 0: the floor)
09:26:23.586 EPOCH 50 seed 2989: rungs 0/0/0 n0 weakest up 0 down 9000 | oracle up 0 down 9000 rung 0 (down signalled at rung 0: the floor)
09:26:23.586 summary: /tmp/igneum-fast-time-attack-f10/summary.json
F10-END exact-downc rc=0 2026-10-07T09:26:25Z

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,54 @@
F10-START exact-floor 2026-10-07T09:01:40Z ladder-exact.mjs --case floor --window 100 --schedule 0:down:0 --epochs 20 --rate 8 --secs 600
09:01:40.879 case floor: schedule 0:down:0 (W 100, 7 windows, threshold 9000 bps); expect steps none; restarts none; 60 DAA per epoch, lead 10, first epoch with seven full windows 12; ladder 27, 35, 53, [88], [173], [267]; 8 blocks/s, up to 20 epochs or 600 s
09:01:42.415 n0 up (start 1) pid 1686174 json 29902 p2p 29901
09:01:43.924 n1 up (start 1) pid 1686554 json 29912 p2p 29911
09:01:45.429 n2 up (start 1) pid 1687162 json 29922 p2p 29921
09:01:45.430 n0: Latency ladder active: rungs 27, 35, 53, [88], [173], [267] shadow passes, this node signals none (header version bits 15 and 14)
09:01:45.430 n1: Latency ladder active: rungs 27, 35, 53, [88], [173], [267] shadow passes, this node signals none (header version bits 15 and 14)
09:01:45.430 n2: Latency ladder active: rungs 27, 35, 53, [88], [173], [267] shadow passes, this node signals none (header version bits 15 and 14)
09:01:45.430 n0 digest: 5d6d9f3d3f504b84
09:01:45.752 first submit at daa 0 sig down version 0x4002: {"report":{"type":"success"}}
09:01:46.423 epoch -1 -> 0 at daa 5, 5.5 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 10000 weakest 0 step epoch 0 | n1 rung 0 e0 | n2 rung 0 e0
09:01:46.424 t=5.5 s produced 5 daa 5 epoch 0 rungs 0/0/0 blocks/sink 5/866cc50e 5/866cc50e 5/866cc50e disagreements 0 rejected 0
09:01:53.941 epoch 0 -> 1 at daa 60, 13.1 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 10000 weakest 0 step epoch 0 | n1 rung 0 e1 | n2 rung 0 e1
09:02:02.236 epoch 1 -> 2 at daa 120, 21.4 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 10000 weakest 0 step epoch 0 | n1 rung 0 e2 | n2 rung 0 e2
09:02:07.086 t=26.2 s produced 155 daa 155 epoch 2 rungs 0/0/0 blocks/sink 155/8e687101 155/8e687101 155/8e687101 disagreements 0 rejected 0
09:02:10.579 epoch 2 -> 3 at daa 180, 29.7 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 10000 weakest 0 step epoch 0 | n1 rung 0 e3 | n2 rung 0 e3
09:02:19.008 epoch 3 -> 4 at daa 240, 38.1 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 10000 weakest 0 step epoch 0 | n1 rung 0 e4 | n2 rung 0 e4
09:02:27.435 epoch 4 -> 5 at daa 300, 46.6 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 10000 weakest 0 step epoch 0 | n1 rung 0 e5 | n2 rung 0 e5
09:02:27.436 t=46.6 s produced 300 daa 300 epoch 5 rungs 0/0/0 blocks/sink 300/fccbf0dd 300/fccbf0dd 300/fccbf0dd disagreements 0 rejected 0
09:02:35.840 epoch 5 -> 6 at daa 360, 55.0 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 10000 weakest 0 step epoch 0 | n1 rung 0 e6 | n2 rung 0 e6
09:02:44.206 epoch 6 -> 7 at daa 420, 63.3 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 10000 weakest 0 step epoch 0 | n1 rung 0 e7 | n2 rung 0 e7
09:02:47.688 t=66.8 s produced 445 daa 445 epoch 7 rungs 0/0/0 blocks/sink 445/2e5b1930 445/2e5b1930 445/2e5b1930 disagreements 0 rejected 0
09:02:52.551 epoch 7 -> 8 at daa 480, 71.7 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 10000 weakest 0 step epoch 0 | n1 rung 0 e8 | n2 rung 0 e8
09:03:00.910 epoch 8 -> 9 at daa 540, 80.0 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 10000 weakest 0 step epoch 0 | n1 rung 0 e9 | n2 rung 0 e9
09:03:07.828 t=86.9 s produced 590 daa 590 epoch 9 rungs 0/0/0 blocks/sink 590/559b2cb9 590/559b2cb9 590/559b2cb9 disagreements 0 rejected 0
09:03:09.237 epoch 9 -> 10 at daa 600, 88.4 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 10000 weakest 0 step epoch 0 | n1 rung 0 e10 | n2 rung 0 e10
09:03:17.691 epoch 10 -> 11 at daa 660, 96.8 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 10000 weakest 10000 step epoch 0 | n1 rung 0 e11 | n2 rung 0 e11
09:03:26.056 epoch 11 -> 12 at daa 720, 105.2 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 10000 weakest 10000 step epoch 0 | n1 rung 0 e12 | n2 rung 0 e12
09:03:28.167 t=107.3 s produced 735 daa 735 epoch 12 rungs 0/0/0 blocks/sink 735/d6567589 735/d6567589 735/d6567589 disagreements 0 rejected 0
09:03:34.502 epoch 12 -> 13 at daa 780, 113.6 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 10000 weakest 10000 step epoch 0 | n1 rung 0 e13 | n2 rung 0 e13
09:03:42.944 epoch 13 -> 14 at daa 840, 122.1 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 10000 weakest 10000 step epoch 0 | n1 rung 0 e14 | n2 rung 0 e14
09:03:48.537 t=127.7 s produced 880 daa 880 epoch 14 rungs 0/0/0 blocks/sink 880/6c6dfc9e 880/6c6dfc9e 880/6c6dfc9e disagreements 0 rejected 0
09:03:51.345 epoch 14 -> 15 at daa 900, 130.5 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 10000 weakest 10000 step epoch 0 | n1 rung 0 e15 | n2 rung 0 e15
09:03:59.786 epoch 15 -> 16 at daa 960, 138.9 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 10000 weakest 10000 step epoch 0 | n1 rung 0 e16 | n2 rung 0 e16
09:04:08.190 epoch 16 -> 17 at daa 1020, 147.3 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 10000 weakest 10000 step epoch 0 | n1 rung 0 e17 | n2 rung 0 e17
09:04:08.901 t=148.0 s produced 1025 daa 1025 epoch 17 rungs 0/0/0 blocks/sink 1025/0c4bb4af 1025/0c4bb4af 1025/0c4bb4af disagreements 0 rejected 0
09:04:16.604 epoch 17 -> 18 at daa 1080, 155.7 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 10000 weakest 10000 step epoch 0 | n1 rung 0 e18 | n2 rung 0 e18
09:04:24.927 epoch 18 -> 19 at daa 1140, 164.0 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 10000 weakest 10000 step epoch 0 | n1 rung 0 e19 | n2 rung 0 e19
09:04:29.086 t=168.2 s produced 1170 daa 1170 epoch 19 rungs 0/0/0 blocks/sink 1170/fc48afd9 1170/fc48afd9 1170/fc48afd9 disagreements 0 rejected 0
09:04:33.264 epoch 19 -> 20 at daa 1200, 172.4 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 10000 weakest 10000 step epoch 0 | n1 rung 0 e20 | n2 rung 0 e20
09:04:33.264 production ended at 172.4 s: 1200 blocks, last daa 1199; settling 4 s
09:04:37.475 SUMMARY PASS (case floor): steps none (oracle none, expected none); 1201 blocks (linear) bits {"none":1,"down":1200} both-bits 0; rejected 0; disagreements 0; sinks a52e6a71 a52e6a71 a52e6a71 at 1200/1200/1200; restarts none
09:04:37.475 EPOCH 11 seed 649: rungs 0/0/0 n0 weakest up 0 down 10000 | oracle up 0 down 10000 rung 0 (windows not full)
09:04:37.475 EPOCH 12 seed 709: rungs 0/0/0 n0 weakest up 0 down 10000 | oracle up 0 down 10000 rung 0 (down signalled at rung 0: the floor)
09:04:37.475 EPOCH 13 seed 769: rungs 0/0/0 n0 weakest up 0 down 10000 | oracle up 0 down 10000 rung 0 (down signalled at rung 0: the floor)
09:04:37.475 EPOCH 14 seed 829: rungs 0/0/0 n0 weakest up 0 down 10000 | oracle up 0 down 10000 rung 0 (down signalled at rung 0: the floor)
09:04:37.475 EPOCH 15 seed 889: rungs 0/0/0 n0 weakest up 0 down 10000 | oracle up 0 down 10000 rung 0 (down signalled at rung 0: the floor)
09:04:37.475 EPOCH 16 seed 949: rungs 0/0/0 n0 weakest up 0 down 10000 | oracle up 0 down 10000 rung 0 (down signalled at rung 0: the floor)
09:04:37.475 EPOCH 17 seed 1009: rungs 0/0/0 n0 weakest up 0 down 10000 | oracle up 0 down 10000 rung 0 (down signalled at rung 0: the floor)
09:04:37.475 EPOCH 18 seed 1069: rungs 0/0/0 n0 weakest up 0 down 10000 | oracle up 0 down 10000 rung 0 (down signalled at rung 0: the floor)
09:04:37.475 EPOCH 19 seed 1129: rungs 0/0/0 n0 weakest up 0 down 10000 | oracle up 0 down 10000 rung 0 (down signalled at rung 0: the floor)
09:04:37.475 EPOCH 20 seed 1189: rungs 0/0/0 n0 weakest up 0 down 10000 | oracle up 0 down 10000 rung 0 (down signalled at rung 0: the floor)
09:04:37.475 summary: /tmp/igneum-fast-time-attack-f10/summary.json
F10-END exact-floor rc=0 2026-10-07T09:04:38Z

View file

@ -0,0 +1,6 @@
queue: baseline-fail rc=1
queue: baseline-pass rc=0
queue: exact-89 rc=1
queue: exact-down rc=1
queue: exact-floor rc=0
QUEUE-END 2026-10-07T09:04:38Z

View file

@ -0,0 +1,3 @@
queue2: exact-89b rc=0
queue2: exact-downb rc=1
QUEUE2-END 2026-10-07T09:18:26Z

View file

@ -0,0 +1,362 @@
{
"pass": false,
"case": "smoke",
"schedule": [
{
"from": 0,
"dir": "up",
"nones": 11
}
],
"expect_steps": [],
"restarts": [],
"checks": {
"chain_is_linear_one_block_per_daa": false,
"zero_rejected_submits": true,
"zero_rejected_by_nodes": false,
"sinks_agree": true,
"block_counts_agree": true,
"every_block_version_2_object_0": true,
"ran_the_epochs": true,
"nodes_never_disagree_on_the_rung_at_the_same_epoch": true,
"every_node_reported_every_epoch_after_the_first_full": true,
"node_weakest_equals_oracle_weakest": true,
"node_rung_equals_oracle_rung_every_epoch": true,
"step_lines_identical_on_every_node": true,
"steps_equal_the_oracle": true,
"steps_equal_the_expectation": true,
"no_step_under_9000_in_its_direction": true,
"every_step_moves_one_rung": true,
"restarted_nodes_recomputed_the_same_steps": true
},
"window": 100,
"windows": 7,
"threshold_bps": 9000,
"epoch_blocks": 60,
"lead": 10,
"first_full_epoch": 12,
"ladder": [
{
"reps": 27,
"admissible": true
},
{
"reps": 35,
"admissible": true
},
{
"reps": 53,
"admissible": true
},
{
"reps": 88,
"admissible": false
},
{
"reps": 173,
"admissible": false
},
{
"reps": 267,
"admissible": false
}
],
"node": "/srv/builds/igneum-wt-ladder/vendor/igneum-node-ladder/target/release/igneumd",
"rate": 8,
"produced": 180,
"run_ended_at_s": 30,
"last_daa": 179,
"max_epoch_seen": 3,
"blocks": {
"total": 181,
"chain": 181,
"linear": false,
"bits": {
"none": 23,
"up": 158
},
"both_bits_blocks": 10,
"low_bytes": [
2
],
"object_bytes": [
0
]
},
"rejected_submits": 0,
"submit_errors": [],
"disagreements": [],
"dag": [
{
"blocks": 180,
"sink": "61c1d7dff1577c06"
},
{
"blocks": 180,
"sink": "61c1d7dff1577c06"
},
{
"blocks": 180,
"sink": "61c1d7dff1577c06"
}
],
"epochs": [
{
"epoch": 0,
"seed_daa": 0,
"rungs": [
0,
0,
0
],
"n0_up_weakest": 0,
"n0_down_weakest": 0,
"oracle_up_weakest": 0,
"oracle_down_weakest": 0,
"oracle_step": 0,
"oracle_reason": "windows not full",
"stepped": false
},
{
"epoch": 1,
"seed_daa": 49,
"rungs": [
0,
0,
0
],
"n0_up_weakest": 0,
"n0_down_weakest": 0,
"oracle_up_weakest": 0,
"oracle_down_weakest": 0,
"oracle_step": 0,
"oracle_reason": "windows not full",
"stepped": false
},
{
"epoch": 2,
"seed_daa": 109,
"rungs": [
0,
0,
0
],
"n0_up_weakest": 0,
"n0_down_weakest": 0,
"oracle_up_weakest": 0,
"oracle_down_weakest": 0,
"oracle_step": 0,
"oracle_reason": "windows not full",
"stepped": false
},
{
"epoch": 3,
"seed_daa": 169,
"rungs": [
0,
0,
0
],
"n0_up_weakest": 0,
"n0_down_weakest": 0,
"oracle_up_weakest": 0,
"oracle_down_weakest": 0,
"oracle_step": 0,
"oracle_reason": "windows not full",
"stepped": false
}
],
"oracle": [
{
"epoch": 0,
"seed_daa": 0,
"full": false,
"first_counted_daa": -699,
"windows": [
0,
0,
0,
0,
0,
0,
0
],
"up_bps": [
0,
0,
0,
0,
0,
0,
0
],
"down_bps": [
0,
0,
0,
0,
0,
0,
0
],
"weakest_up": 0,
"weakest_down": 0,
"step": 0,
"stepped": false,
"reason": "windows not full"
},
{
"epoch": 1,
"seed_daa": 49,
"full": false,
"first_counted_daa": -650,
"windows": [
0,
0,
0,
0,
0,
0,
49
],
"up_bps": [
0,
0,
0,
0,
0,
0,
7959
],
"down_bps": [
0,
0,
0,
0,
0,
0,
0
],
"weakest_up": 0,
"weakest_down": 0,
"step": 0,
"stepped": false,
"reason": "windows not full"
},
{
"epoch": 2,
"seed_daa": 109,
"full": false,
"first_counted_daa": -590,
"windows": [
0,
0,
0,
0,
0,
9,
100
],
"up_bps": [
0,
0,
0,
0,
0,
0,
8900
],
"down_bps": [
0,
0,
0,
0,
0,
0,
0
],
"weakest_up": 0,
"weakest_down": 0,
"step": 0,
"stepped": false,
"reason": "windows not full"
},
{
"epoch": 3,
"seed_daa": 169,
"full": false,
"first_counted_daa": -530,
"windows": [
0,
0,
0,
0,
0,
69,
100
],
"up_bps": [
0,
0,
0,
0,
0,
8550,
8900
],
"down_bps": [
0,
0,
0,
0,
0,
0,
0
],
"weakest_up": 0,
"weakest_down": 0,
"step": 0,
"stepped": false,
"reason": "windows not full"
}
],
"node_steps": [],
"oracle_steps": [],
"step_lines": [
[],
[],
[]
],
"polls": [
{
"t": 5.5,
"daa": 5,
"epoch": 0,
"rungs": [
0,
0,
0
],
"nodes": [
"5/89850549",
"5/89850549",
"5/89850549"
]
},
{
"t": 25.8,
"daa": 150,
"epoch": 2,
"rungs": [
0,
0,
0
],
"nodes": [
"150/e007eba2",
"150/e007eba2",
"150/e007eba2"
]
}
]
}

View file

@ -0,0 +1,21 @@
08:14:09.662 case smoke: schedule 0:up:11 (W 100, 7 windows, threshold 9000 bps); expect steps none; restarts none; 60 DAA per epoch, lead 10, first epoch with seven full windows 12; ladder 27, 35, 53, [88], [173], [267]; 8 blocks/s, up to 3 epochs or 150 s
08:14:11.199 n0 up (start 1) pid 1237888 json 29902 p2p 29901
08:14:12.709 n1 up (start 1) pid 1238170 json 29912 p2p 29911
08:14:14.215 n2 up (start 1) pid 1238330 json 29922 p2p 29921
08:14:14.216 n0: Latency ladder active: rungs 27, 35, 53, [88], [173], [267] shadow passes, this node signals none (header version bits 15 and 14)
08:14:14.216 n1: Latency ladder active: rungs 27, 35, 53, [88], [173], [267] shadow passes, this node signals none (header version bits 15 and 14)
08:14:14.216 n2: Latency ladder active: rungs 27, 35, 53, [88], [173], [267] shadow passes, this node signals none (header version bits 15 and 14)
08:14:14.216 n0 digest: 5d6d9f3d3f504b84
08:14:14.521 first submit at daa 0 sig none version 0x2: {"report":{"type":"success"}}
08:14:15.192 epoch -1 -> 0 at daa 5, 5.5 s: n0 rung 0 (27) next 0 (27) up 0 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e0 | n2 rung 0 e0
08:14:15.193 t=5.5 s produced 5 daa 5 epoch 0 rungs 0/0/0 blocks/sink 5/89850549 5/89850549 5/89850549 disagreements 0 rejected 0
08:14:22.873 epoch 0 -> 1 at daa 60, 13.2 s: n0 rung 0 (27) next 0 (27) up 8305 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e1 | n2 rung 0 e1
08:14:31.295 epoch 1 -> 2 at daa 120, 21.6 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e2 | n2 rung 0 e2
08:14:35.453 t=25.8 s produced 150 daa 150 epoch 2 rungs 0/0/0 blocks/sink 150/e007eba2 150/e007eba2 150/e007eba2 disagreements 0 rejected 0
08:14:39.654 epoch 2 -> 3 at daa 180, 30.0 s: n0 rung 0 (27) next 0 (27) up 8900 weakest 0 down 0 weakest 0 step epoch 0 | n1 rung 0 e3 | n2 rung 0 e3
08:14:39.654 production ended at 30 s: 180 blocks, last daa 179; settling 4 s
08:14:43.696 SUMMARY FAIL (case smoke): steps none (oracle none, expected none); 181 blocks (NOT linear) bits {"none":23,"up":158} both-bits 10; rejected 0; disagreements 0; sinks 61c1d7df 61c1d7df 61c1d7df at 180/180/180; restarts none
08:14:43.696 FAILED CHECK chain_is_linear_one_block_per_daa
08:14:43.696 FAILED CHECK zero_rejected_by_nodes
08:14:43.697 summary: /tmp/igneum-fast-time-attack-f10/summary.json
F10-END smoke rc=1 2026-10-07T08:14:45Z

14
tools/attack/f2-mixer/Cargo.lock generated Normal file
View file

@ -0,0 +1,14 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "attack-f2"
version = "0.1.0"
dependencies = [
"igneum-pow",
]
[[package]]
name = "igneum-pow"
version = "0.2.0"

View file

@ -0,0 +1,20 @@
[package]
name = "attack-f2"
version = "0.1.0"
edition = "2021"
description = "Attack-pass row F2: the mixer's round margin. Ground truth for the SAT models (parameters, vectors, empirical trail and mask verification, rotational-XOR bias, the multiply-layer fold checks) on the exact mixer of igneum-pow/src/memhard.rs"
publish = false
[[bin]]
name = "attack-f2"
path = "src/main.rs"
[dependencies]
igneum-pow = { path = "../../../igneum-pow" }
[workspace]
[profile.release]
opt-level = 3
lto = true
codegen-units = 1

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,50 @@
#!/usr/bin/env bash
# attack-f2 job runner for igneum-build-1: N workers over a job list, every job a resumable `model.py search`
# run in chunks of at most 1,700 s under a SHARED hold of the box measure file (so the F6 timing agent's
# exclusive hold can get in between chunks), on the F2 cores, at nice 10. State and logs under
# /srv/builds/igneum-wt-attack/target-attack-f2 (a target-* path: build-remote's rsync --delete spares it).
#
# run_jobs.sh JOBFILE WORKERS JOBFILE lines: kind family day variant apps cap_seconds
set -u
D=/srv/builds/igneum-wt-attack/target-attack-f2
PY=$D/venv/bin/python3
JOBS=$1; WORKERS=${2:-10}
CHUNK=${CHUNK:-1700}
mkdir -p $D/state $D/logs
run_one() {
local kind=$1 family=$2 day=$3 variant=$4 apps=$5 cap=$6
local name="$kind-$family-$day-$variant-k$apps"
local st=$D/state/$name.json log=$D/logs/$name.log params=$D/params/$day.$variant.txt
local spent=0
echo "$(date -u +%FT%TZ) start $name cap $cap" >> $log
while :; do
if [ -f "$st" ] && $PY -c "import json,sys; s=json.load(open('$st')); sys.exit(0 if s.get('done') else 1)"; then
echo "$(date -u +%FT%TZ) done $name" >> $log; break
fi
if [ $spent -ge $cap ]; then
echo "$(date -u +%FT%TZ) cap reached $name after ${spent}s" >> $log
[ -f "$st" ] && $PY - "$st" <<'PYEOF'
import json, sys
p = sys.argv[1]; s = json.load(open(p))
if not s.get("done"):
s["stuck_at"] = s.get("pending") if s.get("pending") is not None else s.get("stuck_at")
s["cap_reached"] = True
json.dump(s, open(p, "w"), indent=1)
PYEOF
break
fi
local budget=$CHUNK; [ $((cap - spent)) -lt $budget ] && budget=$((cap - spent))
# Matsui: every application of a chain is held to the PROVEN lower bound of the k=1 search of the same model
local pam=0 st1=$D/state/$kind-$family-$day-$variant-k1.json
if [ "$apps" -ge 2 ] && [ -f "$st1" ]; then
pam=$($PY -c "import json; s=json.load(open('$st1')); print(s['unsat_upto'] + 1)")
fi
local t0=$(date +%s)
flock -s /srv/builds/_locks/measure -c "nice -n 10 taskset -c 6-11,54-59 $PY $D/model.py search --kind $kind --family $family --params $params --apps $apps --state $st --budget $budget --per-app-min $pam --verifier /srv/builds/igneum-wt-attack/tools/attack/f2-mixer/target/release/attack-f2" >> $log 2>&1
spent=$((spent + $(date +%s) - t0))
done
}
export -f run_one; export D PY CHUNK
# a tiny queue: xargs runs WORKERS jobs at a time
grep -v '^#' "$JOBS" | grep -v '^\s*$' | xargs -P "$WORKERS" -L 1 bash -c 'run_one "$@"' _
echo "$(date -u +%FT%TZ) all jobs finished" >> $D/logs/runner.log

View file

@ -0,0 +1,519 @@
//! attack-f2: the mixer's round margin (attack pass row F2, docs/plans/cryptanalysis.md 4.2).
//!
//! The SAT models live beside this crate in Python (`model.py`); this binary is the ground truth they are
//! checked against. Every value here comes from `igneum_pow::memhard::mixer`, the bit-level definition that
//! ships, never from a copy of it.
//!
//! attack-f2 params --day D [--variant V] the drawn ROT, MUL, RC of the day (and the variant's)
//! attack-f2 vectors --day D [--variant V] [--n N] [--seed S] N random states and their images after 1..4
//! applications, for the Python model's value check
//! attack-f2 verify-diff --day D [--variant V] --trail FILE [--log2 L] [--rk-base R]
//! FILE: k+1 lines of 16 hex words, the XOR difference entering application 1 and the
//! difference leaving each application; per application the measured probability over
//! 2^L random states, and the whole chain's
//! attack-f2 verify-lin --day D [--variant V] --trail FILE [--log2 L] [--rk-base R]
//! FILE: k+1 lines of 16 hex masks; per application the measured correlation of
//! mask_in . x xor mask_out . y, and the whole chain's
//! attack-f2 rx --day D [--variant V] [--apps K] [--log2 L] [--rk-base R]
//! rotational-XOR: for every rotation r in 1..31 the per-bit bias of
//! rot_r(M^k(x)) xor M^k(rot_r(x)) over 2^L states, the largest |z| per k and r
//! attack-f2 rx-word --day D [--variant V] [--log2 L]
//! the single-word prologue g(x) = (x ^ C) * MUL: for every word and r the most frequent
//! value of rot_r(g(x)) xor g(rot_r(x)) and its count (the word-level RX probability)
//! attack-f2 fold --day D [--log2 L]
//! the multiply layer against the add layer: the identities a chip would need, tested
//! attack-f2 verify-mults --kind diff|lin --day D [--variant V] --file FILE [--rk-base R]
//! FILE (written by model.py beside a trail): lines `app j word i din dout` (diff) or
//! `app j word i min mout` (lin); every word transition of the multiply layer counted
//! EXACTLY over all 2^32 inputs of g(x) = (x ^ (RC + rk)) * MUL (12 threads)
//! attack-f2 word-top --day D [--variant V] --word I --din X [--log2 L]
//! the sampled top output differences of one word's prologue for input difference X
//!
//! Variants: `real` (the day's draw), `rot0` (every rotation 0: the known-fail case for the differential and
//! linear models), `nomul` (MUL 1, RC 0, rk 0: the bare double round, the known-fail case for rotational-XOR).
//! Application j of a chain uses the round key `round_key_mult(rk_base, j, 8)`: round 0's eight keys by default.
use igneum_pow::memhard::{mixer, round_key_mult, MixParams, Shape};
use igneum_pow::seed::{day_key, SplitMix64};
use std::collections::HashMap;
use std::env;
use std::fs;
const M: usize = 8;
fn arg(args: &[String], name: &str) -> Option<String> {
args.iter().position(|a| a == name).and_then(|i| args.get(i + 1).cloned())
}
fn params_for(day: &str, variant: &str) -> MixParams {
let shape = Shape { mixer_mult: M as u32, cache_log2_words: 26, derive_len: 0 };
let mut mp = MixParams::with_shape(day_key(day), shape);
match variant {
"real" => {}
"rot0" => mp.rot = [0; 8],
"nomul" => {
mp.mul = [1; 16];
mp.rc = [0; 16];
}
other => panic!("unknown variant {other}"),
}
mp
}
/// The round key of application `j` of round `rk_base` (0 under `nomul`, which drops the keys too).
fn rk_of(variant: &str, rk_base: usize, j: usize) -> u32 {
if variant == "nomul" {
0
} else {
round_key_mult(rk_base, j, M)
}
}
fn apply(s: &mut [u32; 16], mp: &MixParams, variant: &str, rk_base: usize, apps: usize) {
for j in 0..apps {
mixer(s, rk_of(variant, rk_base, j), mp);
}
}
fn rand_state(rng: &mut SplitMix64) -> [u32; 16] {
let mut s = [0u32; 16];
for w in s.iter_mut() {
*w = rng.next() as u32;
}
s
}
fn hex16(s: &[u32; 16]) -> String {
s.iter().map(|w| format!("{w:08x}")).collect::<Vec<_>>().join(" ")
}
fn parse_trail(path: &str) -> Vec<[u32; 16]> {
let text = fs::read_to_string(path).unwrap_or_else(|e| panic!("read {path}: {e}"));
let mut out = Vec::new();
for line in text.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let words: Vec<u32> = line.split_whitespace().map(|h| u32::from_str_radix(h, 16).expect("hex word")).collect();
assert_eq!(words.len(), 16, "a trail line has 16 hex words");
let mut s = [0u32; 16];
s.copy_from_slice(&words);
out.push(s);
}
out
}
fn rotl_state(s: &[u32; 16], r: u32) -> [u32; 16] {
let mut o = *s;
for w in o.iter_mut() {
*w = w.rotate_left(r);
}
o
}
fn parity(mask: &[u32; 16], s: &[u32; 16]) -> u32 {
let mut p = 0u32;
for i in 0..16 {
p ^= (mask[i] & s[i]).count_ones() & 1;
}
p
}
fn cmd_params(day: &str, variant: &str) {
let mp = params_for(day, variant);
println!("day {day}");
println!("variant {variant}");
println!("key {}", mp.key.iter().map(|w| format!("{w:08x}")).collect::<Vec<_>>().join(" "));
println!("rot {}", mp.rot.iter().map(|r| r.to_string()).collect::<Vec<_>>().join(" "));
println!("mul {}", mp.mul.iter().map(|w| format!("{w:08x}")).collect::<Vec<_>>().join(" "));
println!("rc {}", mp.rc.iter().map(|w| format!("{w:08x}")).collect::<Vec<_>>().join(" "));
let rks: Vec<String> = (0..M).map(|j| format!("{:08x}", rk_of(variant, 0, j))).collect();
println!("rk_round0 {}", rks.join(" "));
let w: Vec<String> = mp.mul.iter().map(|m| m.count_ones().to_string()).collect();
println!("mul_weight {}", w.join(" "));
}
fn cmd_vectors(day: &str, variant: &str, n: usize, seed: u64) {
let mp = params_for(day, variant);
let mut rng = SplitMix64::new(seed);
for _ in 0..n {
let x = rand_state(&mut rng);
print!("{}", hex16(&x));
let mut s = x;
for j in 0..4 {
mixer(&mut s, rk_of(variant, 0, j), &mp);
print!(" | {}", hex16(&s));
}
println!();
}
}
fn cmd_verify_diff(day: &str, variant: &str, path: &str, log2: u32, rk_base: usize) {
let mp = params_for(day, variant);
let trail = parse_trail(path);
let k = trail.len() - 1;
assert!(k >= 1, "a trail needs at least two lines");
let n = 1u64 << log2;
let mut rng = SplitMix64::new(0xf2_d1ff);
let mut per_app = vec![0u64; k];
let mut chain = 0u64;
for _ in 0..n {
// per application j: a fresh random state, the pair (x, x ^ d[j]) through application j alone
for j in 0..k {
let x = rand_state(&mut rng);
let mut a = x;
let mut b = [0u32; 16];
for i in 0..16 {
b[i] = x[i] ^ trail[j][i];
}
let rk = rk_of(variant, rk_base, j);
mixer(&mut a, rk, &mp);
mixer(&mut b, rk, &mp);
let mut ok = true;
for i in 0..16 {
ok &= (a[i] ^ b[i]) == trail[j + 1][i];
}
per_app[j] += ok as u64;
}
// the chain: one pair through all k applications, the final difference only
let x = rand_state(&mut rng);
let mut a = x;
let mut b = [0u32; 16];
for i in 0..16 {
b[i] = x[i] ^ trail[0][i];
}
apply(&mut a, &mp, variant, rk_base, k);
apply(&mut b, &mp, variant, rk_base, k);
let mut ok = true;
for i in 0..16 {
ok &= (a[i] ^ b[i]) == trail[k][i];
}
chain += ok as u64;
}
println!("day {day} variant {variant} apps {k} samples 2^{log2} rk_base {rk_base}");
for j in 0..k {
let p = per_app[j] as f64 / n as f64;
let w = if per_app[j] == 0 { f64::INFINITY } else { -p.log2() };
println!("app {} count {} prob {:.6e} weight {:.3}", j + 1, per_app[j], p, w);
}
let p = chain as f64 / n as f64;
let w = if chain == 0 { f64::INFINITY } else { -p.log2() };
println!("chain count {chain} prob {p:.6e} weight {w:.3}");
}
fn cmd_verify_lin(day: &str, variant: &str, path: &str, log2: u32, rk_base: usize) {
let mp = params_for(day, variant);
let trail = parse_trail(path);
let k = trail.len() - 1;
assert!(k >= 1, "a trail needs at least two lines");
let n = 1u64 << log2;
let mut rng = SplitMix64::new(0xf2_11ea);
let mut per_app = vec![0i64; k];
let mut chain = 0i64;
for _ in 0..n {
for j in 0..k {
let x = rand_state(&mut rng);
let mut y = x;
mixer(&mut y, rk_of(variant, rk_base, j), &mp);
let p = parity(&trail[j], &x) ^ parity(&trail[j + 1], &y);
per_app[j] += 1 - 2 * p as i64;
}
let x = rand_state(&mut rng);
let mut y = x;
apply(&mut y, &mp, variant, rk_base, k);
let p = parity(&trail[0], &x) ^ parity(&trail[k], &y);
chain += 1 - 2 * p as i64;
}
let sigma = (n as f64).sqrt();
println!("day {day} variant {variant} apps {k} samples 2^{log2} rk_base {rk_base}");
for j in 0..k {
let c = per_app[j] as f64 / n as f64;
let z = per_app[j] as f64 / sigma;
let w = if per_app[j] == 0 { f64::INFINITY } else { -c.abs().log2() };
println!("app {} sum {} corr {:+.6e} abs_log2 {:.3} z {:+.2}", j + 1, per_app[j], c, w, z);
}
let c = chain as f64 / n as f64;
let z = chain as f64 / sigma;
let w = if chain == 0 { f64::INFINITY } else { -c.abs().log2() };
println!("chain sum {chain} corr {c:+.6e} abs_log2 {w:.3} z {z:+.2}");
}
fn cmd_rx(day: &str, variant: &str, apps: usize, log2: u32, rk_base: usize) {
let mp = params_for(day, variant);
let n = 1u64 << log2;
let sigma = (n as f64 / 4.0).sqrt();
println!("day {day} variant {variant} samples 2^{log2} rk_base {rk_base}");
println!("# columns: apps r max_abs_z bit ones exact_rx_count (D == 0)");
for k in 1..=apps {
for r in 1..32u32 {
let mut rng = SplitMix64::new(0xf2_0000 + r as u64 + 100 * k as u64);
let mut ones = [0u64; 512];
let mut exact = 0u64;
for _ in 0..n {
let x = rand_state(&mut rng);
let mut a = x;
apply(&mut a, &mp, variant, rk_base, k);
let a = rotl_state(&a, r);
let mut b = rotl_state(&x, r);
apply(&mut b, &mp, variant, rk_base, k);
let mut zero = true;
for i in 0..16 {
let d = a[i] ^ b[i];
zero &= d == 0;
let mut dd = d;
while dd != 0 {
let t = dd.trailing_zeros();
ones[i * 32 + t as usize] += 1;
dd &= dd - 1;
}
}
exact += zero as u64;
}
let mut best = (0.0f64, 0usize);
for (b, &c) in ones.iter().enumerate() {
let z = (c as f64 - n as f64 / 2.0).abs() / sigma;
if z > best.0 {
best = (z, b);
}
}
println!("rx apps {} r {} max_abs_z {:.2} bit {} ones {} exact {}", k, r, best.0, best.1, ones[best.1], exact);
}
}
}
fn cmd_rx_word(day: &str, variant: &str, log2: u32) {
let mp = params_for(day, variant);
let n = 1u64 << log2;
println!("day {day} variant {variant} samples 2^{log2} (prologue word map g(x) = (x ^ (RC + rk0)) * MUL)");
println!("# columns: word r top_delta top_count top_log2prob");
let rk = rk_of(variant, 0, 0);
let mut worst_per_r = vec![0u64; 32];
for i in 0..16 {
let c = mp.rc[i].wrapping_add(rk);
let m = mp.mul[i];
let g = |x: u32| (x ^ c).wrapping_mul(m);
for r in 1..32u32 {
let mut rng = SplitMix64::new(0xf2_0f00 + i as u64 * 64 + r as u64);
let mut counts: HashMap<u32, u32> = HashMap::new();
for _ in 0..n {
let x = rng.next() as u32;
let d = g(x).rotate_left(r) ^ g(x.rotate_left(r));
*counts.entry(d).or_insert(0) += 1;
}
let (delta, cnt) = counts.iter().max_by_key(|(_, &c)| c).map(|(&d, &c)| (d, c)).unwrap();
worst_per_r[r as usize] = worst_per_r[r as usize].max(cnt as u64);
println!("rxw word {} r {} top_delta {:08x} top_count {} top_log2prob {:.2}", i, r, delta, cnt, -((cnt as f64) / (n as f64)).log2());
}
}
for r in 1..32 {
println!("rxw_worst r {} top_count {} top_log2prob {:.2}", r, worst_per_r[r], -((worst_per_r[r] as f64) / (n as f64)).log2());
}
}
fn cmd_fold(day: &str, log2: u32) {
let mp = params_for(day, "real");
let n = 1u64 << log2;
let mut rng = SplitMix64::new(0xf2_f01d);
println!("day {day} samples 2^{log2}");
// (a) the first add of each column quarter round: (xa ^ Ca) * ma + (xb ^ Cb) * mb against ((xa ^ Ca) + (xb ^ Cb)) * ma:
// the multiply folds into the add only when ma == mb (a chip could then do one multiply for two words)
let rk = rk_of("real", 0, 0);
for (a, b) in [(0usize, 4usize), (1, 5), (2, 6), (3, 7)] {
let (ca, cb) = (mp.rc[a].wrapping_add(rk), mp.rc[b].wrapping_add(rk));
let (ma, mb) = (mp.mul[a], mp.mul[b]);
let mut eq = 0u64;
for _ in 0..n {
let (xa, xb) = (rng.next() as u32, rng.next() as u32);
let lhs = (xa ^ ca).wrapping_mul(ma).wrapping_add((xb ^ cb).wrapping_mul(mb));
let rhs = ((xa ^ ca).wrapping_add(xb ^ cb)).wrapping_mul(ma);
eq += (lhs == rhs) as u64;
}
println!("fold_add words {a},{b} mul_equal {} identity_holds {eq} of {n}", ma == mb);
}
// (b) the XOR constant against the multiply: (x ^ C) * m against (x * m) ^ (C * m) and against (x * m) ^ C'
// for the best single C' (counted on word 0): the constant does not pass through the multiply
{
let (c, m) = (mp.rc[0].wrapping_add(rk), mp.mul[0]);
let mut eq = 0u64;
let mut counts: HashMap<u32, u32> = HashMap::new();
for _ in 0..n {
let x = rng.next() as u32;
let lhs = (x ^ c).wrapping_mul(m);
eq += (lhs == x.wrapping_mul(m) ^ c.wrapping_mul(m)) as u64;
*counts.entry(lhs ^ x.wrapping_mul(m)).or_insert(0) += 1;
}
let best = counts.values().max().copied().unwrap_or(0);
println!("fold_xor word 0 (x^C)*m == (x*m)^(C*m): {eq} of {n}; best single C' matches {best} of {n}");
}
// (c) the MSB passes the prologue and every add for free: (x ^ 2^31) through g and through x + y
{
let mut eq_g = 0u64;
let mut eq_add = 0u64;
for i in 0..16 {
let (c, m) = (mp.rc[i].wrapping_add(rk), mp.mul[i]);
for _ in 0..(n >> 4) {
let x = rng.next() as u32;
let y = rng.next() as u32;
eq_g += (((x ^ 0x8000_0000) ^ c).wrapping_mul(m) == (x ^ c).wrapping_mul(m) ^ 0x8000_0000) as u64;
eq_add += ((x ^ 0x8000_0000).wrapping_add(y) == x.wrapping_add(y) ^ 0x8000_0000) as u64;
}
}
println!("msb_free prologue {eq_g} of {} ; add {eq_add} of {}", (n >> 4) * 16, (n >> 4) * 16);
}
// (d) the LSB of a product is the LSB of the input (odd multiplier), and of a sum the XOR of the inputs' LSBs
{
let mut eq = 0u64;
for i in 0..16 {
let (c, m) = (mp.rc[i].wrapping_add(rk), mp.mul[i]);
for _ in 0..(n >> 4) {
let x = rng.next() as u32;
eq += (((x ^ c).wrapping_mul(m)) & 1 == (x ^ c) & 1) as u64;
}
}
println!("lsb_free prologue {eq} of {}", (n >> 4) * 16);
}
// (e) does an XOR constant on any single word commute with the bare double round (so that the next
// application's RC + rk could be folded back into the previous one)? Tested per word against the constant
// coming out on the same word under any rotation. Expected 0 everywhere: every word's value feeds an add.
{
let mut eqs = [0u64; 16];
let mut bare = mp.clone();
bare.mul = [1; 16];
bare.rc = [0; 16];
for _ in 0..(n >> 4) {
let x = rand_state(&mut rng);
let kk = rng.next() as u32;
let mut b = x;
mixer(&mut b, 0, &bare);
for w in 0..16 {
let mut a = x;
a[w] ^= kk;
mixer(&mut a, 0, &bare);
let mut any = false;
for r in 0..32u32 {
let mut c = b;
c[w] ^= kk.rotate_left(r);
any |= c == a;
}
eqs[w] += any as u64;
}
}
println!("xor_const_commutes_with_arx per word: {}", eqs.iter().map(|e| e.to_string()).collect::<Vec<_>>().join(" "));
println!(" (of {} each; a constant that commutes would read the full count)", n >> 4);
}
}
/// Exact count over all 2^32 inputs of `g(u ^ din) ^ g(u) == dout` (diff) or the signed sum of
/// `(-1)^(min.u ^ mout.g(u))` (lin) for the word map g(u) = (u ^ k) * m, on 12 threads.
fn word_exact(k: u32, m: u32, kind: &str, a: u32, b: u32) -> i64 {
const T: u64 = 12;
let chunk = (1u64 << 32) / T;
let totals: Vec<i64> = std::thread::scope(|sc| {
let hs: Vec<_> = (0..T)
.map(|t| {
sc.spawn(move || {
let mut acc: i64 = 0;
let lo = t * chunk;
let hi = if t == T - 1 { 1u64 << 32 } else { lo + chunk };
if kind == "diff" {
for u in lo..hi {
let u = u as u32;
let y0 = (u ^ k).wrapping_mul(m);
let y1 = ((u ^ a) ^ k).wrapping_mul(m);
acc += ((y0 ^ y1) == b) as i64;
}
} else {
for u in lo..hi {
let u = u as u32;
let y = (u ^ k).wrapping_mul(m);
let par = ((a & u).count_ones() + (b & y).count_ones()) & 1;
acc += 1 - 2 * par as i64;
}
}
acc
})
})
.collect();
hs.into_iter().map(|h| h.join().unwrap()).collect()
});
totals.iter().sum()
}
fn cmd_verify_mults(day: &str, variant: &str, kind: &str, path: &str, rk_base: usize) {
let mp = params_for(day, variant);
let text = fs::read_to_string(path).unwrap_or_else(|e| panic!("read {path}: {e}"));
println!("day {day} variant {variant} kind {kind} rk_base {rk_base} (exact over 2^32 per word)");
let mut total_weight = 0.0f64;
let mut n = 0;
for line in text.lines() {
let f: Vec<&str> = line.split_whitespace().collect();
if f.len() < 6 || f[0] != "app" {
continue;
}
let j: usize = f[1].parse().unwrap();
let i: usize = f[3].parse().unwrap();
let a = u32::from_str_radix(f[4], 16).unwrap();
let b = u32::from_str_radix(f[5], 16).unwrap();
let k = mp.rc[i].wrapping_add(rk_of(variant, rk_base, j - 1));
let cnt = word_exact(k, mp.mul[i], kind, a, b);
let w = if kind == "diff" {
if cnt == 0 { f64::INFINITY } else { -((cnt as f64) / 4294967296.0).log2() }
} else if cnt == 0 { f64::INFINITY } else { -((cnt.unsigned_abs() as f64) / 4294967296.0).log2() };
total_weight += w;
n += 1;
println!("app {j} word {i} {a:08x} -> {b:08x} count {cnt} weight {w:.3}");
}
println!("words {n} total_exact_weight {total_weight:.3}");
}
fn cmd_word_top(day: &str, variant: &str, word: usize, din: u32, log2: u32) {
let mp = params_for(day, variant);
let k = mp.rc[word].wrapping_add(rk_of(variant, 0, 0));
let m = mp.mul[word];
let n = 1u64 << log2;
let mut rng = SplitMix64::new(0xf2_70b);
let mut counts: HashMap<u32, u32> = HashMap::new();
for _ in 0..n {
let u = rng.next() as u32;
let d = (u ^ k).wrapping_mul(m) ^ ((u ^ din) ^ k).wrapping_mul(m);
*counts.entry(d).or_insert(0) += 1;
}
let mut v: Vec<(u32, u32)> = counts.into_iter().collect();
v.sort_by(|a, b| b.1.cmp(&a.1));
println!("day {day} variant {variant} word {word} din {din:08x} samples 2^{log2} distinct {}", v.len());
for (d, c) in v.iter().take(8) {
println!("top dout {d:08x} count {c} log2prob {:.2}", -((*c as f64) / (n as f64)).log2());
}
}
fn main() {
let args: Vec<String> = env::args().collect();
let cmd = args.get(1).map(String::as_str).unwrap_or("");
let day = arg(&args, "--day").unwrap_or_else(|| "2026-10-03".to_string());
let variant = arg(&args, "--variant").unwrap_or_else(|| "real".to_string());
let log2: u32 = arg(&args, "--log2").map(|s| s.parse().unwrap()).unwrap_or(20);
let rk_base: usize = arg(&args, "--rk-base").map(|s| s.parse().unwrap()).unwrap_or(0);
match cmd {
"params" => cmd_params(&day, &variant),
"vectors" => {
let n: usize = arg(&args, "--n").map(|s| s.parse().unwrap()).unwrap_or(8);
let seed: u64 = arg(&args, "--seed").map(|s| s.parse().unwrap()).unwrap_or(1);
cmd_vectors(&day, &variant, n, seed)
}
"verify-diff" => cmd_verify_diff(&day, &variant, &arg(&args, "--trail").expect("--trail FILE"), log2, rk_base),
"verify-lin" => cmd_verify_lin(&day, &variant, &arg(&args, "--trail").expect("--trail FILE"), log2, rk_base),
"rx" => {
let apps: usize = arg(&args, "--apps").map(|s| s.parse().unwrap()).unwrap_or(4);
cmd_rx(&day, &variant, apps, log2, rk_base)
}
"rx-word" => cmd_rx_word(&day, &variant, log2),
"fold" => cmd_fold(&day, log2),
"verify-mults" => cmd_verify_mults(&day, &variant, &arg(&args, "--kind").expect("--kind"), &arg(&args, "--file").expect("--file FILE"), rk_base),
"word-top" => cmd_word_top(&day, &variant, arg(&args, "--word").map(|s| s.parse().unwrap()).unwrap_or(0), u32::from_str_radix(&arg(&args, "--din").expect("--din hex"), 16).unwrap(), log2),
_ => {
eprintln!("usage: attack-f2 (params|vectors|verify-diff|verify-lin|rx|rx-word|fold|verify-mults|word-top) --day D [--variant real|rot0|nomul] ...");
std::process::exit(2);
}
}
}

View file

@ -0,0 +1,92 @@
#!/usr/bin/env python3
"""attack-f2: turn the search states into the record's tables, and verify every k >= 2 trail post hoc on the real
code (per application by sampling, the multiply-layer words exactly over 2^32).
summarise.py --state-dir DIR --binary attack-f2 --out DIR/summary.md [--verify-log2 26]
"""
import argparse
import glob
import json
import os
import subprocess
def run(cmd):
return subprocess.run(cmd, capture_output=True, text=True, check=True).stdout
def verify(binary, kind, st, path, log2):
"""Per-application measured weights and the exact multiply-word weights of a trail (written beside the state)."""
trail = path + ".trail"
mults = path + ".mults"
out = {}
if os.path.exists(trail):
txt = run([binary, "verify-" + kind, "--day", st["day"], "--variant", st["variant"], "--trail", trail, "--log2", str(log2)])
per, chain = [], None
for line in txt.splitlines():
f = line.split()
if f and f[0] == "app":
per.append(f[f.index("weight") + 1] if kind == "diff" else f[f.index("abs_log2") + 1])
elif f and f[0] == "chain":
chain = f[f.index("weight") + 1] if kind == "diff" else f[f.index("abs_log2") + 1]
out["per_app"] = per
out["chain"] = chain
open(path + ".verify.log", "w").write(txt)
if os.path.exists(mults) and os.path.getsize(mults) > 0:
txt = run([binary, "verify-mults", "--kind", kind, "--day", st["day"], "--variant", st["variant"], "--file", mults])
open(path + ".mults.log", "w").write(txt)
last = [l for l in txt.splitlines() if l.startswith("words")]
out["mults_exact"] = last[0] if last else ""
return out
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--state-dir", required=True)
ap.add_argument("--binary", required=True)
ap.add_argument("--out", required=True)
ap.add_argument("--verify-log2", type=int, default=26)
ap.add_argument("--no-verify", action="store_true")
a = ap.parse_args()
rows = []
for path in sorted(glob.glob(os.path.join(a.state_dir, "*.json"))):
try:
st = json.load(open(path))
except json.JSONDecodeError:
import time
time.sleep(2)
try:
st = json.load(open(path))
except json.JSONDecodeError:
print("skipping half-written", path)
continue
name = os.path.basename(path)[:-5]
best = st.get("trail_weight") if st.get("trail") else None
v = {}
if not a.no_verify and st.get("trail") and st["apps"] >= 2 and st.get("verified_chain_weight") is None:
v = verify(a.binary, st["kind"], st, path, a.verify_log2)
rows.append({
"name": name, "kind": st["kind"], "family": st.get("family", "general"), "day": st["day"], "variant": st["variant"],
"apps": st["apps"], "best": best, "unsat_upto": st["unsat_upto"], "done": st.get("done", False),
"pending": st.get("pending"), "cap": st.get("cap_reached", False), "per_app_min": st.get("per_app_min", 0),
"by_tag": st.get("weight_by_tag"), "verified_chain": st.get("verified_chain_weight"),
"verified_per_app": st.get("verified_per_app"), "refuted": st.get("refuted", 0), "cancelled": st.get("cancelled", 0),
"solver_s": round(st.get("solver_seconds", 0)), "post": v, "log": st.get("log", [])[-1:],
})
with open(a.out, "w") as f:
f.write("| model | day | variant | k | best trail weight found | no trail at or below (model) | closed | per-app floor | verified on real code (chain; per app) | exact mult words | refuted / cancelled | solver s |\n")
f.write("|---|---|---|---|---|---|---|---|---|---|---|---|\n")
for r in rows:
ver = ""
if r["verified_chain"] is not None:
ver = f"{r['verified_chain']}; {r['verified_per_app']}"
elif r["post"].get("chain") is not None or r["post"].get("per_app"):
ver = f"{r['post'].get('chain')}; {r['post'].get('per_app')} (2^{a.verify_log2})"
closed = "yes" if r["done"] and (r["best"] is None or r["best"] == r["unsat_upto"] + 1) else ("cap" if r["cap"] else "no")
f.write(f"| {r['kind']}/{r['family']} | {r['day']} | {r['variant']} | {r['apps']} | {r['best']} | {r['unsat_upto']} | {closed} | {r['per_app_min']} | {ver} | {r['post'].get('mults_exact', '')} | {r['refuted']} / {r['cancelled']} | {r['solver_s']} |\n")
json.dump(rows, open(a.out + ".json", "w"), indent=1)
print(open(a.out).read())
if __name__ == "__main__":
main()

14
tools/attack/f6-verifier/Cargo.lock generated Normal file
View file

@ -0,0 +1,14 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "attack-f6"
version = "0.1.0"
dependencies = [
"igneum-pow",
]
[[package]]
name = "igneum-pow"
version = "0.2.0"

View file

@ -0,0 +1,21 @@
[package]
name = "attack-f6"
version = "0.1.0"
edition = "2021"
description = "Attack-pass row F6: the verifier's worst case over 10^5 class v4 programs (dataset built once, programs swapped)"
publish = false
[[bin]]
name = "attack-f6"
path = "src/main.rs"
[dependencies]
igneum-pow = { path = "../../../igneum-pow" }
[workspace]
# The same release profile as igneum-pow's own, so the interpreter's codegen matches the measured CLI numbers.
[profile.release]
opt-level = 3
lto = true
codegen-units = 1

View file

@ -0,0 +1,161 @@
#!/usr/bin/env python3
"""attack-f6 ranking: read the scan CSVs of the harness, rank the programs by an exact op-count proxy (per-family
weights from `attack-f6 micro`) and by the measured time column, print the distribution, write the seed lists.
rank.py --scan scan-0.csv scan-50000.csv --weights "add=11,sub=10,..." [--column cold_min_ms] [--top 50]
[--out-prefix worst] [--regress]
The proxy of program p is sum over families F of w_F x (8 x base_F(p) + 8 x 27 x shadow_F(p)): every base instruction
runs 8 times per hash, every shadow instruction 216 times. Loads carry no weight (every class v4 program has 16).
"""
import argparse
import csv
import math
import sys
FAMILIES = ["add", "sub", "mul", "mulhi", "xor", "or", "rotl", "rotr", "mad", "shfl"]
ITER = 8
REPS = 27
def read(paths):
rows = []
for p in paths:
with open(p) as f:
for r in csv.DictReader(f):
rows.append(r)
return rows
def proxy(r, w):
s = 0.0
for f in FAMILIES:
s += w[f] * (ITER * int(r["base_" + f]) + ITER * REPS * int(r["shadow_" + f]))
return s
def pct(sorted_vals, q):
if not sorted_vals:
return float("nan")
k = min(len(sorted_vals) - 1, max(0, int(math.ceil(q * len(sorted_vals))) - 1))
return sorted_vals[k]
def dist(rows, col, label):
vals = sorted((float(r[col]), r["seed"]) for r in rows if r[col] not in ("", "0.000"))
if not vals:
print(f"{label}: no values in {col}")
return
v = [x for x, _ in vals]
n = len(v)
med = v[n // 2]
print(f"| {label} ({col}, n = {n:,}) | min {v[0]:.3f} ({vals[0][1]}) | median {med:.3f} | p99 {pct(v, 0.99):.3f} | p99.9 {pct(v, 0.999):.3f} | max {v[-1]:.3f} ({vals[-1][1]}) |")
return vals
def regress(rows, col):
"""Least squares of col on the per-hash family counts (ordinary normal equations; 10 unknowns and an intercept)."""
xs, ys = [], []
for r in rows:
if r[col] in ("", "0.000"):
continue
# no intercept: every class v4 program has 48 non-load base and 256 shadow instructions, so the total
# executed count is the same for all and an intercept would be collinear with it
xs.append([ITER * int(r["base_" + f]) + ITER * REPS * int(r["shadow_" + f]) for f in FAMILIES])
ys.append(float(r[col]))
k = len(FAMILIES)
ata = [[0.0] * k for _ in range(k)]
aty = [0.0] * k
for x, y in zip(xs, ys):
for i in range(k):
aty[i] += x[i] * y
for j in range(k):
ata[i][j] += x[i] * x[j]
# Gauss-Jordan
m = [row[:] + [aty[i]] for i, row in enumerate(ata)]
for c in range(k):
piv = max(range(c, k), key=lambda r: abs(m[r][c]))
m[c], m[piv] = m[piv], m[c]
if abs(m[c][c]) < 1e-12:
print("regression: singular")
return None
d = m[c][c]
m[c] = [v / d for v in m[c]]
for r in range(k):
if r != c and m[r][c] != 0.0:
f = m[r][c]
m[r] = [a - f * b for a, b in zip(m[r], m[c])]
coef = [m[i][k] for i in range(k)]
pred = [sum(c * xi for c, xi in zip(coef, x)) for x in xs]
ybar = sum(ys) / len(ys)
ss_res = sum((y - p) ** 2 for y, p in zip(ys, pred))
ss_tot = sum((y - ybar) ** 2 for y in ys)
r2 = 1 - ss_res / ss_tot if ss_tot > 0 else float("nan")
print(f"regression of {col} on per-hash family counts, no intercept (n = {len(ys):,}): R^2 {r2:.3f}")
print("| Family | ms per 1,000 executed instrs (regression) | us per 1,000 |")
print("|---|---|---|")
for f, c in zip(FAMILIES, coef):
print(f"| {f} | {c * 1000:.4f} | {c * 1e6:.2f} |")
return dict(zip(FAMILIES, coef))
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--scan", nargs="+", required=True)
ap.add_argument("--weights", default="", help="add=us,sub=us,... per 1,000 instrs (from attack-f6 micro); default 1 each")
ap.add_argument("--column", default="cold_min_ms")
ap.add_argument("--top", type=int, default=50)
ap.add_argument("--out-prefix", default="")
ap.add_argument("--regress", action="store_true")
ap.add_argument("--label", default="scan")
a = ap.parse_args()
rows = read(a.scan)
print(f"{len(rows):,} programs read from {', '.join(a.scan)}")
w = {f: 1.0 for f in FAMILIES}
if a.weights:
for kv in a.weights.split(","):
k, v = kv.split("=")
w[k.strip()] = float(v)
for r in rows:
r["_proxy"] = proxy(r, w)
px = sorted(r["_proxy"] for r in rows)
print(f"| proxy (weighted instrs, n = {len(px):,}) | min {px[0]:.0f} | median {px[len(px) // 2]:.0f} | p99 {pct(px, 0.99):.0f} | p99.9 {pct(px, 0.999):.0f} | max {px[-1]:.0f} |")
vals = dist(rows, a.column, a.label)
if a.regress:
regress(rows, a.column)
by_proxy = sorted(rows, key=lambda r: -r["_proxy"])[: a.top]
by_time = sorted((r for r in rows if r[a.column] not in ("", "0.000")), key=lambda r: -float(r[a.column]))[: a.top]
pset = {r["seed"] for r in by_proxy}
tset = {r["seed"] for r in by_time}
print(f"worst {a.top} by proxy and worst {a.top} by {a.column}: {len(pset & tset)} seeds in both")
if vals:
# rank correlation (Spearman) between proxy and time over all rows
rp = {r["seed"]: i for i, r in enumerate(sorted(rows, key=lambda r: r["_proxy"]))}
rt = {s: i for i, (_, s) in enumerate(vals)}
common = [s for s in rt if s in rp]
n = len(common)
d2 = sum((rp[s] - rt[s]) ** 2 for s in common)
rho = 1 - 6 * d2 / (n * (n * n - 1)) if n > 2 else float("nan")
print(f"Spearman rank correlation proxy vs {a.column}: {rho:.3f} over {n:,}")
print(f"\nworst {a.top} by proxy (seed, proxy, {a.column}, shadow mix):")
for r in by_proxy[:10]:
print(f" {r['seed']} {r['_proxy']:.0f} {r[a.column]} " + " ".join(f"{f}={r['shadow_' + f]}" for f in FAMILIES))
print(f"worst {a.top} by {a.column}:")
for r in by_time[:10]:
print(f" {r['seed']} {r[a.column]} proxy {r['_proxy']:.0f} " + " ".join(f"{f}={r['shadow_' + f]}" for f in FAMILIES))
if a.out_prefix:
with open(a.out_prefix + "-proxy.txt", "w") as f:
f.write("\n".join(r["seed"] for r in by_proxy) + "\n")
with open(a.out_prefix + "-time.txt", "w") as f:
f.write("\n".join(r["seed"] for r in by_time) + "\n")
with open(a.out_prefix + "-union.txt", "w") as f:
seen = []
for r in by_proxy + by_time:
if r["seed"] not in seen:
seen.append(r["seed"])
f.write("\n".join(seen) + "\n")
print(f"wrote {a.out_prefix}-proxy.txt, -time.txt, -union.txt ({len(seen)} seeds)")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,488 @@
//! attack-f6: the verifier's worst case (attack pass row F6, docs/plans/cryptanalysis.md 4.2).
//!
//! The naive search (`igneum-pow bench` per seed) refills the 256 MiB cache for every program (370 ms on the
//! box core). This harness builds the day's dataset ONCE and swaps programs under it: `Epoch { program, dataset }`
//! is just the pair, and `verify::hash_warp(&program, base, &dataset)` takes both, so one `DatasetSource` serves
//! every program of the day.
//!
//! attack-f6 scan --count N [--start S] [--threads T] [--cold-reps R] [--flush swap|sweep|none] [--steady W] --out FILE
//! program i = string seed "attack-f6/<i>" through the class v4 chain draw (generator 4,
//! V4_CLASS, no era); one CSV line per program: op counts by family (base and shadow), the
//! timed cold warps, their min, the steady average. `--threads 1` is the sequential form
//! used under the exclusive measure hold.
//! attack-f6 time (--program-class v4 | --class dr736) (--seed S | --seeds a,b | --seeds-file F) [--reps R]
//! [--steady W] [--flush sweep] the deep re-time with the gate verdict per seed
//! attack-f6 micro [--reps R] per-family cost of a shadow instruction: the genesis program with its shadow
//! block rewritten to one family at a time (ranking weights only)
//! attack-f6 load [--seconds N] hash class v4 warps forever on the calling core (the SMT sibling's load for
//! the half-core proxy; 0 = until killed)
//!
//! "cold" here is one 32-lane warp timed after a flush: `sweep` writes a 256 MiB buffer first (what the cache
//! fill does before `bench`'s "single cold run"), `swap` hashes one warp of a different program first, `none`
//! times the warp as it comes. The gate is 10 ms per warp (spec 01 section 1.9).
use igneum_pow::generator::{self, Instr, LoadClass, Op, Program, ProgramClass, ITERATIONS};
use igneum_pow::memhard::{dataset_log2_words, Shape};
use igneum_pow::verify::{hash_warp, interpret_warp, DatasetMode, DatasetSource, DEFAULT_DATASET_LOG2};
use std::io::Write;
use std::sync::Mutex;
use std::time::Instant;
const GATE_MS: f64 = 10.0;
const DAY: &str = "2026-10-03";
const SEED_PREFIX: &str = "attack-f6/";
const SWEEP_BYTES: usize = 256 << 20;
/// Counted ops per shadow instruction (the 1.83 convention of `sim/horizon/algorithm/model.py`).
const COUNTED_PER_INSTR: f64 = 1.83;
const FAMILIES: [Op; 11] = [Op::Add, Op::Sub, Op::Mul, Op::MulHi, Op::Xor, Op::Or, Op::Rotl, Op::Rotr, Op::Mad, Op::Shfl, Op::Load];
#[derive(Clone, Copy)]
enum Target {
Program(ProgramClass),
Load(LoadClass),
}
impl Target {
fn load_class(&self) -> LoadClass {
match self {
Target::Program(pc) => pc.load_class(),
Target::Load(lc) => *lc,
}
}
fn name(&self) -> String {
match self {
Target::Program(pc) => format!("program class {}", pc.name()),
Target::Load(lc) => format!("load class {}", lc.name()),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Flush {
None,
Swap,
Sweep,
}
struct Args {
cmd: String,
target: Target,
seeds: Vec<String>,
count: usize,
start: usize,
threads: usize,
cold_reps: usize,
steady: usize,
flush: Flush,
out: Option<String>,
seconds: u64,
day: String,
}
fn usage() -> ! {
eprintln!("attack-f6 scan|time|micro|load [--program-class v4 | --class dr736] [--seed S | --seeds a,b | --seeds-file F] [--count N] [--start S] [--threads T] [--cold-reps R] [--steady W] [--flush none|swap|sweep] [--out FILE] [--seconds N] [--day {DAY}]");
std::process::exit(2)
}
fn parse() -> Args {
let mut a = Args {
cmd: String::new(),
target: Target::Program(ProgramClass::V4),
seeds: Vec::new(),
count: 0,
start: 0,
threads: 1,
cold_reps: 1,
steady: 0,
flush: Flush::Sweep,
out: None,
seconds: 0,
day: DAY.to_string(),
};
let mut it = std::env::args().skip(1);
a.cmd = it.next().unwrap_or_else(|| usage());
while let Some(k) = it.next() {
let mut val = || it.next().unwrap_or_else(|| usage());
match k.as_str() {
"--program-class" => a.target = Target::Program(ProgramClass::parse(&val()).unwrap_or_else(|| usage())),
"--class" => a.target = Target::Load(LoadClass::parse(&val()).unwrap_or_else(|| usage())),
"--seed" => a.seeds.push(val()),
"--seeds" => a.seeds.extend(val().split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty())),
"--seeds-file" => {
let text = std::fs::read_to_string(val()).unwrap_or_else(|e| {
eprintln!("seeds file: {e}");
std::process::exit(2)
});
a.seeds.extend(text.lines().map(|l| l.trim().to_string()).filter(|l| !l.is_empty() && !l.starts_with('#')));
}
"--count" => a.count = val().parse().unwrap_or_else(|_| usage()),
"--start" => a.start = val().parse().unwrap_or_else(|_| usage()),
"--threads" => a.threads = val().parse().unwrap_or_else(|_| usage()),
"--cold-reps" => a.cold_reps = val().parse().unwrap_or_else(|_| usage()),
"--steady" => a.steady = val().parse().unwrap_or_else(|_| usage()),
"--flush" => {
a.flush = match val().as_str() {
"none" => Flush::None,
"swap" => Flush::Swap,
"sweep" => Flush::Sweep,
_ => usage(),
}
}
"--out" => a.out = Some(val()),
"--seconds" => a.seconds = val().parse().unwrap_or_else(|_| usage()),
"--day" => a.day = val(),
_ => usage(),
}
}
a
}
/// The day's dataset for a target, built once: exactly what `igneum-pow`'s `epoch_of` builds for a string seed
/// on day 0 of the growth schedule (`Shape::for_class_day(class, 0)`, 2^28 words, memory-hard).
fn dataset_for(target: &Target, day: &str) -> DatasetSource {
let lc = target.load_class();
let shape = Shape::for_class_day(&lc, 0);
let log2 = if lc.growth { dataset_log2_words(DEFAULT_DATASET_LOG2, 0) } else { DEFAULT_DATASET_LOG2 };
DatasetSource::new_shape(day, DatasetMode::MemoryHard, log2, shape)
}
/// The program of a string seed under the target: the chain draw for a program class (`--program-class v4`:
/// generator 4 on V4_CLASS, no era, as `igneum-pow bench --program-class v4 --seed S`), `generate_class` for a
/// bare load class (`--class dr736`, as `igneum-pow bench --class dr736 --seed S`).
fn program_for(target: &Target, seed: &str) -> Program {
match target {
Target::Program(pc) => generator::generate_from_seed_bytes_program_class(seed, seed.as_bytes(), *pc, None),
Target::Load(lc) => generator::generate_class(seed, *lc),
}
}
fn counts(instrs: &[Instr]) -> [usize; 11] {
let mut c = [0usize; 11];
for i in instrs {
if let Some(k) = FAMILIES.iter().position(|&f| f == i.op) {
c[k] += 1;
}
}
c
}
fn family_header(prefix: &str) -> String {
FAMILIES.iter().map(|f| format!("{prefix}{}", f.name())).collect::<Vec<_>>().join(",")
}
struct Sweep(Vec<u8>);
impl Sweep {
fn new() -> Self {
Sweep(vec![0u8; SWEEP_BYTES])
}
/// Write the whole buffer (256 MiB, the cache fill's footprint) so the timed warp starts with the caches
/// holding none of the dataset, the cache or the program.
fn run(&mut self, salt: u8) -> u64 {
let buf = std::hint::black_box(&mut self.0);
for (i, b) in buf.iter_mut().enumerate() {
*b = (i as u8).wrapping_add(salt);
}
let mut s = 0u64;
for k in (0..buf.len()).step_by(1 << 20) {
s = s.wrapping_add(buf[k] as u64);
}
std::hint::black_box(s)
}
}
/// One timed warp of `program` after the chosen flush; returns ms and a sink word.
fn timed_warp(program: &Program, ds: &DatasetSource, base: u32, flush: Flush, other: &Program, sweep: &mut Option<Sweep>, salt: u8) -> (f64, u64) {
match flush {
Flush::None => {}
Flush::Swap => {
let w = hash_warp(other, base ^ 0x5a5a_0000, ds);
std::hint::black_box(w);
}
Flush::Sweep => {
if sweep.is_none() {
*sweep = Some(Sweep::new());
}
sweep.as_mut().unwrap().run(salt);
}
}
let t = Instant::now();
let w = hash_warp(program, base, ds);
let ms = t.elapsed().as_secs_f64() * 1e3;
(ms, std::hint::black_box(w)[0])
}
fn steady_ms(program: &Program, ds: &DatasetSource, warps: usize) -> (f64, u64) {
if warps == 0 {
return (0.0, 0);
}
let t = Instant::now();
let mut sink = 0u64;
for i in 0..warps {
sink ^= hash_warp(program, (i as u32) * 32 + 65536, ds)[0];
}
(t.elapsed().as_secs_f64() * 1e3 / warps as f64, sink)
}
fn fmt_ms(v: &[f64]) -> String {
v.iter().map(|x| format!("{x:.3}")).collect::<Vec<_>>().join(";")
}
fn median(v: &mut [f64]) -> f64 {
v.sort_by(|a, b| a.partial_cmp(b).unwrap());
let n = v.len();
if n == 0 {
0.0
} else if n % 2 == 1 {
v[n / 2]
} else {
(v[n / 2 - 1] + v[n / 2]) / 2.0
}
}
fn describe(target: &Target, ds: &DatasetSource, p: &Program) {
println!(
"attack-f6: {}, day {}, dataset 2^{} words, cache 2^{} words, mixer x{}, derive_len {}; genesis-shape program {} instrs x {} iterations, shadow {} instrs x {} reps ({} shadow instrs per hash, {:.0} counted ops at {COUNTED_PER_INSTR} per instr)",
target.name(),
DAY,
ds.log2_words,
ds.shape().cache_log2_words,
ds.shape().mixer_mult,
ds.shape().derive_len,
p.instrs.len(),
ITERATIONS,
p.shadow.len(),
p.shadow_reps(),
p.shadow_instrs_per_hash(),
p.shadow_instrs_per_hash() as f64 * COUNTED_PER_INSTR
);
}
fn scan(a: &Args) {
let out = a.out.clone().unwrap_or_else(|| usage());
if a.count == 0 {
usage();
}
let t0 = Instant::now();
let ds = dataset_for(&a.target, &a.day);
let fill_ms = t0.elapsed().as_secs_f64() * 1e3;
let other = program_for(&a.target, "igneum-genesis");
describe(&a.target, &ds, &other);
println!("dataset built once in {fill_ms:.1} ms; scanning {} programs from index {} on {} threads, cold reps {}, flush {}, steady {}", a.count, a.start, a.threads, a.cold_reps, flush_name(a.flush), a.steady);
let file = std::fs::File::create(&out).unwrap_or_else(|e| {
eprintln!("out: {e}");
std::process::exit(1)
});
let mut w = std::io::BufWriter::new(file);
// t_s: seconds since the scan started, read just before the first timed warp, so a row can be matched against the
// core sampler's log (which other process sat on core 40 or its sibling 88 at that second)
writeln!(w, "idx,seed,program_id,attempt,t_s,gen_ms,cold_ms,cold_min_ms,steady_ms,shadow_instrs,items_warp0,{},{}", family_header("base_"), family_header("shadow_")).unwrap();
let w = Mutex::new(w);
let done = Mutex::new(0usize);
let threads = a.threads.max(1);
std::thread::scope(|s| {
for t in 0..threads {
let ds = &ds;
let other = &other;
let w = &w;
let done = &done;
s.spawn(move || {
let mut sweep: Option<Sweep> = None;
let mut salt = t as u8;
let mut lines: Vec<String> = Vec::new();
let mut k = t;
while k < a.count {
let idx = a.start + k;
let seed = format!("{SEED_PREFIX}{idx}");
let tg = Instant::now();
let p = program_for(&a.target, &seed);
let gen_ms = tg.elapsed().as_secs_f64() * 1e3;
let t_s = t0.elapsed().as_secs_f64();
let mut cold = Vec::with_capacity(a.cold_reps);
for r in 0..a.cold_reps {
salt = salt.wrapping_add(1);
let (ms, _) = timed_warp(&p, ds, (r as u32) * 32, a.flush, other, &mut sweep, salt);
cold.push(ms);
}
let cold_min = cold.iter().cloned().fold(f64::INFINITY, f64::min);
let (st, _) = steady_ms(&p, ds, a.steady);
// distinct dataset items the warp at base 0 derives (the memory side of the cost; 4,096 at most)
let items = interpret_warp(&p, 0, ds).items_derived;
let bc = counts(&p.instrs);
let sc = counts(&p.shadow);
lines.push(format!(
"{idx},{seed},{:016x},{},{t_s:.1},{gen_ms:.3},{},{cold_min:.3},{st:.3},{},{items},{},{}",
p.program_id(),
p.attempt,
fmt_ms(&cold),
p.shadow_instrs_per_hash(),
bc.iter().map(|c| c.to_string()).collect::<Vec<_>>().join(","),
sc.iter().map(|c| c.to_string()).collect::<Vec<_>>().join(",")
));
if lines.len() >= 200 {
let mut g = w.lock().unwrap();
for l in &lines {
writeln!(g, "{l}").unwrap();
}
g.flush().unwrap();
lines.clear();
let mut d = done.lock().unwrap();
*d += 200;
if *d % 5000 == 0 {
eprintln!("scan: {} of {} after {:.0} s", *d, a.count, t0.elapsed().as_secs_f64());
}
}
k += threads;
}
let mut g = w.lock().unwrap();
for l in &lines {
writeln!(g, "{l}").unwrap();
}
g.flush().unwrap();
});
}
});
w.lock().unwrap().flush().unwrap();
println!("scan done: {} programs in {:.1} s, written to {out}", a.count, t0.elapsed().as_secs_f64());
}
fn flush_name(f: Flush) -> &'static str {
match f {
Flush::None => "none",
Flush::Swap => "swap",
Flush::Sweep => "sweep",
}
}
fn time(a: &Args) {
if a.seeds.is_empty() {
usage();
}
let reps = a.cold_reps.max(1);
let t0 = Instant::now();
let ds = dataset_for(&a.target, &a.day);
let fill_ms = t0.elapsed().as_secs_f64() * 1e3;
let other = program_for(&a.target, "igneum-genesis");
describe(&a.target, &ds, &other);
println!("dataset built once in {fill_ms:.1} ms; {} seeds, {reps} cold reps each (flush {}), steady {} warps; gate {GATE_MS} ms per warp", a.seeds.len(), flush_name(a.flush), a.steady);
let mut sweep: Option<Sweep> = None;
let mut worst: Option<(String, f64)> = None;
let mut salt = 0u8;
for seed in &a.seeds {
let t_s = t0.elapsed().as_secs_f64();
let p = program_for(&a.target, seed);
// the swap flush must hash a different program: for the genesis seed itself use a fixed other seed
let swap_other = if seed == "igneum-genesis" { program_for(&a.target, "attack-f6/0") } else { other.clone() };
let mut cold = Vec::with_capacity(reps);
let mut lane0 = 0u64;
for r in 0..reps {
salt = salt.wrapping_add(1);
let (ms, l0) = timed_warp(&p, &ds, (r as u32) * 32, a.flush, &swap_other, &mut sweep, salt);
cold.push(ms);
if r == 0 {
lane0 = l0;
}
}
let max = cold.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let min = cold.iter().cloned().fold(f64::INFINITY, f64::min);
let mut c2 = cold.clone();
let med = median(&mut c2);
let (st, _) = steady_ms(&p, &ds, a.steady);
let items = interpret_warp(&p, 0, &ds).items_derived;
let verdict = if max < GATE_MS { "PASS" } else { "FAIL" };
println!(
"t={t_s:.1}s seed {seed} id {:016x} attempt {} shadow_mix [{}] base_mix [{}] warp0 lane0 {lane0:016x} items {items}: cold max {max:.3} med {med:.3} min {min:.3} ms (reps {}), steady {st:.3} ms avg of {}: GATE {GATE_MS} ms {verdict}",
p.program_id(),
p.attempt,
p.shadow_op_mix(),
p.op_mix(),
fmt_ms(&cold),
a.steady
);
if worst.as_ref().map(|w| max > w.1).unwrap_or(true) {
worst = Some((seed.clone(), max));
}
}
let (ws, wm) = worst.unwrap();
println!("WORST: seed {ws} cold max {wm:.3} ms; GATE {GATE_MS} ms {}", if wm < GATE_MS { "PASS" } else { "FAIL" });
}
/// Per-family cost of one shadow instruction: the genesis class v4 program with its 256-instruction shadow block
/// rewritten to one family at a time (every other field of each instruction kept as drawn), steady warps, against
/// the same program with its shadow removed. Ranking weights, not a claim about any drawn program.
fn micro(a: &Args) {
let reps = a.cold_reps.max(3);
let steady = if a.steady == 0 { 10 } else { a.steady };
let ds = dataset_for(&a.target, &a.day);
let base = program_for(&a.target, "igneum-genesis");
describe(&a.target, &ds, &base);
let mut none = base.clone();
none.shadow.clear();
none.class.shadow = None;
let time_of = |p: &Program| -> f64 {
let mut best = f64::INFINITY;
for _ in 0..reps {
let (ms, _) = steady_ms(p, &ds, steady);
best = best.min(ms);
}
best
};
let t_none = time_of(&none);
let t_base = time_of(&base);
let n = base.shadow_instrs_per_hash() as f64;
// ms per instruction x 1e3 = us per instruction; x 1e3 again = us per 1,000 instructions (batch A of 7 October 2026
// printed this 1,000x too small)
println!("micro: no shadow {t_none:.3} ms per warp; drawn shadow {t_base:.3} ms ({:.2} us per 1,000 shadow instrs); best of {reps} x {steady} warps", (t_base - t_none) / n * 1e6);
println!("(a block of one family is degenerate for mul, mulhi and or: the registers collapse to 0 or all-ones, every lane then loads one item and the warp gets cheaper on the memory side; read those rows as data effects, not ALU cost)");
println!("| Family | ms per warp, shadow all this family | us per 1,000 shadow instrs | ratio to add |");
println!("|---|---|---|---|");
let mut add_cost = 0.0;
for &f in FAMILIES.iter().take(10) {
let mut p = base.clone();
for i in p.shadow.iter_mut() {
i.op = f;
}
let t = time_of(&p);
let per = (t - t_none) / n * 1e6;
if f == Op::Add {
add_cost = per;
}
println!("| {} | {t:.3} | {per:.3} | {:.2} |", f.name(), if add_cost > 0.0 { per / add_cost } else { 0.0 });
}
}
fn load(a: &Args) {
let ds = dataset_for(&a.target, &a.day);
let p = program_for(&a.target, "igneum-genesis");
describe(&a.target, &ds, &p);
println!("load: hashing class warps on this core for {} s (0 = until killed)", a.seconds);
let t0 = Instant::now();
let mut sink = 0u64;
let mut n = 0u64;
let mut last = 0u64;
loop {
sink ^= hash_warp(&p, (n as u32).wrapping_mul(32), &ds)[0];
n += 1;
let el = t0.elapsed().as_secs();
if el / 30 != last {
last = el / 30;
println!("load: {n} warps after {el} s (checksum {sink:016x})");
}
if a.seconds > 0 && el >= a.seconds {
break;
}
}
println!("load done: {n} warps in {:.1} s", t0.elapsed().as_secs_f64());
}
fn main() {
let a = parse();
match a.cmd.as_str() {
"scan" => scan(&a),
"time" => time(&a),
"micro" => micro(&a),
"load" => load(&a),
_ => usage(),
}
}

View file

@ -0,0 +1,86 @@
#!/usr/bin/env python3
"""attack-f6: read a phase 2 log (`attack-f6 time` output under `== solo` / `== half-core` headers) and the matching
clock log (`clock_start` of lib.sh: one line per second with the clock of cores 40 and 88 and every process whose last
CPU was 40 or 88), print one table per run (seed, cold max / median / min, steady, items, verdict, and whether a foreign
process sat on 40 or 88 during that seed's second), and the worst seeds per run.
timeparse.py --log phase2b.log --clock clock-2b.log [--top 10] [--grep worst-union]
"""
import argparse
import re
from datetime import datetime, timezone
LINE = re.compile(
r"t=(?P<t>[\d.]+)s seed (?P<seed>\S+) id (?P<id>[0-9a-f]+) attempt (?P<att>\d+) shadow_mix \[(?P<smix>[^\]]*)\] base_mix \[(?P<bmix>[^\]]*)\] "
r"warp0 lane0 (?P<lane0>[0-9a-f]+) items (?P<items>\d+): cold max (?P<max>[\d.]+) med (?P<med>[\d.]+) min (?P<min>[\d.]+) ms \(reps (?P<reps>[\d.;]+)\), "
r"steady (?P<steady>[\d.]+) ms avg of (?P<n>\d+): GATE [\d.]+ ms (?P<verdict>PASS|FAIL)"
)
HDR = re.compile(r"^== (?P<kind>solo core 40|half-core \(core 88 loaded, pid (?P<lp>\d+)\)): (?P<cmd>.*) at (?P<ts>\S+)$")
CLK = re.compile(r"^(?P<ts>\S+) t=(?P<t>[\d.]+) cpu40 (?P<c40>\d+) cpu88 (?P<c88>\d+) loadavg (?P<la>\S+ \S+ \S+) on40/88: (?P<procs>.*)$")
OWN = {"attack-f6", "bash", "sleep", "ps", "awk", "date", "cat", "cut", "bc", "tee", "flock", "nice", "taskset"}
def parse_ts(s):
return datetime.strptime(s, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc).timestamp()
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--log", required=True)
ap.add_argument("--clock", required=True)
ap.add_argument("--top", type=int, default=10)
ap.add_argument("--grep", default="", help="only runs whose command line contains this")
a = ap.parse_args()
clock = []
for line in open(a.clock):
m = CLK.match(line.strip())
if m:
procs = [p for p in m["procs"].split() if p]
foreign = [p for p in procs if p.split(":")[4] not in OWN and not p.split(":")[4].startswith("kworker") and p.split(":")[1] != "-"]
clock.append((parse_ts(m["ts"]), int(m["c40"]), int(m["c88"]), foreign, m["la"]))
runs = []
cur = None
for line in open(a.log):
line = line.rstrip("\n")
h = HDR.match(line)
if h:
cur = {"kind": "solo" if h["kind"].startswith("solo") else "half", "cmd": h["cmd"], "start": parse_ts(h["ts"]), "rows": []}
runs.append(cur)
continue
m = LINE.search(line)
if m and cur is not None:
cur["rows"].append(m.groupdict())
for r in runs:
if a.grep and a.grep not in r["cmd"]:
continue
rows = r["rows"]
if not rows:
continue
print(f"\n### {r['kind']}: {r['cmd']} ({len(rows)} seeds)")
# per-seed disturbance flag: any foreign process on 40/88 in the seconds the seed was timed
# (from t to the next seed's t, or 2 s), and the clock of core 40 in that window
for i, row in enumerate(rows):
t0 = r["start"] + float(row["t"])
t1 = r["start"] + (float(rows[i + 1]["t"]) if i + 1 < len(rows) else float(row["t"]) + 2.0)
win = [c for c in clock if t0 - 1.0 <= c[0] <= t1 + 1.0]
foreign = sorted({p.split(":")[4] + "@" + p.split(":")[0] for c in win for p in c[3]})
c40 = [c[1] for c in win]
row["_foreign"] = ",".join(foreign)
row["_c40min"] = min(c40) if c40 else 0
row["_c88max"] = max(c[2] for c in win) if win else 0
vals = sorted((float(x["max"]), x) for x in rows)
maxes = [v for v, _ in vals]
meds = sorted(float(x["med"]) for x in rows)
n = len(maxes)
print(f"over the run: cold-max min {maxes[0]:.3f} median {maxes[n // 2]:.3f} max {maxes[-1]:.3f}; cold-median min {meds[0]:.3f} median {meds[n // 2]:.3f} max {meds[-1]:.3f}; core 40 clock min {min(x['_c40min'] for x in rows)} kHz")
print(f"| Seed | items | cold max | median | min | steady | reps | foreign process on 40/88 in the window | verdict on max |")
print("|---|---|---|---|---|---|---|---|---|")
for v, x in list(reversed(vals))[: a.top]:
print(f"| {x['seed']} | {x['items']} | {x['max']} | {x['med']} | {x['min']} | {x['steady']} | {x['reps']} | {x['_foreign'] or 'none seen'} | {x['verdict']} |")
worst = vals[-1][1]
print(f"WORST by cold max: {worst['seed']} {worst['max']} ms ({worst['verdict']}); worst by cold median: {max(rows, key=lambda x: float(x['med']))['seed']} {max(float(x['med']) for x in rows):.3f} ms")
if __name__ == "__main__":
main()

14
tools/attack/f8-uniform/Cargo.lock generated Normal file
View file

@ -0,0 +1,14 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "attack-f8"
version = "0.1.0"
dependencies = [
"igneum-pow",
]
[[package]]
name = "igneum-pow"
version = "0.2.0"

View file

@ -0,0 +1,21 @@
[package]
name = "attack-f8"
version = "0.1.0"
edition = "2021"
description = "Attack-pass row F8: the uniformity censuses of the class v4 derivation (line index over 2^28 derivations, distinct lines per hash and warp, the cross-hash item histogram), with the plant hooks that prove the harness fires"
license = "MIT"
publish = false
[[bin]]
name = "attack-f8"
path = "src/main.rs"
[dependencies]
igneum-pow = { path = "../../../igneum-pow" }
[workspace]
[profile.release]
opt-level = 3
lto = true
codegen-units = 1

File diff suppressed because it is too large Load diff

14
tools/attack/f9-grind/Cargo.lock generated Normal file
View file

@ -0,0 +1,14 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "attack-f9"
version = "0.1.0"
dependencies = [
"igneum-pow",
]
[[package]]
name = "igneum-pow"
version = "0.2.0"

View file

@ -0,0 +1,20 @@
[package]
name = "attack-f9"
version = "0.1.0"
edition = "2021"
description = "Attack-pass row F9: acceptance edges (closed-form stand-in against the memory-hard dataset on generator 4), the hot-set search over passing programs, and the CPU side of header grinding (init-determined loads, search cost per locality hit, the per-warp init tables for the GPU run)"
publish = false
[[bin]]
name = "attack-f9"
path = "src/main.rs"
[dependencies]
igneum-pow = { path = "../../../igneum-pow" }
[workspace]
[profile.release]
opt-level = 3
lto = true
codegen-units = 1

View file

@ -0,0 +1,283 @@
// Attack-pass F9: the header-grinding rate measurement on one NVIDIA card. NOT a miner: no pool, no network, no wallet.
//
// Built against a memory-hard class v4 pack (program.h, vectors.h, kernel.cu, kernel_bound.cu) plus f9-variants.cu
// (make-variants.py). Fills the cache and the dataset with the pack's own kernels, checks both against vectors.h,
// checks the bound hash against igneum-pow's reference lines (--ref), checks the per-warp kernel reproduces the honest
// kernel on an all-equal table (known-pass) and that the forced kernels do not (known-fail), then times the variants
// in interleaved rounds. Every phase prints its wall-clock window in epoch milliseconds so the nvidia-smi power log can
// be attributed to it (join-power.py).
//
// f9-host --prehash <64 hex> --ref ref.txt --table-random f --table-k10 f --table-k14 f [--rounds 5] [--seconds 8]
// [--batch-log2 24] [--block-warps 1]
#include <cuda_runtime.h>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <chrono>
#include <cmath>
#include <fstream>
#include <string>
#include <vector>
#include "program.h"
#include "vectors.h"
#define CUDA_CHECK(call) do { cudaError_t err_ = (call); if (err_ != cudaSuccess) { \
std::fprintf(stderr, "CUDA error: %s (%d) at %s:%d in %s\n", cudaGetErrorString(err_), (int)err_, __FILE__, __LINE__, #call); \
std::exit(2); } } while (0)
struct IgneumInitWords { uint32_t w[8]; };
cudaError_t igneum_launch_hash_bound(const uint32_t* ds, uint64_t* out, uint32_t baseNonce, uint32_t mask, IgneumInitWords iw, uint32_t nonces, uint32_t blockWarps);
cudaError_t f9_launch_perwarp(const uint32_t* ds, uint64_t* out, uint32_t baseNonce, uint32_t mask, const IgneumInitWords* tbl, uint32_t nonces, uint32_t blockWarps);
cudaError_t f9_launch_forced4(const uint32_t* ds, uint64_t* out, uint32_t baseNonce, uint32_t mask, IgneumInitWords iw, uint32_t nonces, uint32_t blockWarps);
cudaError_t f9_launch_forcedall(const uint32_t* ds, uint64_t* out, uint32_t baseNonce, uint32_t mask, IgneumInitWords iw, uint32_t nonces, uint32_t blockWarps);
cudaError_t f9_launch_forced1(const uint32_t* ds, uint64_t* out, uint32_t baseNonce, uint32_t mask, IgneumInitWords iw, uint32_t nonces, uint32_t blockWarps);
cudaError_t f9_launch_pair(const uint32_t* ds, uint64_t* out, uint32_t baseNonce, uint32_t mask, IgneumInitWords iw, uint32_t nonces, uint32_t blockWarps);
static uint64_t fnv1a64(const void* p, size_t n) {
const uint8_t* b = (const uint8_t*)p;
uint64_t h = 0xcbf29ce484222325ull;
for (size_t i = 0; i < n; ++i) { h ^= b[i]; h *= 0x100000001b3ull; }
return h;
}
static void seedWordsFromBytes(const uint8_t* b, size_t n, uint32_t out[8]) {
for (uint64_t salt = 0; salt < 4; ++salt) {
uint64_t h = 0xcbf29ce484222325ull ^ (salt * 0x9E3779B97F4A7C15ull);
for (size_t i = 0; i < n; ++i) { h ^= b[i]; h *= 0x100000001b3ull; }
h ^= h >> 33; h *= 0xff51afd7ed558ccdull; h ^= h >> 33;
out[2 * salt] = (uint32_t)h;
out[2 * salt + 1] = (uint32_t)(h >> 32);
}
}
static IgneumInitWords blockInit(const uint8_t prehash[32], uint32_t nonceHi) {
uint8_t b[49];
std::memcpy(b, "igneum-block/", 13);
std::memcpy(b + 13, prehash, 32);
b[45] = (uint8_t)nonceHi; b[46] = (uint8_t)(nonceHi >> 8); b[47] = (uint8_t)(nonceHi >> 16); b[48] = (uint8_t)(nonceHi >> 24);
IgneumInitWords iw;
seedWordsFromBytes(b, 49, iw.w);
return iw;
}
static bool unhex(const std::string& s, std::vector<uint8_t>& out) {
if (s.size() % 2) return false;
out.clear();
for (size_t i = 0; i < s.size(); i += 2) out.push_back((uint8_t)std::strtoul(s.substr(i, 2).c_str(), nullptr, 16));
return true;
}
static double nowMs() { return std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now().time_since_epoch()).count(); }
static long long epochMs() { return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count(); }
static std::vector<IgneumInitWords> loadTable(const std::string& path, size_t warps) {
std::vector<IgneumInitWords> t(warps);
std::ifstream f(path, std::ios::binary);
if (!f) { std::fprintf(stderr, "cannot open table %s\n", path.c_str()); std::exit(2); }
f.read((char*)t.data(), (std::streamsize)(warps * sizeof(IgneumInitWords)));
if ((size_t)f.gcount() != warps * sizeof(IgneumInitWords)) { std::fprintf(stderr, "table %s short: %lld bytes\n", path.c_str(), (long long)f.gcount()); std::exit(2); }
return t;
}
enum Variant { HONEST, PERWARP_RANDOM, PERWARP_K10, PERWARP_K14, PAIR, FORCED1, FORCED4, FORCEDALL, NVARIANTS };
static const char* NAMES[NVARIANTS] = { "honest", "perwarp-random", "perwarp-k10", "perwarp-k14", "pair", "forced1", "forced4", "forcedall" };
int main(int argc, char** argv) {
std::string prehashHex(64, '0'), refPath, tRandom, tK10, tK14;
int rounds = 5, seconds = 8, batchLog2 = 24, blockWarps = 1;
for (int i = 1; i < argc; ++i) {
std::string a = argv[i];
auto next = [&]() -> std::string { if (i + 1 >= argc) { std::fprintf(stderr, "missing value for %s\n", a.c_str()); std::exit(2); } return argv[++i]; };
if (a == "--prehash") prehashHex = next();
else if (a == "--ref") refPath = next();
else if (a == "--table-random") tRandom = next();
else if (a == "--table-k10") tK10 = next();
else if (a == "--table-k14") tK14 = next();
else if (a == "--rounds") rounds = std::atoi(next().c_str());
else if (a == "--seconds") seconds = std::atoi(next().c_str());
else if (a == "--batch-log2") batchLog2 = std::atoi(next().c_str());
else if (a == "--block-warps") blockWarps = std::atoi(next().c_str());
else { std::fprintf(stderr, "unknown argument %s\n", a.c_str()); return 2; }
}
std::vector<uint8_t> ph;
if (!unhex(prehashHex, ph) || ph.size() != 32) { std::fprintf(stderr, "--prehash needs 64 hex\n"); return 2; }
cudaDeviceProp prop; std::memset(&prop, 0, sizeof prop);
CUDA_CHECK(cudaGetDeviceProperties(&prop, 0));
std::printf("f9-host: %s, %d SMs, %.0f MiB, pack %s class %s\n", prop.name, prop.multiProcessorCount, prop.totalGlobalMem / 1048576.0, IGNEUM_SEED_STRING, IGNEUM_PROGRAM_CLASS);
// 1. cache: filled on the GPU by the pack's kernel, checked against the pack's FNV, head and last line
const uint32_t cacheWords = 1u << IGNEUM_CACHE_LOG2_WORDS;
uint32_t* dCache = nullptr;
CUDA_CHECK(cudaMalloc((void**)&dCache, (size_t)cacheWords * 4));
double t0 = nowMs();
CUDA_CHECK(igneum_launch_cache_fill(dCache, IGNEUM_CACHE_SEGMENTS));
CUDA_CHECK(cudaDeviceSynchronize());
double fillMs = nowMs() - t0;
{
std::vector<uint32_t> h(cacheWords);
CUDA_CHECK(cudaMemcpy(h.data(), dCache, (size_t)cacheWords * 4, cudaMemcpyDeviceToHost));
uint64_t fnv = fnv1a64(h.data(), (size_t)cacheWords * 4);
bool ok = fnv == IGNEUM_CACHE_FNV64 && std::memcmp(h.data(), IGNEUM_CACHE_HEAD, 64) == 0 && std::memcmp(h.data() + cacheWords - 16, IGNEUM_CACHE_LAST, 64) == 0;
std::printf("check cache: %s (GPU fill %.0f ms, FNV %016llx vs pack %016llx)\n", ok ? "PASS" : "FAIL", fillMs, (unsigned long long)fnv, (unsigned long long)IGNEUM_CACHE_FNV64);
if (!ok) return 1;
}
// 2. dataset
const uint32_t words = 1u << IGNEUM_DATASET_LOG2;
const uint32_t mask = words - 1u;
uint32_t* dDs = nullptr;
CUDA_CHECK(cudaMalloc((void**)&dDs, (size_t)words * 4));
t0 = nowMs();
CUDA_CHECK(igneum_launch_build(dDs, dCache, words / 16u));
CUDA_CHECK(cudaDeviceSynchronize());
double buildMs = nowMs() - t0;
{
int bad = 0;
for (int k = 0; k < IGNEUM_DS_SAMPLES; ++k) {
uint32_t v = 0;
CUDA_CHECK(cudaMemcpy(&v, dDs + IGNEUM_DS_SAMPLE_INDEX[k], 4, cudaMemcpyDeviceToHost));
if (v != IGNEUM_DS_SAMPLE_VALUE[k]) ++bad;
}
uint32_t head[16]; CUDA_CHECK(cudaMemcpy(head, dDs, 64, cudaMemcpyDeviceToHost));
uint32_t last = 0; CUDA_CHECK(cudaMemcpy(&last, dDs + IGNEUM_DS_LAST_INDEX, 4, cudaMemcpyDeviceToHost));
bool ok = bad == 0 && std::memcmp(head, IGNEUM_DS_HEAD, 64) == 0 && last == IGNEUM_DS_LAST;
std::printf("check dataset: %s (GPU build %.0f ms, %d of %d samples wrong)\n", ok ? "PASS" : "FAIL", buildMs, bad, (int)IGNEUM_DS_SAMPLES);
if (!ok) return 1;
}
// 3. the pack's own vectors through the bench kernel (init words = seed words)
const uint32_t batch = 1u << batchLog2;
const size_t warps = batch / 32;
uint64_t* dOut = nullptr;
CUDA_CHECK(cudaMalloc((void**)&dOut, (size_t)batch * 8));
std::vector<uint64_t> hOut(batch);
{
int bad = 0;
for (int w = 0; w < IGNEUM_VEC_WARPS; ++w) {
CUDA_CHECK(igneum_launch_hash(dDs, dOut, IGNEUM_VEC_BASE[w], mask, 32u, 1u));
CUDA_CHECK(cudaDeviceSynchronize());
CUDA_CHECK(cudaMemcpy(hOut.data(), dOut, 32 * 8, cudaMemcpyDeviceToHost));
for (int l = 0; l < 32; ++l) if (hOut[l] != IGNEUM_VEC_OUT[w][l]) ++bad;
}
std::printf("check vectors: %s (%d of 96 lanes wrong)\n", bad == 0 ? "PASS" : "FAIL", bad);
if (bad) return 1;
}
// 4. the bound hash against igneum-pow's reference (nonce_hi 0, lane nonces 0..63)
IgneumInitWords iw0 = blockInit(ph.data(), 0u);
std::vector<uint64_t> honest64(64);
{
CUDA_CHECK(igneum_launch_hash_bound(dDs, dOut, 0u, mask, iw0, 64u, 1u));
CUDA_CHECK(cudaDeviceSynchronize());
CUDA_CHECK(cudaMemcpy(honest64.data(), dOut, 64 * 8, cudaMemcpyDeviceToHost));
int bad = 0, n = 0;
std::ifstream rf(refPath);
std::string line;
while (std::getline(rf, line)) {
if (line.empty() || line[0] == '#') continue;
unsigned long long nonce = 0, hash = 0;
if (std::sscanf(line.c_str(), "%llu %llx", &nonce, &hash) != 2 || nonce >= 64) continue;
++n;
if (honest64[nonce] != hash) ++bad;
}
std::printf("check bound vs igneum-pow: %s (%d reference lines, %d wrong; init %08x %08x ...)\n", (n == 64 && bad == 0) ? "PASS" : "FAIL", n, bad, iw0.w[0], iw0.w[1]);
if (n != 64 || bad) return 1;
}
// 5. the per-warp kernel on an all-equal table reproduces the honest kernel (known-pass); the random table's
// second warp differs (known-fail); the forced kernels differ (known-fail)
IgneumInitWords* dTbl = nullptr;
CUDA_CHECK(cudaMalloc((void**)&dTbl, warps * sizeof(IgneumInitWords)));
{
std::vector<IgneumInitWords> same(2, iw0);
CUDA_CHECK(cudaMemcpy(dTbl, same.data(), 2 * sizeof(IgneumInitWords), cudaMemcpyHostToDevice));
CUDA_CHECK(f9_launch_perwarp(dDs, dOut, 0u, mask, dTbl, 64u, 1u));
CUDA_CHECK(cudaDeviceSynchronize());
CUDA_CHECK(cudaMemcpy(hOut.data(), dOut, 64 * 8, cudaMemcpyDeviceToHost));
int diff = 0; for (int l = 0; l < 64; ++l) diff += hOut[l] != honest64[l];
std::printf("check perwarp all-equal table == honest: %s (%d of 64 differ)\n", diff == 0 ? "PASS" : "FAIL", diff);
if (diff) return 1;
std::vector<IgneumInitWords> two = { iw0, blockInit(ph.data(), 1u) };
CUDA_CHECK(cudaMemcpy(dTbl, two.data(), 2 * sizeof(IgneumInitWords), cudaMemcpyHostToDevice));
CUDA_CHECK(f9_launch_perwarp(dDs, dOut, 0u, mask, dTbl, 64u, 1u));
CUDA_CHECK(cudaDeviceSynchronize());
CUDA_CHECK(cudaMemcpy(hOut.data(), dOut, 64 * 8, cudaMemcpyDeviceToHost));
int d0 = 0, d1 = 0; for (int l = 0; l < 32; ++l) { d0 += hOut[l] != honest64[l]; d1 += hOut[32 + l] != honest64[32 + l]; }
std::printf("check perwarp two-init table: warp 0 equal (%d differ), warp 1 differs (%d of 32): %s (known-fail fires)\n", d0, d1, (d0 == 0 && d1 == 32) ? "PASS" : "FAIL");
if (d0 || d1 != 32) return 1;
CUDA_CHECK(f9_launch_forced4(dDs, dOut, 0u, mask, iw0, 64u, 1u));
CUDA_CHECK(cudaDeviceSynchronize());
CUDA_CHECK(cudaMemcpy(hOut.data(), dOut, 64 * 8, cudaMemcpyDeviceToHost));
int f4 = 0; for (int l = 0; l < 64; ++l) f4 += hOut[l] != honest64[l];
CUDA_CHECK(f9_launch_forcedall(dDs, dOut, 0u, mask, iw0, 64u, 1u));
CUDA_CHECK(cudaDeviceSynchronize());
CUDA_CHECK(cudaMemcpy(hOut.data(), dOut, 64 * 8, cudaMemcpyDeviceToHost));
int fa = 0; for (int l = 0; l < 64; ++l) fa += hOut[l] != honest64[l];
CUDA_CHECK(f9_launch_forced1(dDs, dOut, 0u, mask, iw0, 64u, 1u));
CUDA_CHECK(cudaDeviceSynchronize());
CUDA_CHECK(cudaMemcpy(hOut.data(), dOut, 64 * 8, cudaMemcpyDeviceToHost));
int f1 = 0; for (int l = 0; l < 64; ++l) f1 += hOut[l] != honest64[l];
CUDA_CHECK(f9_launch_pair(dDs, dOut, 0u, mask, iw0, 64u, 1u));
CUDA_CHECK(cudaDeviceSynchronize());
CUDA_CHECK(cudaMemcpy(hOut.data(), dOut, 64 * 8, cudaMemcpyDeviceToHost));
int pr = 0; for (int l = 0; l < 64; ++l) pr += hOut[l] != honest64[l];
std::printf("check forced kernels change the hash (known-fail fires): forced4 %d of 64 differ, forcedall %d, forced1 %d, pair %d: %s\n", f4, fa, f1, pr, (f4 >= 60 && fa >= 60 && f1 >= 60 && pr >= 2) ? "PASS" : "FAIL");
if (f4 < 60 || fa < 60 || f1 < 60 || pr < 2) return 1;
}
// tables
std::vector<IgneumInitWords> tblRandom = loadTable(tRandom, warps), tblK10 = loadTable(tK10, warps), tblK14 = loadTable(tK14, warps);
{
// the random table's warp 0 is nonce_hi 0: equal to iw0 by construction
bool ok = std::memcmp(&tblRandom[0], &iw0, sizeof iw0) == 0;
std::printf("check table-random warp 0 == init(nonce_hi 0): %s\n", ok ? "PASS" : "FAIL");
if (!ok) return 1;
}
std::printf("timed rounds: %d rounds x %d variants, %d s each, batch 2^%d nonces, %d warps per block\n", rounds, (int)NVARIANTS, seconds, batchLog2, blockWarps);
cudaEvent_t e0, e1;
CUDA_CHECK(cudaEventCreate(&e0));
CUDA_CHECK(cudaEventCreate(&e1));
std::vector<std::vector<double>> rate(NVARIANTS);
for (int r = 0; r < rounds; ++r) {
for (int v = 0; v < NVARIANTS; ++v) {
const std::vector<IgneumInitWords>* tbl = v == PERWARP_RANDOM ? &tblRandom : v == PERWARP_K10 ? &tblK10 : v == PERWARP_K14 ? &tblK14 : nullptr;
if (tbl) CUDA_CHECK(cudaMemcpy(dTbl, tbl->data(), warps * sizeof(IgneumInitWords), cudaMemcpyHostToDevice));
auto launch = [&](uint32_t nonceHi) -> cudaError_t {
IgneumInitWords iw = blockInit(ph.data(), nonceHi);
switch (v) {
case HONEST: return igneum_launch_hash_bound(dDs, dOut, 0u, mask, iw, batch, (uint32_t)blockWarps);
case PERWARP_RANDOM: case PERWARP_K10: case PERWARP_K14: return f9_launch_perwarp(dDs, dOut, 0u, mask, dTbl, batch, (uint32_t)blockWarps);
case FORCED4: return f9_launch_forced4(dDs, dOut, 0u, mask, iw, batch, (uint32_t)blockWarps);
case FORCED1: return f9_launch_forced1(dDs, dOut, 0u, mask, iw, batch, (uint32_t)blockWarps);
case PAIR: return f9_launch_pair(dDs, dOut, 0u, mask, iw, batch, (uint32_t)blockWarps);
default: return f9_launch_forcedall(dDs, dOut, 0u, mask, iw, batch, (uint32_t)blockWarps);
}
};
// warm-up
CUDA_CHECK(launch(0xffffffffu));
CUDA_CHECK(cudaDeviceSynchronize());
long long start = epochMs();
double wall0 = nowMs();
double gpuMs = 0;
uint64_t hashes = 0;
uint32_t d = 0;
while (nowMs() - wall0 < seconds * 1000.0) {
CUDA_CHECK(cudaEventRecord(e0));
CUDA_CHECK(launch(d));
CUDA_CHECK(cudaEventRecord(e1));
CUDA_CHECK(cudaEventSynchronize(e1));
float ms = 0; CUDA_CHECK(cudaEventElapsedTime(&ms, e0, e1));
gpuMs += ms;
hashes += batch;
++d;
}
long long end = epochMs();
double mhs = hashes / (gpuMs * 1e3);
rate[v].push_back(mhs);
std::printf("phase %s round %d start_ms %lld end_ms %lld dispatches %u hashes %llu gpu_ms %.1f MHs %.4f\n", NAMES[v], r, start, end, d, (unsigned long long)hashes, gpuMs, mhs);
std::fflush(stdout);
}
}
std::printf("summary (MH/s per variant over %d rounds: mean, sd, relative to honest)\n", rounds);
double hm = 0; for (double x : rate[HONEST]) hm += x; hm /= rate[HONEST].size();
for (int v = 0; v < NVARIANTS; ++v) {
double m = 0, s = 0; for (double x : rate[v]) m += x; m /= rate[v].size();
for (double x : rate[v]) s += (x - m) * (x - m); s = rate[v].size() > 1 ? std::sqrt(s / (rate[v].size() - 1)) : 0;
std::printf("variant %-15s mean %.4f sd %.4f rel %+.3f%%\n", NAMES[v], m, s, 100.0 * (m - hm) / hm);
}
cudaFree(dTbl); cudaFree(dOut); cudaFree(dDs); cudaFree(dCache);
return 0;
}

View file

@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""Attribute the 1-s nvidia-smi samples (epoch_ms power_w, sm_mhz, mem_mhz, temp_c, util) to the host log's phases."""
import sys
from collections import defaultdict
host, power = sys.argv[1], sys.argv[2]
phases = []
for line in open(host):
f = line.split()
if len(f) >= 14 and f[0] == "phase":
d = dict(zip(f[2::2], f[3::2]))
phases.append((f[1], int(d["round"]), int(d["start_ms"]), int(d["end_ms"]), float(d["MHs"])))
samples = []
for line in open(power):
f = line.replace(",", " ").split()
if len(f) < 6:
continue
try:
samples.append((int(f[0]), float(f[1]), float(f[2]), float(f[3]), float(f[4]), float(f[5])))
except ValueError:
pass
agg = defaultdict(list)
print("phase\tround\tsamples\tmean_W\tsm_MHz\tmem_MHz\ttemp_C\tutil\tMH/s\tMH/J")
for name, r, s, e, mhs in phases:
# drop the first second of each window (the sampler lags the launch)
rows = [x for x in samples if s + 1000 <= x[0] <= e]
if not rows:
print(f"{name}\t{r}\t0")
continue
w = sum(x[1] for x in rows) / len(rows)
sm = sum(x[2] for x in rows) / len(rows)
mem = sum(x[3] for x in rows) / len(rows)
t = sum(x[4] for x in rows) / len(rows)
u = sum(x[5] for x in rows) / len(rows)
agg[name].append((w, mhs))
print(f"{name}\t{r}\t{len(rows)}\t{w:.1f}\t{sm:.0f}\t{mem:.0f}\t{t:.0f}\t{u:.0f}\t{mhs:.3f}\t{mhs / w:.5f}")
print()
print("variant\trounds\tmean_W\tmean_MH/s\tMH/J\trel_rate_vs_honest\trel_MH/J_vs_honest")
base = None
for name in [p[0] for p in phases]:
if name in agg and name not in [b[0] for b in (base or [])]:
pass
order = []
for p in phases:
if p[0] not in order:
order.append(p[0])
ref = None
for name in order:
rows = agg[name]
w = sum(x[0] for x in rows) / len(rows)
m = sum(x[1] for x in rows) / len(rows)
eff = m / w
if ref is None:
ref = (m, eff)
print(f"{name}\t{len(rows)}\t{w:.1f}\t{m:.3f}\t{eff:.5f}\t{100 * (m - ref[0]) / ref[0]:+.3f}%\t{100 * (eff - ref[1]) / ref[1]:+.3f}%")

View file

@ -0,0 +1,86 @@
#!/usr/bin/env python3
"""Attack-pass F9: the grinding and forced-locality variants of a pack's kernel_bound.cu, as a second translation unit.
Reads the pack's kernel_bound.cu (never edited in place) and writes f9-variants.cu with three kernels copied from
igneum_hash_bound and changed only where stated:
f9_hash_perwarp the init words come from a per-warp table (tbl[gid >> 5]) instead of the kernel argument:
the grinding miner's shape (one nonce_hi per warp); also run with an unsearched table as the control
f9_hash_forced4 the loads named by --force (the iteration-0 sites whose address depends on the init words and the
nonce only) read lane 0's address in every lane (the index broadcast by __shfl_sync): the ceiling
of any locality a grind could reach on those sites; its hashes are wrong on purpose
f9_hash_forcedall every load reads lane 0's address: the harness-firing case (DRAM traffic divided by 32)
f9_hash_forced1 only the first forced site reads lane 0's address in every lane (31 lines saved per warp)
f9_hash_pair at the first forced site lane 1 reads lane 0's address (1 line saved per warp of 4,096: the
grind's own order of magnitude, placed deterministically)
Usage: make-variants.py <pack>/kernel_bound.cu f9-variants.cu --force 7,8,9,10,31
"""
import re
import sys
src_path, out_path = sys.argv[1], sys.argv[2]
force = set()
if "--force" in sys.argv:
force = {int(x) for x in sys.argv[sys.argv.index("--force") + 1].split(",")}
text = open(src_path).read()
head, rest = text.split("__global__ void igneum_hash_bound(", 1)
sig, body_and_tail = rest.split(") {", 1)
body, tail = body_and_tail.split("\ncudaError_t igneum_launch_hash_bound(", 1)
assert body.rstrip().endswith("}"), "kernel body end not found"
load_re = re.compile(r"^(\s*r\d = r\d \^ )ds\[(.*) & mask\];(\s*// (\d+) load)$")
def forced(body, which, pair=False):
out = []
n = 0
for line in body.split("\n"):
m = load_re.match(line)
if m and (which is None or int(m.group(4)) in which):
if pair:
line = (f"{m.group(1).rstrip()} 0; {{ uint32_t a_ = ({m.group(2)}) & mask; uint32_t b_ = __shfl_sync(0xffffffffu, a_, 0); "
f"{m.group(1).strip()}ds[((threadIdx.x & 31u) == 1u) ? b_ : a_]; }}{m.group(3)} PAIR")
else:
line = f"{m.group(1)}ds[__shfl_sync(0xffffffffu, ({m.group(2)}) & mask, 0)];{m.group(3)} FORCED"
n += 1
out.append(line)
return "\n".join(out), n
perwarp_body = body.replace("uint32_t gid = blockIdx.x * blockDim.x + threadIdx.x;",
"uint32_t gid = blockIdx.x * blockDim.x + threadIdx.x;\n IgneumInitWords iw = tbl[gid >> 5];", 1)
assert "tbl[gid >> 5]" in perwarp_body, "gid line not found"
sig_tbl = sig.replace("IgneumInitWords iw", "const IgneumInitWords* __restrict__ tbl")
assert sig_tbl != sig
f4_body, n4 = forced(body, force)
fa_body, na = forced(body, None)
first = {min(force)}
f1_body, n1 = forced(body, first)
pr_body, npr = forced(body, first, pair=True)
assert n1 == 1 and npr == 1
assert n4 == len(force), f"forced {n4} of {len(force)} sites"
assert na == 16, f"forced {na} of 16 load slots"
launcher = """
cudaError_t f9_launch_{name}(const uint32_t* ds, uint64_t* out, uint32_t baseNonce, uint32_t mask,
{argtype} iw, uint32_t nonces, uint32_t blockWarps) {{
if (blockWarps == 0u || blockWarps > 32u) return cudaErrorInvalidValue;
uint32_t block = 32u * blockWarps;
if (nonces == 0u || (nonces % block) != 0u) return cudaErrorInvalidValue;
f9_hash_{name}<<<nonces / block, block>>>(ds, out, baseNonce, mask, iw);
return cudaGetLastError();
}}
"""
with open(out_path, "w") as f:
f.write("// Generated by tools/attack/f9-grind/pod/make-variants.py from " + src_path + ". Attack-pass F9 variants; not a miner.\n")
f.write(head)
f.write("__global__ void f9_hash_perwarp(" + sig_tbl + ") {" + perwarp_body + "\n")
f.write("__global__ void f9_hash_forced4(" + sig + ") {" + f4_body + "\n")
f.write("__global__ void f9_hash_forcedall(" + sig + ") {" + fa_body + "\n")
f.write("__global__ void f9_hash_forced1(" + sig + ") {" + f1_body + "\n")
f.write("__global__ void f9_hash_pair(" + sig + ") {" + pr_body + "\n")
f.write(launcher.format(name="perwarp", argtype="const IgneumInitWords*"))
f.write(launcher.format(name="forced4", argtype="IgneumInitWords"))
f.write(launcher.format(name="forcedall", argtype="IgneumInitWords"))
f.write(launcher.format(name="forced1", argtype="IgneumInitWords"))
f.write(launcher.format(name="pair", argtype="IgneumInitWords"))
print(f"wrote {out_path}: perwarp (table init), forced4 ({n4} sites {sorted(force)}), forcedall ({na} load slots), forced1 and pair (site {min(force)})")

View file

@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Attack-pass F9 pod job: build the pack kernels and the F9 variants on the pod, run the checks and the timed rounds
# with nvidia-smi 1-s power samples, write everything to the log directory. Run on the pod from the job directory that
# holds: pack/ (the pack files), f9-host.cu, f9-variants.cu (generated here), make-variants.py, join-power.py,
# ref.txt, table-random.bin, table-k10.bin, table-k14.bin.
# bash run.sh <rounds> <seconds> [arch]
set -euo pipefail
cd "$(dirname "$0")"
ROUNDS="${1:-5}"; SECS="${2:-8}"; ARCH="${3:-sm_120}"; BATCH="${4:-24}"
export PATH=/usr/local/cuda/bin:$PATH
LOG=log; mkdir -p "$LOG"
echo "f9 pod job start $(date -u +%Y-%m-%dT%H:%M:%SZ) rounds $ROUNDS seconds $SECS arch $ARCH" | tee "$LOG/run.log"
nvidia-smi --query-gpu=name,driver_version,power.limit,memory.total --format=csv | tee -a "$LOG/run.log"
python3 make-variants.py pack/kernel_bound.cu f9-variants.cu --force 7,8,9,10,31 | tee -a "$LOG/run.log"
sha256sum pack/kernel_bound.cu pack/kernel.cu pack/program.h pack/vectors.h f9-variants.cu f9-host.cu table-*.bin ref.txt | tee "$LOG/sha256.txt"
t0=$(date +%s)
nvcc -O3 -std=c++17 -arch="$ARCH" -I pack -o f9-host f9-host.cu pack/kernel.cu pack/kernel_bound.cu f9-variants.cu 2>&1 | tee "$LOG/nvcc.log"
echo "nvcc done in $(( $(date +%s) - t0 )) s" | tee -a "$LOG/run.log"
# power sampler: epoch ms prefixed by the shell, one line a second
# the sampler runs in its own session (setsid) so the whole group can be killed and no orphan holds the ssh open
setsid bash -c 'stdbuf -oL nvidia-smi --query-gpu=power.draw,clocks.sm,clocks.mem,temperature.gpu,utilization.gpu --format=csv,noheader,nounits -lms 1000 | while IFS= read -r l; do echo "$(date +%s%3N) $l"; done' > "$LOG/power.csv" 2>&1 < /dev/null &
PW=$!
sleep 2
set +e
./f9-host --prehash "$(cat prehash.txt)" --ref ref.txt --table-random table-random.bin --table-k10 table-k10.bin --table-k14 table-k14.bin --rounds "$ROUNDS" --seconds "$SECS" --batch-log2 "$BATCH" 2>&1 | tee "$LOG/host.log"
rc=${PIPESTATUS[0]}
set -e
sleep 2
kill -- -$PW 2>/dev/null || kill $PW 2>/dev/null || true; pkill -x nvidia-smi 2>/dev/null || true
python3 join-power.py "$LOG/host.log" "$LOG/power.csv" | tee "$LOG/power-by-variant.txt"
echo "f9 pod job end $(date -u +%Y-%m-%dT%H:%M:%SZ) host rc $rc" | tee -a "$LOG/run.log"
exit $rc

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""Summaries of the F9 census outputs. summarise.py edges <part.tsv...> | hotset <part.tsv...>"""
import sys
from collections import Counter, defaultdict
MIN_DISTINCT = 245760
MAX_SAT = 164
BIAS = 136
def edges(paths):
cand = 0; agree = 0; dis = []; seeds = 0; chosen_differs = 0; cf_exh = 0; mh_exh = 0
first_fail = Counter(); rejected_cf = 0; rejected_mh = 0
# margins of accepted candidates on each stand-in: min distinct, max sat, max bias, max near_const
marg = {"cf": [0, 0, 0, 10**9], "mh": [0, 0, 0, 10**9]} # max sat, max bias, max near_const, min distinct
diff_stats = defaultdict(list)
for p in paths:
for line in open(p):
if line.startswith("#"):
continue
f = line.rstrip("\n").split("\t")
if f[0] == "S":
seeds += 1
a, b = int(f[2]), int(f[3])
if a < 0: cf_exh += 1
if b < 0: mh_exh += 1
if a != b: chosen_differs += 1
continue
cand += 1
cf, mh = f[2:10], f[10:18]
if cf[1] == "static":
first_fail["static"] += 1; rejected_cf += 1; rejected_mh += 1; agree += 1
continue
if cf[0] == "R": rejected_cf += 1; first_fail["cf:" + cf[1]] += 1
if mh[0] == "R": rejected_mh += 1; first_fail["mh:" + mh[1]] += 1
for name, m in (("cf", cf), ("mh", mh)):
if m[0] == "A":
s = marg[name]
s[0] = max(s[0], int(m[5])); s[1] = max(s[1], int(m[6])); s[2] = max(s[2], int(m[3])); s[3] = min(s[3], int(m[7]))
if f[18] == "1":
agree += 1
else:
dis.append((int(f[0]), int(f[1]), cf, mh))
print(f"seeds {seeds}, candidates {cand}, agree {agree}, disagree {len(dis)}, seeds whose chosen attempt differs {chosen_differs}, exhausted cf {cf_exh} mh {mh_exh}")
print(f"rejected: closed-form {rejected_cf}, memory-hard {rejected_mh}; first failing condition counts: {dict(first_fail)}")
for name in ("cf", "mh"):
s = marg[name]
print(f"accepted margins on {name}: max saturated {s[0]} (limit {MAX_SAT}), max bias {s[1]} (limit {BIAS}), max near-constant |ones-1024| {s[2]} (1024 = constant), min distinct sum {s[3]} (bound {MIN_DISTINCT}, mean {s[3]/2048:.3f})")
print("disagreements (seed, attempt, side that accepts, condition, closed-form metrics, memory-hard metrics):")
kinds = Counter()
for seed, att, cf, mh in dis:
side = "closed-form accepts" if cf[0] == "A" else "memory-hard accepts"
cond = mh[1] if cf[0] == "A" else cf[1]
kinds[(side, cond)] += 1
print(f" {seed}\t{att}\t{side}\t{cond}\tcf: cb={cf[2]} nc={cf[3]} lc={cf[4]} sat={cf[5]} bias={cf[6]} dist={cf[7]}\tmh: cb={mh[2]} nc={mh[3]} lc={mh[4]} sat={mh[5]} bias={mh[6]} dist={mh[7]}")
print("by kind:", dict(kinds))
def hotset(paths):
n = 0; prog = set(); any_hot = Counter(); flagged = Counter(); worst = {}
taint = Counter(); taint_total_max = 0
share_hist = Counter(); const_max = 0; skew_max = 0; mind_min = 10**9; top_max = 0
hot_programs = set(); flag_programs = set()
worst_share = (0.0, None)
mind_hist = Counter(); top_hist = Counter()
for p in paths:
for line in open(p):
if line.startswith("#"):
continue
f = line.rstrip("\n").split("\t")
if len(f) < 20:
continue
seed, attempt, init = int(f[0]), int(f[1]), f[2]
n += 1; prog.add(seed)
t0, tt = int(f[3]), int(f[4])
if init == "seed":
taint[t0] += 1; taint_total_max = max(taint_total_max, tt)
cb, sk, mind, top = int(f[5]), int(f[6]), int(f[7]), int(f[8])
const_max = max(const_max, cb); skew_max = max(skew_max, sk); mind_min = min(mind_min, mind); top_max = max(top_max, top)
share = float(f[18]); anyh = f[19] == "hot"; flag = f[20] == "FLAG"
if init == "seed":
mind_hist[min(mind // 8 * 8, 2048)] += 1
top_hist[1 if top <= 1 else 2 if top <= 2 else 4 if top <= 4 else 8 if top <= 8 else 16 if top <= 16 else 64 if top <= 64 else 256 if top <= 256 else 2048] += 1
if anyh: any_hot[init] += 1; hot_programs.add(seed)
if flag: flagged[init] += 1; flag_programs.add(seed)
b = "0" if share == 0 else ("<0.1%" if share < 0.001 else "<0.5%" if share < 0.005 else "<1%" if share < 0.01 else ">=1%")
share_hist[(init, b)] += 1
if share > worst_share[0]: worst_share = (share, (seed, attempt, init, line.strip()))
print(f"rows {n}, programs {len(prog)}; programs with any hot bucket (strict) {len(hot_programs)} (rows by init {dict(any_hot)}); programs flagged at the gate reading (hot share >= 1% or 7 constant bits) {len(flag_programs)} (rows {dict(flagged)})")
print(f"max site constant bits {const_max}, max skewed bits {skew_max}, min site distinct {mind_min}, max site top-address count {top_max}")
print("hot share histogram (init, band) -> rows:", dict(sorted(share_hist.items())))
print("worst hot share:", worst_share)
print("init-determined loads in iteration 0 (count -> programs):", dict(sorted(taint.items())), "max total", taint_total_max)
print("min site distinct (seed init), floor-8 bins -> programs:", dict(sorted(mind_hist.items())))
print("max site top-address count (seed init), upper bin -> programs:", dict(sorted(top_hist.items())))
share_sum = 0.0; share_n = 0
for p in paths:
for line in open(p):
if line.startswith("#"): continue
f = line.rstrip("\n").split("\t")
if len(f) >= 20 and f[2] == "seed":
share_sum += float(f[18]); share_n += 1
print(f"mean hot share over programs (seed init): {share_sum / max(share_n, 1):.6f}")
if __name__ == "__main__":
{"edges": edges, "hotset": hotset}[sys.argv[1]](sys.argv[2:])