Registry batch: FIN-08 through the finality-sim cell (RUNNING, the real-node rows joined to the simulator's; the combined-boundary runs are tomorrow's plan), so sim/results_v2.md and its rows move together

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-08 19:35:40 +00:00
commit 1cb3e890b7
47 changed files with 31766 additions and 282 deletions

View file

@ -0,0 +1,280 @@
# AMD and Intel energy on class v6: why the measured cards pay 3x to 6x a Blackwell card per hash, and what a kernel can change
8 October 2026, written 19:4x to 20:3x UK, the AMD-and-Intel energy lane. Register rows: GPU-03 (the 64-register
window's real cost) in its AMD and Intel cells; ECO-05's named cause (`docs/analysis/class-v6/eco-05-results.md` on
counter-asic-4: "the measured RX 9070 XT (7.9 microjoules at its knee), the RX 7600 (6.2 estimated, 8.1 measured at stock)
and the Arc B580 (10.4, watts estimated) cost 4x to 6x a Blackwell card per joule"); the P02 cohort's AMD and Intel cells.
The one acceptance rule is P03: a change must cost the honest card no more than 5 percent per accepted work against the
paired baseline (and no more than 2 percent of accepted throughput) and must not help the adversary.
**Labels.** Every number carries one: **measured** (an instrument read it; the instrument is named), **modelled**
(arithmetic on labelled inputs, or an offline compiler's report standing in for the driver's), **estimated** (no
instrument behind it), **team-reported** (read by a person, not a job). Vendor and JEDEC figures are marked
approximate. No row here is a wall reading: neither PC has a wall meter, so every watt is the device's own telemetry
(ADLX `GPUPower` on AMD, `nvidia-smi power.draw` on NVIDIA, the Level Zero sysman energy counter on Intel, IOReport on
Apple), and none meets P02's calibrated 2 percent wall meter. **Efficiency kind** (review B's rule, main's order 8 October
2026): every efficiency row says which of two things it is, and the two are never mixed: **KT**, kernel throughput (the
worker's `--bench-pack` or `--memprobe`, device event time, no pool, no shares) against device-reported watts; **SV**, the
serving hash rate (the installed app's own `hash_now` for the card while it mines) against device-reported watts. No row
here is the third kind, **end-to-end accepted work per wall joule in serving mode**: that needs accepted shares and a wall
meter, and is owed (section 4.3). Nothing was built or run on the Mac: the offline compiles
ran on igneum-build-1 (LLVM 18.1.3), the jobs on PC 1 and PC 2 through the signed jobs queue.
## 0. The answer in one paragraph
On this hash every card runs at 87 to 100 percent of its own dependent random-read ceiling divided by 128 (the unit of
work is 128 dependent random 4-byte reads), so joules per hash = 128 x watts / random reads per second. Normalised per
32 bits of memory bus, the AMD and Intel cards burn about what the RTX 5090 burns (18 to 19 W per 32 bits at their
knees; the RX 7600 28 W at stock) but complete 0.21x to 0.47x its random reads per second (the 5090 1.08 G/s per 32
bits; the 9070 XT 0.30; the 7600 0.44 to 0.51; the B580 0.23). That ratio is the whole gap: 3.4x for the 9070 XT, 3.1x
to 3.5x for the 7600, 4.4x for the B580 against the 5090's floor, and 3.8x to 6.1x against the 5080 and the 5070 Ti,
which is ECO-05's "4x to 6x". It sits in the memory system's random-access rate (GDDR7's four channels per device
against GDDR6's two, and a per-channel rate that varies 2.2x between the three GDDR6 cards), not in the kernel: the
ALU work is about 5 percent of an AMD card's joules, occupancy is 3x to 6x above what saturates the DRAM, the
64-register window costs the RX 7600 and the Arc B580 no rate per unit of work (0 and -0.3 percent, measured), and a 16-byte read costs what a 4-byte read costs on every
card measured. The kernel changes that keep every hash identical are worth 0 to about 10 percent on AMD, ranked in
section 3; none closes the gap, and the one hash change that does (64-byte reads) halves the 5090 and fails P03.
The levers that move ECO-05 tonight are the AMD operating point below the driver's floor (a baseline, not a candidate)
and the RX 7600's metered knee (a PC 1 job queued at landing; section 4), which ECO-05 says adds sustainable worlds
at 0.03 and 0.10 if it reads under 5 microjoules (modelled tonight at 5.0 to 5.4).
## 1. The measured rows as they stand, with their instruments
### 1.1 Rate and watts
| Card | Class / point | MH/s (rate instrument) | Watts (watts instrument) | Microjoules per hash | Label | Kind | Source |
|---|---|---|---|---|---|---|---|
| RX 9070 XT (gfx1201, 16 GB GDDR6, 256-bit, PC 1 eGPU) | class v2, stock, app mining | 17.73 (the app's hash_now, mean of 24) | 198.9 (ADLX GPUPower, 12 samples, 193 to 212) | 11.2 | measured | SV | docs/bench-log.md, 5 Oct, job tele-measure-1 |
| RX 9070 XT | class v5, stock (grid point 0/0) | 18.9 (the app's hash_now, median over a 75 s hold) | 202 (ADLX GPUPower, mean after a 30 s settle) | 10.7 | measured | SV | the ADLX grid, 8 Oct 12:13, `relay/playbooks/ca3-pc1-amd-grid.ps1`; floor/denominator.md |
| RX 9070 XT | class v5, gmax -500 MHz, plimit -30 percent (the driver's floor; the grid's best of 24) | 18.9 to 19.0 (same) | 149.3 (same) | 7.9 | measured | KT | same |
| RX 7600 (gfx1102, 8 GB GDDR6, 128-bit, PC 1) | class v4 sub-version 3, stock | 13.88 (card-in job, worker bench) | 113 (ADLX, card-in job) | 8.14 | measured | KT | floor/denominator.md, 15:50 UK |
| RX 7600 | class v6 hl-v6-foldrw (the partner), stock, quiet | 15.79 (worker --bench-pack) | none | | measured rate only | KT | hash lane, 18:3x UK |
| RX 7600 | class v6 hl-v6-all (window + fold + rw; 256 loads per hash = two units of work), stock, quiet | 7.92 = **15.84 per unit of work** | none | | measured rate only | KT | hash lane, 18:3x UK |
| RX 7600 | class v6 sizes 1 / 2 / 4 / 5.5 GiB | 15.75 / 15.46 / 15.34 / 15.35 | none | | measured rate only | KT | reference-population.md section 6 |
| RX 7600 | class v6 at stock with the class v4 run's 113 W | 15.79 | 113 (a different class's reading) | **7.2** | modelled | KT (modelled) | this file |
| RX 7600 | knee | 13.9 | 86 (the 9070 XT's 24 percent applied) | 6.19 | estimated | KT (modelled) | reference-population.md |
| Arc B580 (12 GB GDDR6, 192-bit, PC 2 eGPU and a PC 1 slot) | class v4, stock | 10.6 to 11.0 (worker bench) | about 110 (the board's class) | 10.4 | measured rate, estimated watts | KT | floor/denominator.md; the Intel lane, 7 Oct |
| Arc B580 (PC 2, enclosure) | class v6 hl-v6-foldrw, stock, quiet (SIMD32, sub-group shuffle exchange) | 11.008 (worker --bench-pack, 60 dispatches of 2^24, device time) | not read: the Level Zero energy counter answered ZE_RESULT_ERROR_UNSUPPORTED_FEATURE (0x78000003) on all three power domains, unelevated, driver 32.0.101.6733 | | measured rate only | KT | job run-ae-pc2-b580-energy-20261008, 18:52Z |
| Arc B580 | class v6 hl-v6-all, stock, quiet (the compiler drops to SIMD16: sub-group 16, local-memory exchange with barriers) | 5.489 = **10.98 per unit of work** | not read (same) | | measured rate only | KT | same, 18:54Z |
| Arc B580 | class v6 at stock with the board-class watts | 11.0 | about 110 | 10.0 | measured rate, estimated watts | KT | this file |
| RTX 5090 (32 GB GDDR7, 512-bit, PC 1) | class v5, the 1,300 MHz lock | 134.8 (worker) | 314 (nvidia-smi; 2.33 x 134.8) | 2.33 | measured | KT | reference-population.md (PC 1, tiers file) |
| RTX 5080 (16 GB GDDR7, 256-bit) | class v5, 1,100 MHz | 71.2 | 146.6 | 2.06 | measured (rented) | KT | reference-population.md |
| RTX 5070 Ti (16 GB GDDR7, 256-bit) | class v5 knee | 77.0 | 131 | 1.70 | stock measured (rented), knee modelled | KT | reference-population.md |
| Apple M5 Max (36 GB LPDDR5X) | class v4 | 26.67 | 37.3 (IOReport GPU and DRAM channels) | 1.40 | measured | KT | floor/denominator.md |
Tonight's jobs (section 4.1) added the B580's class v6 rows; its watts could not be read unelevated. The RX 7600's metered
class v6 rows (ADLX at stock and two knob points) are owed to a PC 1 job still queued at landing.
### 1.2 The random-read ceiling (the instrument that explains the rows)
`igneum-worker-opencl --memprobe` (proto-opencl/host.c): dependent random 4-byte loads over a buffer, device event
time, best over lanes in flight; `chase` is one dependent load per step per lane.
| Card | Ceiling at 1024 MiB, G loads/s | Hash-implied (MH/s x 128) | Hash / ceiling | 64-byte read against 4-byte | Label | Source |
|---|---|---|---|---|---|---|
| RTX 5090 | 17.5 to 18.2 (card off in the app, CUDA) | 17.25 | 0.96 | 0.52x to 0.86x (two 32-byte sectors; bandwidth-bound at 584 GB/s) | measured | bench-log, read-width entry, 5 Oct |
| RX 9070 XT | 2.42 to 2.66 (4,096 lanes already saturate it; eight independent chains per lane give the same) | 2.42 | 0.87 to 0.95 | 1.0x (2.47 to 2.87: the line is fetched either way) | measured | bench-log, 5 Oct |
| RX 7600 | owed (the PC 1 job of section 4.1) | 2.02 (class v6) | | | hash-implied, modelled | |
| Arc B580 | 1.407 to 1.409 (4,096 lanes and up; eight independent chains per lane 1.41, the same); at 256 MiB 1.55 to 1.57 | 1.409 (class v6) | 1.00 | not probed | measured | job run-ae-pc2-b580-energy-20261008 (tonight); the Intel lane 7 Oct read 1.41 |
| Apple M5 Max | 3.50 | 3.47 | 1.0 | 1.0x | measured | bench-log, 5 Oct |
## 2. The decomposition
### 2.1 The identity
The unit of work is 128 dependent random 4-byte dataset reads (program.json of both class v6 packs:
`loads_per_hash` 128, `load_width_counts_4_16_64` [16, 0, 0], `bytes_per_hash` 512; hl-v6-all does two units per hash).
Every card above runs at 0.87 to 1.0 of its probe ceiling / 128 (table 1.2), so:
microjoules per unit = 128 x P / A, with P the card's watts at its operating point and A its random reads per second.
Splitting both by the memory bus (32-bit units: the 5090 16, the 9070 XT 8, the 7600 4, the B580 6, the 5080 and 5070 Ti
8; vendor figures, approximate):
| Card | A per 32 bits (G reads/s) | P per 32 bits (W) | Nanojoules per random read (P / A) | Microjoules per hash | Against the 5090: watts ratio x reads ratio = joules ratio | Label |
|---|---|---|---|---|---|---|
| RTX 5090 at the lock | 1.08 | 19.6 | 18.2 | 2.33 | 1 | A measured, P measured |
| RTX 5080 at 1,100 MHz | 1.14 | 18.3 | 16.1 | 2.06 | 0.94 x 0.95 = 0.88 | measured (rented) |
| RX 9070 XT at the floor point | 0.30 | 18.7 | 61.7 | 7.9 | 0.95 x 3.6 = 3.4 | measured |
| RX 9070 XT at stock | 0.30 | 25.3 | 84 | 10.7 | 1.29 x 3.6 = 4.6 | measured |
| RX 7600 at stock, class v4 run | 0.44 | 28.3 | 63.5 | 8.14 | 1.44 x 2.4 = 3.5 | measured |
| RX 7600 at stock, class v6 rate | 0.51 | 28.3 | 56 | 7.2 | 1.44 x 2.1 = 3.1 | modelled (watts from the v4 run) |
| Arc B580 at stock | 0.23 | 18.3 | 78 to 80 | 10.4 | 0.93 x 4.7 = 4.4 | A measured, P estimated |
| Apple M5 Max (for contrast) | 0.22 | 2.3 | 10.7 | 1.40 | 0.12 x 4.9 = 0.60 | measured |
Reading: at their knees the AMD and Intel cards spend about the same watts per 32 bits of memory bus as the 5090
(0.93x to 0.95x; the 7600 at stock and the 9070 XT at stock 1.3x to 1.4x because nothing has been taken off them), and
the whole 3x to 4.5x is the random-read rate per 32 bits. Against the 5080 and the 5070 Ti (1.70 to 2.06) the same rows
read 3.8x to 6.1x, which is the "4x to 6x" of ECO-05. The Mac shows the other way out: a memory system with a quarter of
the 5090's random-read rate per bus width wins on joules because it spends an eighth of the watts per bus width.
### 2.2 The random-read rate per 32 bits: where the 3.6x lives
| Factor | 5090 against the 9070 XT | Label |
|---|---|---|
| Channels per 32-bit device: GDDR7 four, GDDR6 two | 2x | JEDEC device organisation, approximate |
| Random reads per channel per second: 5090 about 270 M, 9070 XT about 150 M (the per-channel figure of floor/denominator.md) | 1.8x | modelled from the measured ceilings and the channel counts |
| Product | 3.6x | matches the measured 1.08 / 0.30 |
Within GDDR6 the per-channel rate varies 2.2x on the same DRAM family: the RX 7600 about 220 to 250 M per channel per
second (8 channels; hash-implied, modelled), the 9070 XT about 150 M (16 channels; measured ceiling), the B580 about 115
M (12 channels; measured ceiling). The 7600's rate says GDDR6 itself allows 1.5x to 2.2x more random reads per channel
than the 9070 XT and the B580 obtain, so part of their deficit sits above the DRAM, in the memory controller, the fabric,
the last-level cache path or address translation. That part is the only piece of the gap that software might reach
(section 3, candidate 2); the GDDR7 against GDDR6 part is hardware. Tonight's B580 probe bounds the translation share
on Xe2: its ceiling is 1.55 to 1.57 G/s at 256 MiB and 1.41 at 1024 MiB (measured), both far beyond its 18 MB L2, so
the footprint (page reach) costs it about 10 percent and the other 90 percent of its per-channel shortfall is the memory
controller and fabric's.
Bandwidth is not the bound on the AMD and Intel cards: the 9070 XT moves 2.42 G x 64 bytes = 155 GB/s, 24 percent of its
640 GB/s rating, and a 64-byte read costs it exactly what a 4-byte read costs (measured); the bound is the rate of random
accesses, each opening a DRAM row. The 5090 at 64 bytes is the opposite case (it becomes bandwidth-bound and halves).
### 2.3 The kernel's memory path on RDNA 3, RDNA 4 and Xe2
Instrument: the class v6 OpenCL texts (`kernel.cl` of hl-v6-all, id 0x9d40978601a7df2a, and hl-v6-foldrw, id
0x605d06cabc489f94, from build-1 `/srv/artefacts/packs/`) compiled offline with clang 18.1.3 for amdgcn-amd-amdhsa,
-O3, OpenCL C 1.2, `IGNEUM_EXCHANGE 0` (the local-memory path the driver takes on the 9070 XT), the work-item and
rotate built-ins shimmed to the target's own built-ins (no libclc on the box). The driver's compiler is AMD's own LLVM
(the PAL,LC stack) of a different version, so these are **modelled** stand-ins for the driver's binary; the driver's own
numbers come from the host's kernel line (private memory, local memory, sub-group) and, for the 9070 XT, the 5 October
measurement (wave32, private memory 0).
| Quantity | hl-v6-all, gfx1102 | hl-v6-all, gfx1201 | hl-v6-foldrw, gfx1102 | hl-v6-foldrw, gfx1201 | Label |
|---|---|---|---|---|---|
| Wave size | 32 | 32 | 32 | 32 | modelled; the driver reports wave32 on gfx1201 (measured, 5 Oct) |
| VGPRs | 160 | 160 | 96 | 96 | modelled |
| Waves per SIMD (LLVM 18's model) | 6 | 6 (LLVM 18 models 1,024 VGPRs for gfx1201; its gfx1100 model gives 9 at 156) | 10 | 10 | modelled |
| Spills (scratch bytes) | 0 | 0 | 0 | 0 | modelled; the driver reports private memory 0 on the 9070 XT (measured, class v2) |
| LDS per work-group | 256 B (the exchange's two buffers) | 256 B | 256 B | 256 B | modelled |
| Barriers emitted | 0 (work-group = one wave32: elided) | 0 | 0 | 0 | modelled; matches WAVEFRONT.md's measured "barriers elided" |
| Global loads in the loop body | 32 x global_load_b32 | 32 | 16 x global_load_b32 | 16 | modelled |
| Loads with a full wait right after them | all 32 (51 vmcnt(0) waits) | all 32 | 13 of 16 (three overlap) | | modelled |
| Hash-kernel code size | 33.6 KB | 34.0 KB | 6.3 KB | 6.6 KB | modelled |
| Integer multiplies (mul_lo, mad, mul_hi) per body | 99 | 99 | 67 | 67 | modelled |
The five questions the brief asks, answered on these numbers:
1. **Wave size.** Wave32 on RDNA 3 and 4 (the compiler's choice and the driver's report); a work-group of 32 is one wave,
so the local-memory exchange compiles to LDS writes and reads with no barrier. The exchange path and the work-group
shape cost nothing on the 9070 XT (`--group-warps 1, 2, 4, 8` = 18.02 to 18.07 MH/s, measured 5 Oct). Xe2: the
kernel takes the khr or Intel sub-group shuffle only at a queried sub-group of exactly 32; the Arc's sub-group and
SIMD width under the 64-register window, measured tonight on the B580 (the host's kernel line, job
run-ae-pc2-b580-energy-20261008): the partner hl-v6-foldrw compiles at sub-group 32 and takes `sub_group_shuffle_xor`;
the window pack hl-v6-all drops to sub-group 16 (SIMD16, the Intel compiler's answer to 64 live registers), so the
host takes the local-memory exchange with barriers (256 bytes); private memory 0 in both (no spill). The cost of that
drop: none in rate (10.98 MH/s per unit of work against 11.01, -0.3 percent, measured), because the card is at 100
percent of its random-read ceiling either way.
2. **The 16-byte read's coalescing.** Class v6 reads 4 bytes per load (program.json above); there is no 16-byte read in
the frozen object. The read-width experiment's 16-byte loads (w16) cost every card what 4 bytes cost (5090 139.8
against 136.1 MH/s, 9070 XT 17.90 against 18.15, measured 5 Oct): a lane's random read fetches a whole line (64 bytes
on AMD and Apple, a 32-byte sector on NVIDIA) and the 32 lanes of a wave hit 32 different lines (a 1 GiB dataset has
16.8 M 64-byte lines; two lanes of a wave share one with probability about 3 x 10^-5), so there is nothing to
coalesce.
3. **LDS use.** 256 bytes per wave, only the exchange: 8 exchanges per iteration in the body of hl-v6-all, 4 in
hl-v6-foldrw, plus 13 per shadow pass x 27 passes; about 2,840 LDS write-read pairs per unit of work. At a few
picojoules per lane-access that is a few nanojoules per hash against 7,900 (modelled, under 0.1 percent).
4. **The dependent-load chain's latency hiding.** Under the full chain every load's address mixes all 64 registers,
including the previous load's result, so each wave has one load in flight (all 32 loads wait vmcnt(0)). It does not
matter on these cards: 4,096 lanes in flight already saturate the 9070 XT's DRAM (2.64 G/s at 4,096 lanes, measured),
and occupancy 6 on 64 CUs holds 24,576 lanes (the 7600 at 32 CUs: 12,288). The latency is hidden by waves, not by
loads in flight within a wave, with 3x to 6x to spare.
5. **Occupancy under the 64-register window.** 160 VGPRs against 96: 6 waves per SIMD against 10 (modelled), no spill.
Measured consequence on the 7600: none in rate (15.84 per unit of work against 15.79). On NVIDIA the hash lane
measured the window at 96/88 registers (5090) and 104/87 (4090), occupancy 67 to 83 percent, within 5 percent per
load. **GPU-03's AMD cell, rate: 0 percent per unit of work on the RX 7600 (measured, quiet, rate only); energy:
section 4. GPU-03's Intel cell, rate: -0.3 percent per unit of work on the Arc B580 (measured, quiet, rate only),
with the class v6 fingerprints equal to the CUDA and Metal references on both packs (5a6ad122a71a888f and
59e6708e46f1e87c, self-test PASS): the B580 computes class v6 bit-exact.**
### 2.4 Where an AMD card's joules go (modelled split at the 9070 XT's floor point, 149.3 W, 18.9 MH/s)
| Term | Watts | Basis | Label |
|---|---|---|---|
| ALU work | 6 to 12 | 55.3 k lane-ops per unit of work for the partner (8 iterations x (64 + 27 x 256 shadow ops)), 42 k for the window pack (its shadow covers two units); at 5 to 10 pJ per lane-op (the M5 Max's measured marginal 6.9 pJ per counted op as the scale); 17 percent of the card's measured 6.2 T int op/s chain throughput in use | modelled |
| DRAM array and I/O for 2.42 G random 64-byte reads per second | 10 to 20 | 4 to 8 nJ per activate-read-transfer of a 64-byte burst (public GDDR6 figures, approximate) | modelled |
| LDS exchange | under 0.2 | section 2.3 | modelled |
| The rest: the die and board kept awake at full memory data rate (clock trees, fabric and last-level cache, memory PHY, VRM losses, fans) | about 120 | the remainder | modelled |
The kernel's own work (ALU, LDS, the loads it issues) is 15 to 30 W of 149; the remainder is the price of a whole
graphics card serving 8 or 16 GDDR6 channels' worth of random reads. That is why the knobs that take the core down
without touching memory (the 9070 XT's grid: 24 points, the rate flat at 18.9 MH/s, measured) are worth more than any
kernel text, and why they stop at the driver's floor (-500 MHz, -30 percent), not at the hash's.
## 3. Candidate kernel changes that keep every hash identical, ranked
Common to all: none changes the hash function, so none changes the adversary's cost (the adversary's chip computes the
function, not our kernel), and none can help the adversary; each moves only the GPU side. The equality proof for each:
(a) the pack's self-test on the changed kernel (cache head and FNV, dataset samples, 96 of 96 vector lanes), (b) the 2^24
batch fingerprint at base nonce 0 equal to the unchanged kernel's and to the CUDA and Metal references on the same pack
(hl-v6-foldrw 5a6ad122a71a888f, hl-v6-all 59e6708e46f1e87c on the pre-review export; the re-export's pair when it lands),
on every platform the change ships to, (c) the P01 vector file (`igneum-pow hash-bound --count 1000000`, staged at
build-1 `/srv/artefacts/packs/p01-vectors/`) through the changed kernel. P03 is then a paired energy and rate run on
every mandatory cell the change touches (an OpenCL-only change touches no CUDA card). No candidate reduces the
specialist's advantage, so none is a G2 upgrade: they are efficiency work on the baseline.
| Rank | Change | Where | Expected gain (all modelled) | The test that proves equality | Risk under P03 | Clock |
|---|---|---|---|---|---|---|
| 0 (a baseline, not a candidate) | The AMD operating point below the driver's floor: a lower absolute core clock or a voltage offset where ADLX exposes one (RDNA 3 and 4 Adrenalin carry a voltage offset; ADLX's manual tuning interface for it is unverified here) | the Ember tune's AMD path, not the kernel | the ALU needs about 17 percent of the 9070 XT's shader throughput and about 30 percent of the 7600's (muls at a quarter rate), so the core can lose half its clock before the rate moves: 149 W to 110 to 125 W on the 9070 XT, 7.9 to 5.8 to 6.6 microjoules; the 7600 similar in proportion | clocks never change outputs; the grid's own per-point self-test and fingerprint | none on correctness; P03 places clock savings in the baseline, so it lowers the AMD baseline (ECO-05's input) and scores nothing as a candidate | the update-return lane's AMD knob; a read of ADLX's absolute and voltage interfaces by 12:00 tomorrow |
| 1 | Occupancy throttle: launch only the lanes that saturate the DRAM (about 4,096 to 8,192 on the 9070 XT, measured) on a fraction of the CUs, so idle CUs clock-gate and the driver's power manager sees a partly idle die | host (a `--max-groups` cap on the dispatch, about 20 lines in host.c; the persistent-warp loop already exists for variant-5 packs) | 0 to 10 percent of the card's watts, unknown until measured; the ALU budget bounds the throttle at about a quarter of the 9070 XT's CUs and half the 7600's | per-nonce outputs do not depend on the launch shape (WAVEFRONT.md: work-groups of 32 to 256 bit-exact; the 2^24 fingerprint at every cap) | a cap below the ALU need loses rate; the sweep finds the knee | host flag and a sweep job: tomorrow 15:00 |
| 2 | Find the GDDR6 per-channel shortfall above the DRAM (the 9070 XT and the B580 at 0.45x to 0.65x the 7600's per-channel rate) and remove it if it is software's: page size and TLB reach of the 1 GiB dataset buffer, the buffer's placement, the driver's allocation flags | host allocation and driver flags | on the B580 at most about 10 percent (its probe falls 10 percent from 256 to 1024 MiB, measured tonight: that is the translation share); the 9070 XT unprobed at two sizes, the same bound expected (modelled) | allocation never changes outputs; the fingerprint | none on correctness; a different allocation could cost the NVIDIA path nothing because it would be vendor-gated | B580 read tonight (10 percent); the 9070 XT's two-size probe when it is back in the housing |
| 3 | Non-temporal dataset loads on AMD (`__builtin_nontemporal_load` in the OpenCL text, which AMD's LLVM lowers to the non-temporal bit on RDNA 3 and the TH_LOAD_NT hint on RDNA 4): no last-level-cache or L2 allocation for lines that are not reused | OpenCL text, AMD only | 0 to 3 percent of the watts (no 64-byte fill into the 32 or 64 MB cache per read); risk of losing the 3 to 6 percent of reads that hit that cache (0 to -3 percent rate) | the hint changes no value; the fingerprint and the vectors | the rate risk; paired run decides | an emitter flag and a PC 1 pair: tomorrow 15:00 |
| 4 | The window's address mix computed incrementally: m is linear over GF(2) in the 64 registers, so m(src) = A xor T_src xor P xor rotr(P, 1), with T_k = rotl(r_k, 63 - k), A the xor of all T_k kept up to date on each register write (two ops per write) and P the xor of T_k below src; the compiler today emits about 108 ops per load (52 rotates, 54 xors, 2 xor3) | emitter, all three dialects | removes about 20 to 25 percent of the window pack's ALU ops per unit of work, so 1 to 2 percent of an AMD card's joules and less on NVIDIA (its ALU share is smaller still) | the identity is exact; a property test in igneum-pow over random register files and every src, then the fingerprint and vectors | none expected; it must not move the 5090 or 4090 window rows by more than noise | an emitter change on the hash lane's line, after the post-review freeze: days two and three |
| 5 | The exchange through DPP or ds_swizzle instead of LDS (RDNA), sub-group shuffles on Intel | OpenCL text, vendor-gated | under 0.1 percent (section 2.3, item 3) | the fingerprint across the exchange paths (WAVEFRONT.md's emulator table already proves path equality) | none | not worth a slot; recorded to close the question |
Not a candidate (changes the hash; recorded so it is not proposed again): reads of 64 bytes per load (w64) close the
5090 against 9070 XT gap from 7.5x to 4.1x in rate, entirely by halving the 5090 (measured, 5 Oct), so the honest NVIDIA
cells lose about 50 percent of accepted throughput against P03's 2 percent: out. W = 8 and W = 32 are standing rejected
knobs. The hot table and the scratch read-modify-write cost the 9070 XT 13 to 33 percent (measured): out.
What this means for the vendor question: no kernel change that keeps the hash identical is worth more than about 10
percent on AMD (candidate 1) plus whatever candidate 2 finds above the DRAM; the 3x to 4.5x is GDDR7 against GDDR6 and a
graphics card's fixed power over few channels. Closing it by changing the hash would cost the honest NVIDIA cards more
than P03 allows, which the standing rule forbids ("no ratio is bought with honest GPU energy").
## 4. Rows for the 21:00 economics landing and the P02 cohort, with clocks
### 4.1 Tonight's two jobs
| Job | Machine | What it reads | Instrument | Status and clock |
|---|---|---|---|---|
| run-ae-pc1-7600-energy-20261008 | PC 1 (ae432dc7), RX 7600 | memprobe at 1024 MiB; hl-v6-foldrw and hl-v6-all as the load at stock, plimit -30, gmax -500 with plimit -30; reset and read-back | the kit worker's bench (rate), ADLX through igneum-gpu-telemetry.exe every 5 s, samples after 20 s (watts) | published 19:41 UK behind the hash lane's ds2g and l8off; at 20:1x UK PC 1's runner was still held by the withdrawn overnight 7600 grid job (its last upload 19:06 UTC), so none of the three had started; the follow-up reset job run-ae-pc1-7600-reset-20261008 is queued behind it |
| fetch-ae-ze-power-20261008 + run-ae-pc2-b580-energy-20261008 | PC 2 (1ccfe586), Arc B580 | memprobe at 256 and 1024 MiB; both v6 packs at stock | the kit worker's bench (rate), ze-power.exe (Level Zero sysman energy counters, read only, built on build-1 with mingw, sha256 354abddde13a5341521eb298a5728e65675af7474823faf9b8d5613caf840d8f) | ran 19:50 to 19:54 UK (268 s, done): rates, probes and fingerprints read; the energy counter unsupported unelevated (rc 0x78000003 on all three domains, the device seen as `Intel(R) Graphics [0xe20b]`) |
### 4.2 The rows, each with its clock
| # | Row | For | Value and label | Clock |
|---|---|---|---|---|
| 1 | The RX 7600's class v6 rate replaces the class v4 card-in rate in the reference population | the research lane's 21:00 landing | 15.79 MH/s (measured, quiet, rate only); the stock cell 7.2 microjoules (modelled on the v4 run's 113 W) until row 2 | now |
| 2 | The RX 7600 metered at stock and at two knob points on class v6 | the 21:00 landing; ECO-05's section 5 condition (a 7600 knee under 5 microjoules adds the worlds at 0.03 and 0.10) | OWED: the PC 1 runner had not reached the job at landing; until it reports, the cells stay 7.2 at stock (modelled on the class v4 run's 113 W) and 5.0 to 5.4 at the floor point (modelled: the 9070 XT's 24 percent, or a flat rate at -30 percent power) | when PC 1's runner reaches it, about 20 minutes after the hash lane's ds2g and l8off; the rows go to the research lane's ECO-05 batch 02 the minute they land |
| 3 | The Arc B580 on class v6 | the 21:00 landing; ECO-05's section 5 condition (a B580 under 8 adds worlds) | rate 11.01 MH/s (measured, KT); watts NOT READ: the Level Zero sysman energy counter answers unsupported (0x78000003) unelevated on driver 32.0.101.6733, so the cell stays about 110 W estimated, 10.0 microjoules (estimated); under 8 would need under 88 W | rate landed 19:54 UK; watts owed: an unelevated IGCL telemetry read (unverified) or a wall meter, tomorrow 15:00; an elevated read needs main's exception to the nothing-elevated rule |
| 4 | GPU-03's AMD cell: the window's cost per unit of work on the RX 7600 | the register (row 11 and GPU-03) | rate 0 percent (15.84 against 15.79, measured, KT); energy OWED with row 2 (the same job reads both packs at each point) | rate landed; energy with row 2 |
| 5 | GPU-03's Intel cell | the register | rate -0.3 percent per unit of work (measured, KT); SIMD16 under the window, no spill; fingerprints equal on both packs; energy not read | landed 19:54 UK |
| 6 | The named cause of ECO-05's two-vendor failure | the 21:00 landing's text and eco-05-results.md | section 0 and 2.1 of this file (per 32 bits of bus: the same watts, 0.21x to 0.47x the random reads) | landed with this file |
| 7 | The 9070 XT | the cohort | unchanged: 7.9 at the floor point (measured); no v6 row tonight (out of the housing since 14:2x) | when it is back on the bus |
### 4.3 The P02 cohort, AMD and Intel cells
| P02 requirement | State tonight | Owed |
|---|---|---|
| At least 3 AMD retail configurations | 2 measured (RX 9070 XT 16 GB, RX 7600 8 GB); the 9060 XT and 7600 XT are in the card-in queue, not on a PC | a third AMD card on PC 1; the hash lane's card-in job runs it as it arrives |
| An advertised 8 GB mining tier | the RX 7600 8 GB (measured; the 5.5 GiB dataset fits at 6,732 MiB of 8,176) | |
| Usable, not nominal, memory | the 7600's 8,176 MiB (measured) | the B580's and the 9070 XT's usable figure from the host's device line |
| Every advertised vendor in the competitive core | Intel: the Arc B580 only; its class v6 rate and fingerprints measured tonight, its watts not readable unelevated | a second Intel configuration if Intel stays advertised |
| Calibrated wall meter, at most 2 percent | none: every AMD and Intel watt is ADLX or Level Zero device telemetry | a wall meter on each PC (a purchase and a hand at the socket); until then every row reads "device-reported" |
| 5 paired 30-minute runs per primary cell after 15 minutes of warm-up | none: tonight's points are about 1 minute each after 20 s | the paired protocol on PC 1 and PC 2 once the 2.0 devnet mines there (GPU-02 and GPU-03 under the standard) |
| Three unaffiliated operators | none | the served kit (D1) |
## 5. Unverified and owed
- The offline compiles are LLVM 18 standing in for AMD's driver compiler; the driver's VGPR count and wave size on the
7600 and the B580 come from tonight's host kernel lines.
- The channel counts per device and the GDDR6 and GDDR7 energy per access are JEDEC and public figures, approximate.
- The split of section 2.4 is modelled; only a per-rail reading (not available through ADLX) would measure it.
- The B580's watts: the Level Zero sysman energy counter is unsupported unelevated on driver 32.0.101.6733 (measured
tonight); the 110 W stays an estimate until an unelevated IGCL read, a wall meter, or main's exception for one
elevated read.
- The RX 7600's metered class v6 rows (job run-ae-pc1-7600-energy-20261008, queued on PC 1 behind a held runner).
- The ALU energy per op on RDNA is taken at the M5 Max's measured scale, not measured on AMD.
- Candidate 0's ADLX absolute-clock and voltage interfaces are unverified on the tool; candidates 1 to 4 are unmeasured.

View file

@ -0,0 +1,10 @@
// shims for an offline AMDGPU compile without libclc (analysis only; never a mining kernel)
#define get_global_id(d) ((size_t)(__builtin_amdgcn_workgroup_id_x() * 32u + __builtin_amdgcn_workitem_id_x()))
#define get_local_id(d) ((size_t)__builtin_amdgcn_workitem_id_x())
#define get_global_size(d) ((size_t)(1u<<20))
#define get_sub_group_size() 32u
#define rotate(x, n) __builtin_rotateleft32((x), (n))
#define mul_hi(a, b) ((uint)(((ulong)(a) * (ulong)(b)) >> 32))
#define barrier(f) do { __builtin_amdgcn_fence(__ATOMIC_RELEASE, "workgroup"); __builtin_amdgcn_s_barrier(); __builtin_amdgcn_fence(__ATOMIC_ACQUIRE, "workgroup"); } while (0)
#define vload4(o, p) (*(const __global uint4*)((p) + 4u*(o)))
#define vstore4(v, o, p) (*(__global uint4*)((p) + 4u*(o)) = (v))

View file

@ -0,0 +1,34 @@
== hl-v6-all-gfx1102 (clang 18.1.3, -O3, OpenCL C 1.2, IGNEUM_EXCHANGE 0)
; codeLenInByte = 33576
; NumSgprs: 18
; NumVgprs: 160
; ScratchSize: 0
; LDSByteSize: 256 bytes/workgroup (compile time only)
; Occupancy: 6
global_load_b32 32; s_barrier 0; full waits 51; VALU 4080; v_alignbit 1657; v_xor_b32 1724; v_xor3 72; int mul 99; ds_ 42; scratch 0
== hl-v6-all-gfx1201 (clang 18.1.3, -O3, OpenCL C 1.2, IGNEUM_EXCHANGE 0)
; codeLenInByte = 34004
; NumSgprs: 18
; NumVgprs: 160
; ScratchSize: 0
; LDSByteSize: 256 bytes/workgroup (compile time only)
; Occupancy: 6
global_load_b32 32; s_barrier 0; full waits 49; VALU 4077; v_alignbit 1657; v_xor_b32 1724; v_xor3 72; int mul 99; ds_ 42; scratch 0
== hl-v6-foldrw-gfx1102 (clang 18.1.3, -O3, OpenCL C 1.2, IGNEUM_EXCHANGE 0)
; codeLenInByte = 6268
; NumSgprs: 20
; NumVgprs: 96
; ScratchSize: 0
; LDSByteSize: 256 bytes/workgroup (compile time only)
; Occupancy: 10
global_load_b32 16; s_barrier 0; full waits 25; VALU 675; v_alignbit 84; v_xor_b32 104; v_xor3 20; int mul 67; ds_ 34; scratch 0
== hl-v6-foldrw-gfx1201 (clang 18.1.3, -O3, OpenCL C 1.2, IGNEUM_EXCHANGE 0)
; codeLenInByte = 6584
; NumSgprs: 20
; NumVgprs: 96
; ScratchSize: 0
; LDSByteSize: 256 bytes/workgroup (compile time only)
; Occupancy: 10
global_load_b32 16; s_barrier 0; full waits 23; VALU 675; v_alignbit 84; v_xor_b32 104; v_xor3 20; int mul 67; ds_ 34; scratch 0
b3314b187c414925e52febb3683120cf7f8db2c8f4b802816b964d705c33c571 ../hl-v6-all/hl-v6-all/kernel.cl
b66c22e859244f86f164e05841858ed1d39b42f1ab6611504872c652fb02ee23 ../hl-v6-foldrw/hl-v6-foldrw/kernel.cl

View file

@ -0,0 +1,107 @@
# AMD-and-Intel energy lane (Igneum 2.0 register: GPU-03's AMD cells, ECO-05's owed RX 7600 knee), 8 October 2026.
# The three-card Windows rig's RX 7600 (OpenCL gfx1102), measure only, unelevated: the class v6 kit's own igneum-worker-opencl.exe is the load
# (--bench-pack, no app involvement: the app is never quit, paused or resumed, no api/cards, no app tune), the rebuilt
# igneum-gpu-telemetry.exe (ADLX GPUPower, else GPUTotalBoardPower) samples every 5 s beside it, at three knob points:
# stock, plimit -30, gmax -500 + plimit -30 (the 9070 XT's best). ADLX manual tuning is reset at the end and a read-back
# sample is printed. First: --memprobe at 1024 MiB (the card's dependent random-read ceiling, the 9070 XT's and B580's
# instrument). Every result line starts with RESULT; SUMMARY {json} ends it. The sampler is a child process whose pid is
# written to this job's folder and stopped by that pid only.
$ErrorActionPreference = 'Continue'
$jobName = 'pc1-7600-energy'
$started = Get-Date
function Stamp { (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') }
function Summary([string] $status, [hashtable] $extra) {
$o = [ordered]@{ job = $jobName; status = $status; duration_s = [int]((Get-Date) - $started).TotalSeconds; finished_at = (Stamp) }
foreach ($k in $extra.Keys) { $o[$k] = $extra[$k] }
'SUMMARY ' + ($o | ConvertTo-Json -Compress -Depth 5)
}
"RESULT start $(Stamp) job=$jobName machine=$env:COMPUTERNAME app_version=$env:IGNEUM_APP_VERSION"
$jobs = Split-Path $env:IGNEUM_JOB_DIR
$kitId = $env:IGNEUM_V6_KIT_ID; if (-not $kitId) { $kitId = 'fetch-class-v6-kit-20261008' }
$kit = Join-Path $jobs $kitId
$exe = Join-Path $kit 'bin\windows\igneum-worker-opencl.exe'
$packs = Join-Path $kit 'packs'
if (-not (Test-Path $exe)) { "RESULT error worker missing at $exe (fetch job $kitId first)"; Summary 'failed' @{ error = 'worker missing' }; exit 2 }
"RESULT worker $exe sha256 $((Get-FileHash -Algorithm SHA256 $exe).Hash.ToLower()) kit=$kitId"
# the telemetry tool: the newest job-folder copy that prints a tune line, else the installed one (the grid playbook's rule)
$install = Join-Path $env:LOCALAPPDATA 'Programs\Igneum Miner'
$tool = $null; $cands = @()
$jobsDir = Join-Path $env:LOCALAPPDATA 'igneum\app\jobs'
if (Test-Path -LiteralPath $jobsDir) { $cands += @(Get-ChildItem -LiteralPath $jobsDir -Recurse -Filter 'igneum-gpu-telemetry*.exe' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | ForEach-Object { $_.FullName }) }
$cands += Join-Path $install 'igneum-gpu-telemetry.exe'
foreach ($c in $cands) { if (-not (Test-Path -LiteralPath $c)) { continue }; $pr = @(& $c --tune 2>&1 | ForEach-Object { "$_" }) | Where-Object { $_ -match '^tune \d+ ' }; if ($pr) { $tool = $c; break } }
if (-not $tool) { "RESULT error no telemetry tool with a tune line"; Summary 'failed' @{ error = 'no tool' }; exit 2 }
"RESULT tool $tool sha256 $((Get-FileHash -LiteralPath $tool -Algorithm SHA256).Hash.ToLower())"
$tune = @(& $tool --tune 2>&1 | ForEach-Object { "$_" })
$tune | ForEach-Object { "RESULT tune $_" }
$line = $tune | Where-Object { $_ -match '^tune (\d+) name "([^"]*7600[^"]*)" .* ok\s*$' } | Select-Object -First 1
if (-not $line) { "RESULT error no RX 7600 tune line"; Summary 'failed' @{ error = 'no 7600 tune line' }; exit 2 }
$ord = [int]([regex]::Match($line, '^tune (\d+)').Groups[1].Value)
$gr = [regex]::Match($line, 'gmax_range (-?\d+) (-?\d+)'); $prr = [regex]::Match($line, 'plimit_range (-?\d+) (-?\d+)')
"RESULT card ordinal=$ord gmax_range=$($gr.Groups[1].Value)..$($gr.Groups[2].Value) plimit_range=$($prr.Groups[1].Value)..$($prr.Groups[2].Value)"
# the OpenCL device of the 7600
$list = @(& $exe --list 2>&1 | ForEach-Object { "$_" }); $list | ForEach-Object { "RESULT list $_" }
$dev = $null; foreach ($l in $list) { if ($l -match '^\s*\[(\d+)\].*gfx1102' -and $l -notmatch 'dup') { $dev = [int]$Matches[1]; break } }
if ($null -eq $dev) { "RESULT error no gfx1102 in --list"; Summary 'failed' @{ error = 'no gfx1102' }; exit 2 }
$w = @(Get-CimInstance Win32_Process -Filter "Name = 'igneum-worker-opencl.exe'" -ErrorAction SilentlyContinue | ForEach-Object { "$($_.ProcessId):[$($_.CommandLine -replace '\s+', ' ')]" })
$loaded = ($w | Where-Object { $_ -match "--device\s+$dev(\s|$)" }).Count -gt 0
"RESULT device $dev gfx1102 card_state=$(if ($loaded) { 'loaded (another worker on the card: the rows are labelled loaded)' } else { 'quiet' }) workers=[$($w -join ' ')]"
function SampleLine() { @(& $tool 2>&1 | ForEach-Object { "$_" }) | Where-Object { $_ -match ('^amd ' + $ord + ' ') } | Select-Object -First 1 }
"RESULT idle_sample $(Stamp) $(SampleLine)"
# 1. the random-read ceiling at the dataset size
$mp = @(& $exe --memprobe --probe-mib 1024 --device $dev 2>&1 | ForEach-Object { "$_" })
$mp | ForEach-Object { "RESULT memprobe $_" }
# 2. the three knob points x the two packs, the bench as the load, the sampler beside it
$sampler = Join-Path $env:IGNEUM_JOB_DIR 'sampler.ps1'
$pidFile = Join-Path $env:IGNEUM_JOB_DIR 'sampler.pid'
Set-Content -LiteralPath $sampler -Value @'
param([string] $tool, [int] $ord, [string] $out)
while ($true) { $l = @(& $tool 2>&1 | ForEach-Object { "$_" }) | Where-Object { $_ -match ('^amd ' + $ord + ' ') } | Select-Object -First 1; Add-Content -LiteralPath $out -Value ((Get-Date).ToUniversalTime().ToString('o') + ' ' + $l); Start-Sleep -Seconds 5 }
'@
function SetPoint([int] $g, [int] $p) {
$a = @(& $tool --card $ord --set-gmax $g 2>&1 | ForEach-Object { "$_" }) | Where-Object { $_ -match '^tune ' } | Select-Object -First 1
$b = @(& $tool --card $ord --set-plimit $p 2>&1 | ForEach-Object { "$_" }) | Where-Object { $_ -match '^tune ' } | Select-Object -First 1
return @{ ok = (($a -match ' ok\s*$') -and ($b -match ' ok\s*$')); lines = ($a + ' | ' + $b) }
}
$rows = @()
$points = @(@(0, 0), @(0, -30), @(-500, -30))
foreach ($pt in $points) {
$g = $pt[0]; $p = $pt[1]
if ($g -lt [int]$gr.Groups[1].Value -or $p -lt [int]$prr.Groups[1].Value) { "RESULT point gmax=$g plimit=$p skipped (outside the card's range)"; continue }
$set = SetPoint $g $p
"RESULT point gmax=$g plimit=$p set ok=$($set.ok) $($set.lines)"
if (-not $set.ok) { continue }
foreach ($pk in @('hl-v6-foldrw', 'hl-v6-all')) {
$d = Join-Path $packs $pk
$nb = if ($pk -eq 'hl-v6-all') { 40 } else { 80 }
$samp = Join-Path $env:IGNEUM_JOB_DIR ("samples-$pk-g$g-p$p.txt")
$sp = Start-Process -FilePath 'powershell.exe' -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $sampler, '-tool', $tool, '-ord', $ord, '-out', $samp) -WindowStyle Hidden -PassThru
Set-Content -LiteralPath $pidFile -Value $sp.Id
$t0 = Get-Date
$out = @(& $exe --bench-pack --pack $d --device $dev --batch-log2 24 --batches $nb 2>&1 | ForEach-Object { "$_" })
$t1 = Get-Date
$spid = [int](Get-Content -LiteralPath $pidFile); Stop-Process -Id $spid -Force -ErrorAction SilentlyContinue; Remove-Item -LiteralPath $pidFile -ErrorAction SilentlyContinue
foreach ($l in $out) { if ($l -match '^(pack |class |RESULT |FAIL|error|warm-up|kernel|exchange)') { "RESULT bench $pk g=$g p=$p out $l" } }
$res = $out | Where-Object { $_ -match '^RESULT ' } | Select-Object -Last 1
$fp = ''; $mhs = 0; $check = ''
if ($res -match 'fingerprint=([0-9a-f]{16})') { $fp = $Matches[1] }
if ($res -match 'mhs=([0-9.]+)') { $mhs = [double]$Matches[1] }
if ($res -match 'check=(\w+)') { $check = $Matches[1] }
# the watts: samples from 20 s after the bench started (the dataset build and warm-up excluded) to its end
$ws = @(); $gc = @(); $raw = @()
if (Test-Path -LiteralPath $samp) { foreach ($s in Get-Content -LiteralPath $samp) { $raw += $s; $ts = [datetime]::Parse(($s -split ' ')[0]).ToUniversalTime(); if ($ts -ge $t0.ToUniversalTime().AddSeconds(20) -and $ts -le $t1.ToUniversalTime()) { $m = [regex]::Match($s, ' watts (-?[\d.]+)'); if ($m.Success -and [double]$m.Groups[1].Value -gt 0) { $ws += [double]$m.Groups[1].Value }; $c = [regex]::Match($s, ' gclk_mhz (-?[\d.]+)'); if ($c.Success) { $gc += [double]$c.Groups[1].Value } } } }
$raw | Select-Object -First 3 | ForEach-Object { "RESULT sample $pk g=$g p=$p $_" }
$wm = if ($ws.Count) { [math]::Round((($ws | Measure-Object -Average).Average), 1) } else { 0 }
$gm = if ($gc.Count) { [math]::Round((($gc | Measure-Object -Average).Average), 0) } else { 0 }
$uj = if ($mhs -gt 0 -and $wm -gt 0) { [math]::Round($wm / $mhs, 3) } else { 0 }
"RESULT ROW card=RX7600 pack=$pk gmax_off=$g plimit=$p mhs=$mhs watts=$wm uj_per_hash=$uj gclk=$gm samples=$($ws.Count) seconds=$([int]($t1 - $t0).TotalSeconds) fingerprint=$fp check=$check $(Stamp)"
$rows += [ordered]@{ pack = $pk; g = $g; p = $p; mhs = $mhs; watts = $wm; uj = $uj; gclk = $gm; samples = $ws.Count; fingerprint = $fp; check = $check }
}
}
$r = @(& $tool --card $ord --reset 2>&1 | ForEach-Object { "$_" }) | Where-Object { $_ -match '^tune ' } | Select-Object -First 1
"RESULT reset $r"
Start-Sleep -Seconds 3
"RESULT after_reset_sample $(Stamp) $(SampleLine)"
"RESULT after_reset_tune $((@(& $tool --tune 2>&1 | ForEach-Object { "$_" }) | Where-Object { $_ -match ('^tune ' + $ord + ' ') } | Select-Object -First 1))"
Summary $(if ($rows.Count) { 'done' } else { 'failed' }) @{ rows = $rows; device = $dev; ordinal = $ord; kit = $kitId }
exit $(if ($rows.Count) { 0 } else { 1 })

View file

@ -0,0 +1,23 @@
# AMD-and-Intel energy lane, 8 October 2026: the guaranteed ADLX reset of the RX 7600 after run-ae-pc1-7600-energy-20261008
# (a timeout, a killed job tree or a lost relay must never leave the card capped). Unelevated; the card's ordinal from the
# tool's own tune line (the integrated Radeon's all-dash line is not a card); --reset, then a tune-line read-back.
$ErrorActionPreference = 'Continue'
$install = Join-Path $env:LOCALAPPDATA 'Programs\Igneum Miner'
$tool = $null; $cands = @()
$jobsDir = Join-Path $env:LOCALAPPDATA 'igneum\app\jobs'
if (Test-Path -LiteralPath $jobsDir) { $cands += @(Get-ChildItem -LiteralPath $jobsDir -Recurse -Filter 'igneum-gpu-telemetry*.exe' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | ForEach-Object { $_.FullName }) }
$cands += Join-Path $install 'igneum-gpu-telemetry.exe'
foreach ($c in $cands) { if (-not (Test-Path -LiteralPath $c)) { continue }; $pr = @(& $c --tune 2>&1 | ForEach-Object { "$_" }) | Where-Object { $_ -match '^tune \d+ ' }; if ($pr) { $tool = $c; break } }
if (-not $tool) { 'RESULT error no telemetry tool with a tune line'; 'SUMMARY {"job":"pc1-7600-reset","status":"failed"}'; exit 2 }
$line = @(& $tool --tune 2>&1 | ForEach-Object { "$_" }) | Where-Object { $_ -match '^tune (\d+) name "([^"]*7600[^"]*)" .* ok\s*$' } | Select-Object -First 1
if (-not $line) { 'RESULT error no RX 7600 tune line'; 'SUMMARY {"job":"pc1-7600-reset","status":"failed"}'; exit 2 }
$ord = [int]([regex]::Match($line, '^tune (\d+)').Groups[1].Value)
"RESULT before $line"
$r = @(& $tool --card $ord --reset 2>&1 | ForEach-Object { "$_" }) | Where-Object { $_ -match '^tune ' } | Select-Object -First 1
"RESULT reset $r"
Start-Sleep -Seconds 2
$after = @(& $tool --tune 2>&1 | ForEach-Object { "$_" }) | Where-Object { $_ -match ('^tune ' + $ord + ' ') } | Select-Object -First 1
"RESULT after $after"
$ok = ($after -match ' gmax 0 ') -and ($after -match ' plimit 0 ')
"SUMMARY {""job"":""pc1-7600-reset"",""status"":""$(if ($ok) { 'done' } else { 'check' })"",""ordinal"":$ord}"
exit 0

View file

@ -0,0 +1,70 @@
# AMD-and-Intel energy lane (Igneum 2.0 register: GPU-03's Intel cell, ECO-05's owed B580 watts), 8 October 2026.
# The second Windows rig's Intel Arc B580, measure only, unelevated, no knob written (Intel has none in our tools): the class v6 kit's own
# igneum-worker-opencl.exe is the load (--bench-pack; the app is never quit, paused or resumed, no api/cards), and
# ze-power.exe (fetch job fetch-ae-ze-power-20261008, sha256 354abddd...; Level Zero sysman energy counters through the
# driver's ze_loader.dll, read only) samples every 5 s beside it. First: --memprobe at 256 and 1024 MiB (the dependent
# random-read ceiling; two sizes to see whether the rate falls with the footprint beyond the caches, a TLB-reach sign).
# The sampler is a child process whose pid is written to this job's folder and stopped by that pid only.
$ErrorActionPreference = 'Continue'
$env:IGNEUM_V6_KIT_ID = 'fetch-class-v6-kit-20261008'
$jobName = 'pc2-b580-energy'
$started = Get-Date
function Stamp { (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') }
function Summary([string] $status, [hashtable] $extra) {
$o = [ordered]@{ job = $jobName; status = $status; duration_s = [int]((Get-Date) - $started).TotalSeconds; finished_at = (Stamp) }
foreach ($k in $extra.Keys) { $o[$k] = $extra[$k] }
'SUMMARY ' + ($o | ConvertTo-Json -Compress -Depth 5)
}
"RESULT start $(Stamp) job=$jobName machine=$env:COMPUTERNAME app_version=$env:IGNEUM_APP_VERSION"
$jobs = Split-Path $env:IGNEUM_JOB_DIR
$ze = Join-Path (Join-Path $jobs 'fetch-ae-ze-power-20261008') 'ze-power.exe'
if (-not (Test-Path $ze)) { $ze = @(Get-ChildItem -LiteralPath $jobs -Recurse -Filter 'ze-power.exe' -ErrorAction SilentlyContinue | Select-Object -First 1 | ForEach-Object { $_.FullName }) | Select-Object -First 1 }
if (-not $ze -or -not (Test-Path $ze)) { "RESULT error ze-power.exe missing (fetch job fetch-ae-ze-power-20261008 first)"; Summary 'failed' @{ error = 'no sampler' }; exit 2 }
"RESULT sampler $ze sha256 $((Get-FileHash -Algorithm SHA256 $ze).Hash.ToLower())"
@(& $ze 2>&1 | ForEach-Object { "$_" }) | ForEach-Object { "RESULT ze_idle $_" }
# the kit: IGNEUM_V6_KIT_ID, else the newest job folder holding the worker and packs\hl-v6-foldrw
$kit = $null
if ($env:IGNEUM_V6_KIT_ID) { $kit = Join-Path $jobs $env:IGNEUM_V6_KIT_ID }
if (-not $kit -or -not (Test-Path $kit)) { $kit = @(Get-ChildItem -LiteralPath $jobs -Directory -ErrorAction SilentlyContinue | Where-Object { (Test-Path (Join-Path $_.FullName 'packs\hl-v6-foldrw\kernel_bound.cl')) -and (Test-Path (Join-Path $_.FullName 'bin\windows\igneum-worker-opencl.exe')) } | Sort-Object LastWriteTime -Descending | ForEach-Object { $_.FullName }) | Select-Object -First 1 }
if (-not $kit) { "RESULT error no class v6 kit on this PC"; Summary 'failed' @{ error = 'no kit' }; exit 2 }
$exe = Join-Path $kit 'bin\windows\igneum-worker-opencl.exe'; $packs = Join-Path $kit 'packs'
"RESULT worker $exe sha256 $((Get-FileHash -Algorithm SHA256 $exe).Hash.ToLower()) kit=$(Split-Path $kit -Leaf)"
$list = @(& $exe --list 2>&1 | ForEach-Object { "$_" }); $list | ForEach-Object { "RESULT list $_" }
$dev = $null; foreach ($l in $list) { if ($l -match '^\s*\[(\d+)\].*(B580|Arc|Battlemage)' -and $l -notmatch 'dup') { $dev = [int]$Matches[1]; break } }
if ($null -eq $dev) { "RESULT error no Arc B580 in --list"; Summary 'failed' @{ error = 'no B580' }; exit 2 }
$w = @(Get-CimInstance Win32_Process -Filter "Name = 'igneum-worker-opencl.exe'" -ErrorAction SilentlyContinue | ForEach-Object { "$($_.ProcessId):[$($_.CommandLine -replace '\s+', ' ')]" })
$loaded = ($w | Where-Object { $_ -match "--device\s+$dev(\s|$)" }).Count -gt 0
"RESULT device $dev card_state=$(if ($loaded) { 'loaded' } else { 'quiet' }) workers=[$($w -join ' ')]"
foreach ($mib in @(256, 1024)) { @(& $exe --memprobe --probe-mib $mib --device $dev 2>&1 | ForEach-Object { "$_" }) | ForEach-Object { "RESULT memprobe $mib $_" } }
$pidFile = Join-Path $env:IGNEUM_JOB_DIR 'ze.pid'
$rows = @()
foreach ($pk in @('hl-v6-foldrw', 'hl-v6-all')) {
$d = Join-Path $packs $pk
if (-not (Test-Path (Join-Path $d 'kernel_bound.cl'))) { "RESULT error pack $pk missing"; continue }
$nb = if ($pk -eq 'hl-v6-all') { 30 } else { 60 }
$samp = Join-Path $env:IGNEUM_JOB_DIR ("ze-$pk.txt")
$sp = Start-Process -FilePath $ze -ArgumentList @('-l', '5') -RedirectStandardOutput $samp -WindowStyle Hidden -PassThru
Set-Content -LiteralPath $pidFile -Value $sp.Id
$t0 = Get-Date
$out = @(& $exe --bench-pack --pack $d --device $dev --batch-log2 24 --batches $nb 2>&1 | ForEach-Object { "$_" })
$t1 = Get-Date
$zpid = [int](Get-Content -LiteralPath $pidFile); Stop-Process -Id $zpid -Force -ErrorAction SilentlyContinue; Remove-Item -LiteralPath $pidFile -ErrorAction SilentlyContinue
foreach ($l in $out) { if ($l -match '^(pack |class |RESULT |FAIL|error|warm-up|kernel|exchange)') { "RESULT bench $pk out $l" } }
$res = $out | Where-Object { $_ -match '^RESULT ' } | Select-Object -Last 1
$fp = ''; $mhs = 0; $check = ''
if ($res -match 'fingerprint=([0-9a-f]{16})') { $fp = $Matches[1] }
if ($res -match 'mhs=([0-9.]+)') { $mhs = [double]$Matches[1] }
if ($res -match 'check=(\w+)') { $check = $Matches[1] }
# ze lines arrive every 5 s; the lines after the first 20 s of the bench (warm-up and dataset build excluded): the sampler
# started with the bench, so the first four sample rounds are dropped
$lines = @(); if (Test-Path -LiteralPath $samp) { $lines = @(Get-Content -LiteralPath $samp) }
$lines | Select-Object -First 6 | ForEach-Object { "RESULT ze $pk $_" }
$dom = @{}
$round = @{}
foreach ($l in $lines) { if ($l -match '^intel (\d+) dom (\d+) card (\d) watts ([\d.]+)') { $k = "$($Matches[1])/$($Matches[2])/card$($Matches[3])"; if (-not $round.ContainsKey($k)) { $round[$k] = 0 }; $round[$k]++; if ($round[$k] -gt 4) { if (-not $dom.ContainsKey($k)) { $dom[$k] = @() }; $dom[$k] += [double]$Matches[4] } } }
$dm = [ordered]@{}
foreach ($k in $dom.Keys) { $dm[$k] = [math]::Round((($dom[$k] | Measure-Object -Average).Average), 1); "RESULT ROW card=ArcB580 pack=$pk domain=$k mhs=$mhs watts=$($dm[$k]) uj_per_hash=$(if ($mhs -gt 0) { [math]::Round($dm[$k] / $mhs, 3) } else { 0 }) samples=$($dom[$k].Count) seconds=$([int]($t1 - $t0).TotalSeconds) fingerprint=$fp check=$check $(Stamp)" }
$rows += [ordered]@{ pack = $pk; mhs = $mhs; watts = $dm; fingerprint = $fp; check = $check }
}
Summary $(if ($rows.Count) { 'done' } else { 'failed' }) @{ rows = $rows; device = $dev }
exit $(if ($rows.Count) { 0 } else { 1 })

View file

@ -0,0 +1,98 @@
/* ze-power: Intel GPU power from the Level Zero sysman energy counters (AMD-and-Intel energy lane, 8 October 2026).
* Windows: loads ze_loader.dll (installed by the Intel graphics driver) at run time; no SDK, no headers.
* ze-power.exe one reading: two energy-counter reads 1 s apart per power domain
* ze-power.exe -l N a line every N seconds until killed
* Line: intel <dev> dom <k> card <0|1> watts <W> energy_uj <E> ts_us <T> name "<first printable strings>"
* The counter is the device's own (package or card domain, as the driver exposes it), not the wall. Read only:
* no set call exists in this program. */
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <windows.h>
typedef int32_t zr;
typedef void *H;
typedef struct { uint64_t energy; uint64_t timestamp; } ecnt;
typedef zr (__cdecl *f_init)(uint32_t);
typedef zr (__cdecl *f_get)(uint32_t *, H *);
typedef zr (__cdecl *f_get2)(H, uint32_t *, H *);
typedef zr (__cdecl *f_card)(H, H *);
typedef zr (__cdecl *f_cnt)(H, ecnt *);
typedef zr (__cdecl *f_props)(H, void *);
#define MAXD 8
#define MAXP 8
static void names(H dev, f_props gp, char *out, size_t cap) {
out[0] = 0;
if (!gp) return;
static unsigned char buf[8192];
memset(buf, 0, sizeof buf);
*(uint32_t *)(buf + 0) = 0x1; /* ZES_STRUCTURE_TYPE_DEVICE_PROPERTIES */
*(uint32_t *)(buf + 16) = 0x3; /* core: ZE_STRUCTURE_TYPE_DEVICE_PROPERTIES */
if (gp(dev, buf) != 0) return;
size_t o = 0; int run = 0; size_t start = 0;
for (size_t i = 0; i < sizeof buf && o + 2 < cap; i++) {
unsigned char c = buf[i];
if (c >= 32 && c < 127) { if (!run) { start = i; run = 1; } }
else { if (run && i - start >= 4) { size_t n = i - start; if (o + n + 2 >= cap) break; if (o) out[o++] = '|'; memcpy(out + o, buf + start, n); o += n; } run = 0; }
}
out[o] = 0;
}
int main(int argc, char **argv) {
int loop = 0;
if (argc >= 3 && strcmp(argv[1], "-l") == 0) loop = atoi(argv[2]);
HMODULE m = LoadLibraryA("ze_loader.dll");
if (!m) { printf("error no ze_loader.dll (%lu)\n", GetLastError()); return 2; }
f_init zesInit = (f_init)GetProcAddress(m, "zesInit");
f_get zesDriverGet = (f_get)GetProcAddress(m, "zesDriverGet");
f_get2 zesDeviceGet = (f_get2)GetProcAddress(m, "zesDeviceGet");
f_get2 enumPwr = (f_get2)GetProcAddress(m, "zesDeviceEnumPowerDomains");
f_card cardPwr = (f_card)GetProcAddress(m, "zesDeviceGetCardPowerDomain");
f_cnt getE = (f_cnt)GetProcAddress(m, "zesPowerGetEnergyCounter");
f_props gp = (f_props)GetProcAddress(m, "zesDeviceGetProperties");
f_init zeInit = (f_init)GetProcAddress(m, "zeInit");
f_get zeDriverGet = (f_get)GetProcAddress(m, "zeDriverGet");
f_get2 zeDeviceGet = (f_get2)GetProcAddress(m, "zeDeviceGet");
if (!enumPwr || !getE) { printf("error the loader has no sysman power entry points\n"); return 2; }
H drv[MAXD], dev[MAXD * 4]; uint32_t nd = 0, ndev = 0; const char *path = "zesInit";
zr r = zesInit ? zesInit(0) : -1;
if (r == 0 && zesDriverGet && zesDeviceGet) {
uint32_t c = MAXD; if (zesDriverGet(&c, drv) == 0) nd = c;
for (uint32_t i = 0; i < nd; i++) { uint32_t k = MAXD; if (zesDeviceGet(drv[i], &k, dev + ndev) == 0) ndev += k; }
}
if (ndev == 0 && zeInit && zeDriverGet && zeDeviceGet) {
path = "zeInit+ZES_ENABLE_SYSMAN"; SetEnvironmentVariableA("ZES_ENABLE_SYSMAN", "1");
r = zeInit(0);
uint32_t c = MAXD; if (r == 0 && zeDriverGet(&c, drv) == 0) nd = c;
for (uint32_t i = 0; i < nd; i++) { uint32_t k = MAXD; if (zeDeviceGet(drv[i], &k, dev + ndev) == 0) ndev += k; }
}
printf("info path %s init %d drivers %u devices %u\n", path, (int)r, nd, ndev);
if (ndev == 0) { printf("error no sysman device\n"); return 3; }
H pw[MAXD * 4][MAXP + 1]; uint32_t np[MAXD * 4]; int iscard[MAXD * 4][MAXP + 1]; char nm[MAXD * 4][256];
for (uint32_t d = 0; d < ndev; d++) {
names(dev[d], gp, nm[d], sizeof nm[d]);
uint32_t k = MAXP; np[d] = 0;
if (enumPwr(dev[d], &k, pw[d]) == 0) np[d] = k;
for (uint32_t j = 0; j < np[d]; j++) iscard[d][j] = 0;
H c = 0;
if (cardPwr && cardPwr(dev[d], &c) == 0 && c) { pw[d][np[d]] = c; iscard[d][np[d]] = 1; np[d]++; }
printf("info dev %u domains %u name \"%s\"\n", d, np[d], nm[d]);
}
ecnt prev[MAXD * 4][MAXP + 1];
for (uint32_t d = 0; d < ndev; d++) for (uint32_t j = 0; j < np[d]; j++) { memset(&prev[d][j], 0, sizeof(ecnt)); getE(pw[d][j], &prev[d][j]); }
int every = loop > 0 ? loop : 1;
for (;;) {
Sleep(every * 1000);
for (uint32_t d = 0; d < ndev; d++) for (uint32_t j = 0; j < np[d]; j++) {
ecnt e; memset(&e, 0, sizeof e);
zr q = getE(pw[d][j], &e);
double dt = (double)(e.timestamp - prev[d][j].timestamp) * 1e-6, de = (double)(e.energy - prev[d][j].energy) * 1e-6;
double w = dt > 0 ? de / dt : 0;
printf("intel %u dom %u card %d watts %.2f energy_uj %llu ts_us %llu rc %d name \"%s\"\n", d, j, iscard[d][j], w,
(unsigned long long)e.energy, (unsigned long long)e.timestamp, (int)q, nm[d]);
prev[d][j] = e;
}
fflush(stdout);
if (loop <= 0) break;
}
return 0;
}

View file

@ -0,0 +1,40 @@
# The proving outcome ledger (review B F08)
8 October 2026, the enforced proving lane. Review B, finding F08: the proving pipeline reported waste and deadline censoring, never sustained capacity; "add an outcome ledger for every eligible job: completed, active, expired or explicitly cancelled, then report paid completions, missed deadlines and wasted work by cause".
## What an eligible job is
A segment a prover claimed (`RESULT claim` in `tools/fleet/box-prover.py`): every block of the segment present in its worklist, every shard open, unpaid and absent from its pool, the deadline (last block's DAA plus the unproven window) at least the margin past the tip. A segment nobody claimed is not a job; the chain's own view of those (pending, unproven, paid per segment) is `igneum_getProvingStatus` and stays as it was.
## The one outcome per job
| Outcome | When | Cause field |
|---|---|---|
| active | claimed and not yet closed: in export, cut or chain; submitted and waiting for a carrier; held for a retry | none |
| paid | a carrying block paid the segment record (`RESULT paid`) | none |
| expired | the deadline passed with no paid record | `unpaid` (submitted, never carried in time), `held_expired` (a held record past its deadline), `never_submitted` (log reconstruction only: the log ran 50 DAA past the deadline with no submit) |
| cancelled | this prover gave the job up | `disk`, `export`, `cut`, `chain`, `timeout`, `shards`, `statement`, `sign`, `refused` |
A job closes once; a second close is ignored. Each segment row in `prover-state.json` carries `outcome`, `cause`, `deadline`, `margin_daa` (at the claim), `spent_s` (export, cut and chain seconds), `wasted_s` (the same unless paid), `miss_daa` (an expiry's distance past the deadline at the close), `closed_at`. The state carries `outcomes` (paid, active, expired, cancelled), `wasted_s` by cause and `deadline_misses`. Every close is a `RESULT outcome` line and the run ends with a `RESULT ledger` line naming the still-active segments.
## The report
`tools/fleet/prover-outcomes.py --state <prover-state.json> ... --log <prover.log> ... [--json]` merges the fleet's state files (a state row wins over the same segment in a log) and reconstructs the ledger for a prover from before this change from its RESULT lines (claim, chain, shards, statement, sign, segment_refused, held, submitted, paid, unpaid, held_expired). It prints:
- Outcome ledger: jobs by outcome.
- Paid completions: segments, shards, IGN, median end to end, median time from submit to pay, median margin at the claim.
- Missed deadlines: count by cause, median miss in DAA (never negative; none when the log's last tip is stale), median margin at the claim, the seconds spent on them.
- Wasted work by cause: jobs and seconds per cause, against the seconds spent in all.
- Throughput over the covered span: segments paid per hour, shards paid per hour, the paid share of the seconds spent.
- Active: the open jobs, with whether each is submitted.
`--self-test` runs a synthetic log (six jobs: paid, timeout, unpaid, shards, held_expired, active) and four old-shape state rows to known numbers; the gate runs it (`tools/ci/pre-push.sh`). `tools/fleet/night.py` sums the fleet's counters into its hourly row (`outcomes`, `deadline_misses`, `wasted_s`), which `page.py` publishes.
## Reading it
The question the finding asks is answered by three ratios the report prints: paid jobs over claimed jobs, the paid share of the seconds spent, and the median margin at the claim for paid against expired jobs. A pipeline whose expired jobs claimed with a margin close to the floor (240 DAA) and whose paid jobs claimed wider is sizing its jobs too close to the deadline; the fix is the margin, never a longer exclusive window (the finding's warning: a longer window is tested against slow or malicious claimants before it moves). A pipeline whose waste sits under `chain` or `timeout` has a prover problem, under `unpaid` a carrier problem (records accepted and never carried in time), under `refused` a chaining problem (fresh records refused while the previous segment waits).
## Not in this change
- The chain-side ledger (every segment the chain planned, claimed or not, with its pending, unproven and paid state per claimant) stays on the node's RPC as it is; a per-segment outcome feed from the observer is the next step if the fleet's view and the chain's view disagree.
- Job sizing, resumable verified work, early cancellation and bounded assignment protection (the finding's second paragraph) are design items for the pool and prover lanes; this ledger is the instrument they read.

View file

@ -48,6 +48,12 @@ Measured on the test EVM: a 29-voter table costs about 5.3 million gas to instal
verifies in about 3.9 million gas (the pairing and the two map-to-curve calls dominate; a G1 addition per signer is
375 gas). On Sepolia at a 1 gwei tip that is under 0.01 ETH per certificate.
Recovery locks (finality rule v4's majority-continuity recovery after an empty window, review B item F04) are not a
kind the contract knows: the certificate bytes carry no lock label, so the verifier reads only weight against the
installed table and accepts the final rule alone. A certificate signed by over half but under two thirds of the
table returns `ok = false` and `submitCertificate` reverts: a recovery lock is never recorded as final, and a
consumer that needs recovery locks must carry its own state, since nothing here tells them apart.
## What the account proof proves
`verifyAccount(stateRoot, account, proof)` walks an Ethereum account proof (the `eth_getProof` shape: RLP nodes from

File diff suppressed because one or more lines are too long

View file

@ -6,8 +6,8 @@ The fixture every case of the Test and Acceptance Standard (docs/plans/igneum-2.
| Field | Value | Read from | Owner |
|---|---|---|---|
| Miner cut tip (release-2.0.1) | 2826f37e (2ea7b43f + the DMG README line + the node pin ef0f2ed8 + elf/prior from key-succession-pin; the app crate byte-identical to 2ea7b43f's, so the pow and app cells read on 2ea7b43f cover it by content; a re-run on 2826f37e records the literal) | the shipper's line 19:3x | shipper (ae892a8b0f78fe31c) |
| Node sha for the roll | ef0f2ed8 = 291ee6ae (key succession over 417c4a57) + the workspace version 2.0.1 | the node lane's line 19:0x | node lane (a283f5f0d364ceef0) |
| Miner cut tip (release-2.0.1) | aa0e0f45 (9c844503 + the pin file; 9c844503 = 2826f37e + the six-target packaged peer list; the app crate byte-identical to 2ea7b43f's throughout, so the pow and app cells read on 9c844503 cover it by content) | the shipper's line 19:5x | shipper (ae892a8b0f78fe31c) |
| Node sha for the roll | 7cfa422a = ef0f2ed8 (291ee6ae + the 2.0.1 version) + the miner base-unit fix, amended (777214af did not compile: a self-recursive connect, caught by the steward's read at 19:50) | the node lane's and shipper's lines 19:5x | node lane (a283f5f0d364ceef0) |
| Node line read green tonight | 4cdcc488, d5981514, bee41b5e, 9fc9f42a, 5713d547, 417c4a57, 291ee6ae (ef0f2ed8 = 291ee6ae + the version bump, its own read on build-4 recorded as the literal) | the steward's matrices on build-2 and build-4 | CI steward |
| Network | igneum-devnet-4, a fresh genesis; EVM chain id 4465 (0x1171), set in devnet4_params, read by the canaries as eth_chainId on every candidate tonight, pinned by the digest be5f4068; every 2.0 devnet node, pool and reference app signs with it; mainnet's and the testnet's ids unchanged from the 0.3 line | the node lane's line 19:3x | node lane |
| Object digest | be5f406802cec1227a5ef50d42181ab42444f79af170775b22c85cb9a917273b (4cdcc488 and every sha after it; no live digest move since) | the node lane's lines | node lane |
@ -37,6 +37,13 @@ The fixture every case of the Test and Acceptance Standard (docs/plans/igneum-2.
| Trust anchors (light client, oracle) | BLOCKED | reference-apps lane | VER-01 and VER-04 read BLOCKED |
| The class v6 freeze line in packaging/pow-freeze.txt | open: three D1 candidates on the node mirrors (class-v6-node 3af510ec on c245d50b9 fingerprint a65e4c5a; class-v6-node-b 46e7ac18 with the acceptance fix, fingerprint 6cdd922a; class-v6-node-review 2f6eb9e9 on 04442d9ca, fingerprint 7a1dec1c, six suites green on build-1 at 19:3x) | hash lane, node lane | class v5 stays the mining class |
## Resolved by the founder (8 October 2026, 19:57 UK, through the coordinator)
| Field | Ruling |
|---|---|
| F04 (Review B), the recovery lock | kept, and always labelled "recovery", never "final", on every surface (the checkpoint field, the explorer, receipts, the light client, the oracle, the site) |
| F14 (Review B), fleet control and the public client | the public miner ships from 2.0.2 without remote jobs; our own fleet runs the lab build with its own signing root |
## Signatures (by 23:30 UK, 8 October 2026)
| Role | Name or lane | Commit signed | Time |

View file

@ -0,0 +1,9 @@
# IGNEUM 2.0, Complete Master Edition (8 October 2026)
THE reference from 20:13 UK on 8 October 2026. It consolidates, in one 292-page document, the strategy text, the 37-page plan, the Test and Acceptance Standard (128 cases, 17 profiles), the additional closure requirements (18 INT integration gates and 44 R2 closure requirements, which overlap the 128 cases and are not 190 independent tests), and all three external reviews: R0 (the historical algorithm review), R1 (the full-system review, findings I01 to I10 and V6-01 to V6-11) and R2 (the updated-stack review, F01 to F14). Where the master and the earlier plan PDF or RTF differ, the master governs; it preserves them and preserves differing interpretations (REC-01, finality healing) without choosing.
Reading order (page numbers of the PDF): master introduction 1 to 18; strategy 19 to 55; test standard 56 to 128; closure requirements 129 to 139; R2 140 to 198; R1 199 to 270; R0 271 to 288; evidence inventory 289 to 292.
Files: igneum-2.0-complete-master.pdf (the light edition; the obsidian edition has the same substance), igneum-2.0-complete-master.txt (text extraction), traceability.json (the master traceability register: the original registry, the INT gates, the R2 closure requirements, the crosswalk, the unresolved interpretations, the claim), evidence/ (the companion's manifest, README, the R1 review with its results and harness, the R0 evidence). The six reviewed source archives are our own code snapshots of 8 October and are not duplicated here; their SHA-256 values are in evidence/MANIFEST.json.
The claim the master makes: "A complete, independently substantiated technical/economic/operational/commercial pass supports a credible leadership-contender assessment, not a numerical rank certificate." Status: compilation only; no new technical tests were run for this edition. Missing or unrun evidence is not PASS; a defect reproduced by a probe is not a passed gate.

View file

@ -0,0 +1,345 @@
{
"review": "Igneum v6 five-package source and integration review",
"date": "2026-10-08",
"native_rust_gpu_sp1_or_fullnode_executed": false,
"baseline_checks": {
"scope": "2026-10-08 uploaded archives; no native Rust/GPU/node execution",
"source_guards": {
"accept_shape_flag_omission": true,
"eight_register_acceptance": true,
"unscheduled_acceptance": true,
"counter_uses_unscheduled_instructions": true,
"confirm_requires_efficiency_gain": true,
"address_chain_excludes_source": true
},
"source_sha256": {
"pow/src/accept.rs": "a4baf9286508e73406dd05b9f89bc6660baf017f107c650ce09bda504cfc92f9",
"pow/src/generator.rs": "751f39aeedb2987856343e2a67c7aeca7daa47ddafe5192c50a19b1d425e3a8a",
"pow/src/verify.rs": "033ae9f2ccd32e1170e7ff4f259b206b26e78ddc9ebf68d7b26933794aa1f345",
"ember/src/ember.rs": "e1e9d83e37b3acb47e89027994dba1638d7c4c547a0d1f587b95455cbe3c4873",
"ember/src/powertask.rs": "ab7cefacdc0758cbf0d192cc464449a7b51ec6a6d0a614e43c1844538108ccd6",
"proof/core/src/executor.rs": "1db37a599b24d8f1a1ba3bae05b915fbcd1fa3134eb7a5a825c1e3aeb43cf62d",
"proof/elf/manifest.json": "dfbf66a2c8426e27b27fd8915325619e21f3d44478a7ed2e8d2d04ca9359df7c"
},
"manifest_integrity": [
{
"role": "shard",
"kind": "elf",
"file": "igneum-prove-program.elf",
"bytes": 2832504,
"sha256": "150f4c05a2951fc56174a87089707a030b18df8fbe7e053a66459edb83053083",
"manifest_match": true,
"declared_size_match": true
},
{
"role": "shard",
"kind": "vk",
"file": "igneum-prove-program.vk",
"bytes": 104,
"sha256": "8b4da5bff86d963f4210a78e5d800a1cd00ab41b158f6962f4ac009edc249d4c",
"manifest_match": true,
"declared_size_match": null
},
{
"role": "aggregator",
"kind": "elf",
"file": "igneum-prove-aggregator.elf",
"bytes": 319744,
"sha256": "143d9c243dd12e87e90be71f6b8cd42353e513bf8ce78903ef6f972f1bc9aa7b",
"manifest_match": true,
"declared_size_match": true
},
{
"role": "aggregator",
"kind": "vk",
"file": "igneum-prove-aggregator.vk",
"bytes": 104,
"sha256": "ad17bc1ae5be816554dbb13cb5b4d242678adfb8e1a4f7247ceb8b5ba9001b9f",
"manifest_match": true,
"declared_size_match": null
}
],
"shape_predicate_transcription": [
{
"class": "v5",
"strengthened_shape": true,
"attempt_cap": 256,
"shape_gated_freshness_and_hot_index_checks": true,
"shape_gated_last_resort": true
},
{
"class": "v5_fold_rw",
"strengthened_shape": true,
"attempt_cap": 256,
"shape_gated_freshness_and_hot_index_checks": true,
"shape_gated_last_resort": true
},
{
"class": "v6_window",
"strengthened_shape": false,
"attempt_cap": 32,
"shape_gated_freshness_and_hot_index_checks": false,
"shape_gated_last_resort": false
},
{
"class": "v6_full_chain_fold_rw",
"strengthened_shape": false,
"attempt_cap": 32,
"shape_gated_freshness_and_hot_index_checks": false,
"shape_gated_last_resort": false
}
],
"load_counter_arithmetic": {
"base_loads_per_iteration": 16,
"iterations": 8,
"reported_loads_per_hash": 128,
"scheduled_reg64_loads_per_hash": 256,
"note": "Derived from generic counters versus scheduled() for a 16-load reg64 class, not measured memory transactions. The nested reg64 metadata separately reports the schedule correctly."
},
"ember_confirm_transcription": [
{
"name": "large_rate_deficit",
"tolerance_pct": 1.0,
"prior": {
"point": 1300,
"mhs": 50,
"watts": 80,
"ok": true
},
"neighbour": {
"point": 1500,
"mhs": 100,
"watts": 180,
"ok": true
},
"chosen_point": 1500,
"confirm_verdict": "Keep",
"prior_within_rate_floor": false,
"policy_violation": true
},
{
"name": "modest_rate_deficit",
"tolerance_pct": 1.0,
"prior": {
"point": 1300,
"mhs": 98,
"watts": 100,
"ok": true
},
"neighbour": {
"point": 1500,
"mhs": 100,
"watts": 103,
"ok": true
},
"chosen_point": 1500,
"confirm_verdict": "Keep",
"prior_within_rate_floor": false,
"policy_violation": true
},
{
"name": "legitimate_efficiency_gain",
"tolerance_pct": 1.0,
"prior": {
"point": 1300,
"mhs": 100,
"watts": 180,
"ok": true
},
"neighbour": {
"point": 1500,
"mhs": 100,
"watts": 160,
"ok": true
},
"chosen_point": 1500,
"confirm_verdict": "FullDue",
"prior_within_rate_floor": true,
"policy_violation": false
},
{
"name": "prior_within_policy",
"tolerance_pct": 1.0,
"prior": {
"point": 1300,
"mhs": 99.5,
"watts": 90,
"ok": true
},
"neighbour": {
"point": 1500,
"mhs": 100,
"watts": 103,
"ok": true
},
"chosen_point": 1300,
"confirm_verdict": "Keep",
"prior_within_rate_floor": true,
"policy_violation": false
},
{
"name": "max_rate_goal",
"tolerance_pct": 0.0,
"prior": {
"point": 1300,
"mhs": 99,
"watts": 90,
"ok": true
},
"neighbour": {
"point": 1500,
"mhs": 100,
"watts": 103,
"ok": true
},
"chosen_point": 1500,
"confirm_verdict": "Keep",
"prior_within_rate_floor": false,
"policy_violation": true
}
],
"address_fold_algebra": {
"static_queries": 16384,
"queries_after_state_updates": 16384,
"mismatches": 0,
"seed": "0x1a6e0026",
"scope": "Exact subexpression equivalence only. Not a whole-hash run, GPU benchmark, exploit, reduction in state information, or ASIC cost result.",
"identity": "u[k]=ROTL32(r[k],63-k); P=prefix_xor(u,a); T=xor(u); address(a)=r[a]^ROTR32(P,1)^T^P^u[a]"
},
"supplied_js_tier_tests": {
"returncode": 0,
"tests": "10",
"pass": "10",
"output": "TAP version 13\n# Subtest: the shipped table validates with no faults\nok 1 - the shipped table validates with no faults\n ---\n duration_ms: 3.341691\n type: 'test'\n ...\n# Subtest: the measured rows are the record's (the 5090 at its 1,300 MHz knee, the 5080 at 1,100, the 4070 at its tune, the 9070 XT grid, the M5 Max meter)\nok 2 - the measured rows are the record's (the 5090 at its 1,300 MHz knee, the 5080 at 1,100, the 4070 at its tune, the 9070 XT grid, the M5 Max meter)\n ---\n duration_ms: 0.701674\n type: 'test'\n ...\n# Subtest: every entry carries the three tiers where the card has a lever, and only max where it has none\nok 3 - every entry carries the three tiers where the card has a lever, and only max where it has none\n ---\n duration_ms: 1.564993\n type: 'test'\n ...\n# Subtest: the match rule takes the longer name: a 5070 Ti is not a 5070, a 4060 Ti is not a 4060, a 9060 XT is not a 9070 XT\nok 4 - the match rule takes the longer name: a 5070 Ti is not a 5070, a 4060 Ti is not a 4060, a 9060 XT is not a 9070 XT\n ---\n duration_ms: 0.615752\n type: 'test'\n ...\n# Subtest: a class flip reads stale exactly as src/ember.rs tiers_stale does\nok 5 - a class flip reads stale exactly as src/ember.rs tiers_stale does\n ---\n duration_ms: 0.454489\n type: 'test'\n ...\n# Subtest: known-failed: a power rung under 50 is refused\nok 6 - known-failed: a power rung under 50 is refused\n ---\n duration_ms: 0.861034\n type: 'test'\n ...\n# Subtest: known-failed: a row missing a field tier_from_json reads is refused\nok 7 - known-failed: a row missing a field tier_from_json reads is refused\n ---\n duration_ms: 1.304932\n type: 'test'\n ...\n# Subtest: known-failed: a tuned row dearer per hash than stock is refused, and a max tier that is not stock\nok 8 - known-failed: a tuned row dearer per hash than stock is refused, and a max tier that is not stock\n ---\n duration_ms: 1.611869\n type: 'test'\n ...\n# Subtest: known-failed: a card class of the brief left out is refused, and a stale uj (not w over mhs) is refused\nok 9 - known-failed: a card class of the brief left out is refused, and a stale uj (not w over mhs) is refused\n ---\n duration_ms: 2.807709\n type: 'test'\n ...\n# Subtest: known-failed: a table under another class is refused\nok 10 - known-failed: a table under another class is refused\n ---\n duration_ms: 1.569598\n type: 'test'\n ...\n1..10\n# tests 10\n# suites 0\n# pass 10\n# fail 0\n# cancelled 0\n# skipped 0\n# todo 0\n# duration_ms 86.25061\n"
},
"supplied_ui_test_attempt": {
"returncode": 1,
"status": "BLOCKED_MISSING_SOURCE",
"missing": "ember/ui/app.js",
"output": "TAP version 13\n# node:fs:442\n# return binding.readFileUtf8(path, stringToFlags(options.flag));\n# ^\n# Error: ENOENT: no such file or directory, open '/mnt/data/igneum_v6_review/igneum-ember-2026-10-08/igneum-ember/app/igneum-app/ui/app.js'\n# at readFileSync (node:fs:442:20)\n# at file:///mnt/data/igneum_v6_review/igneum-ember-2026-10-08/igneum-ember/app/igneum-app/ui/tune-line.test.mjs:9:13\n# at ModuleJob.run (node:internal/modules/esm/module_job:274:25)\n# at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:644:26)\n# at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:117:5) {\n# errno: -2,\n# code: 'ENOENT',\n# syscall: 'open',\n# path: '/mnt/data/igneum_v6_review/igneum-ember-2026-10-08/igneum-ember/app/igneum-app/ui/app.js'\n# }\n# Node.js v22.16.0\n# Subtest: /mnt/data/igneum_v6_review/ember/ui/tune-line.test.mjs\nnot ok 1 - /mnt/data/igneum_v6_review/ember/ui/tune-line.test.mjs\n ---\n duration_ms: 51.870567\n type: 'test'\n location: '/mnt/data/igneum_v6_review/ember/ui/tune-line.test.mjs:1:1'\n failureType: 'testCodeFailure'\n exitCode: 1\n signal: ~\n error: 'test failed'\n code: 'ERR_TEST_FAILURE'\n ...\n1..1\n# tests 1\n# suites 0\n# pass 0\n# fail 1\n# cancelled 0\n# skipped 0\n# todo 0\n# duration_ms 63.240523\n"
},
"not_executed": [
"Native Rust test suites",
"CUDA / Metal / OpenCL kernels",
"SP1 proof creation or cryptographic verification",
"Full-node integration",
"Physical hardware or ASIC measurement"
]
},
"integration_checks": {
"scope": "Source-guarded transcriptions and supplied C test; NOT native Rust, GPU, SP1 or node execution",
"source_guards": [
{
"path": "node/igneum/exec/src/proving.rs",
"sha256": "92302ee089fca720f2ee6ac0756c0054a01c995db4c997d358683a2a08f3401b"
},
{
"path": "node/consensus/src/processes/finality.rs",
"sha256": "6194a96a80e5ec5a4daeb63afa8fda55a8c7a2f959d5becab60ea733790f5bf4"
},
{
"path": "pool/src/payout.rs",
"sha256": "839619378b7e9b0b7eda22ce900aa2ada6410aa90e3d7f1f000a02f0b8449c1f"
},
{
"path": "pool/src/state.rs",
"sha256": "54835bcf498328fdfb111f9a8911378d622c7ff57a87ca5ec5b58f41fe6e019d"
},
{
"path": "app/src/prover.rs",
"sha256": "8dfd507d475ed2bd4a36385603c743d722009f092bda9113241f29b3c5e2d64c"
},
{
"path": "workers/proto-metal/main.swift",
"sha256": "0f909ca4d20345635765e03442b06c79f34bc8a2ec8bdac22a3e87f6d7e209b5"
},
{
"path": "workers/proto-cuda/nvrtc/worker.cpp",
"sha256": "faf4782ff0fbfdea594e4635200a75db9084f96c663053d7c074df8e5bebf92f"
}
],
"proof_cache_model": {
"initial_valid": "VERIFIED",
"warm_wrong_statement": "VERIFIED",
"cold_wrong_statement": "INVALID",
"warm_wrong_kind_same_accepted_id": "VERIFIED",
"cold_wrong_kind_same_accepted_id": "INVALID",
"after_negative_cache_valid_record": "INVALID",
"fresh_valid_record": "VERIFIED",
"limitation": "Toy valid-proof oracle, no cryptography or block/record construction. A native two-node proof fixture is required."
},
"anchored_finality_model": {
"before_window_100_percent": true,
"after_window_pause_mode_100_percent": false,
"after_window_recovery_mode_100_percent": true,
"recovery_50_percent": false,
"recovery_55_percent_left": true,
"recovery_55_percent_right": true,
"limitation": "Pure anchor predicate only. Both-55 example needs 10 percent equivocation; not a full GHOSTDAG simulation."
},
"pool_crash_order_model": {
"owed": 100,
"broadcast_transactions": [
{
"nonce": 0,
"amount": 100
},
{
"nonce": 1,
"amount": 100
}
],
"broadcast_total": 200,
"limitation": "Synthetic crash/RPC ledger schedule with ample pool funds; no real money, RPC, signature, or network used."
},
"shard_retry_model": {
"initially_eligible": true,
"eligible_after_transient_failure_same_session": false,
"limitation": "Shard branch only. Segment retry logic is separate and exists."
},
"memory_geometry_arithmetic": {
"declared_words": 1476395008,
"metal_log2_allocated_words": 1073741824,
"declared_bytes": 5905580032,
"metal_bytes": 4294967296,
"shortfall_bytes": 1610612736,
"two_dataset_bytes": 11811160064,
"two_pair_bytes_assuming_256_MiB_cache_each": 12348030976,
"limitation": "Arithmetic plus allocation-path inspection; not a GPU allocation or an observed out-of-bounds access."
},
"native_c_packfile_test": {
"status": "EXECUTED",
"assertions_passed": 45,
"exit_code": 0,
"optional_real_pack_arguments": false,
"scope": "supplied C99 pack metadata/seed/geometry/leaf unit tests; no GPU"
}
},
"archive_provenance": [
{
"filename": "igneum-ember-2026-10-08(1).zip",
"sha256": "c219211b49fccda65b151df230b10ded5971ac2a8c7d1e5847c0a68bffe4cf4a",
"size": 78946
},
{
"filename": "igneum-mining-workers-2026-10-08.zip",
"sha256": "808abcecf157a3716d1c72fa6e76c540bf5808af1c0e4dd0fd888576d546cade",
"size": 6090014
},
{
"filename": "igneum-node-dag-2026-10-08.zip",
"sha256": "ead2cf94092b7e27aab2e44d653b2d969cb2a52878ef12abf6e5d7a9e7421cff",
"size": 1519903
},
{
"filename": "igneum-proving-2026-10-08(1).zip",
"sha256": "9ccf4112e274f586392e8e4a4f98ece2e82b4990fe5f1ee89ae0ee0a151ce01e",
"size": 1612294
},
{
"filename": "igneum-v6-freeze-tree-2026-10-08(1).zip",
"sha256": "f448981b2ceeab2e59e8bbd137a1a13ea1c730ecd9db72b9a89bd2bbf5d85acb",
"size": 696492
}
]
}

View file

@ -0,0 +1,752 @@
# IGNEUM - Mining algorithm source review and reproducible evidence
**Date:** 8 October 2026
**Basis:** `igneum-mining-algorithm-2026-10-08.zip`
**Archive SHA-256:** `94edb290005ecb8379cc599da90c877c7e25562f727cc4291069b4fbc149c7c1`
## Executive assessment
There is concrete room to improve this snapshot, especially program-resource guarantees, acceptance coverage, cryptographic boundaries, artifact identity and launch safety. This review does not establish that Igneum has the best GPU algorithm, reaches a 1.5x specialised-hardware ceiling, or contains a profitable live-network exploit.
The supplied snapshot contains V2/V3/V4 paths, not the 64-register v6 described in the strategy discussion. Its actual production activation status is unknown from these files. Do not transfer old benchmark ratios to a modified candidate.
## Scope and execution boundary
The archive contains 13 source/document files (527,803 uncompressed bytes), including 11 Rust files, the algorithm specification and a README. It does not include Cargo.toml/Cargo.lock, the pack fixtures referenced by tests, the complete mining worker host, the node integration, raw GPU measurements, or adversarial RTL/physical-design reports.
Rust, Cargo and CUDA were unavailable in the review environment. No original Rust test binary was built; no GPU kernel, live-node test or ASIC benchmark was run. The executable work is an independent C/Python transcription of the selected source paths. It is a diagnostic model, not a replacement implementation.
The transcription matches the supplied genesis program id/op mix, two closed-form hashes, three rejection/attempt vectors, and all eight published header-bound memory-hard hash vectors. Those anchors increase confidence in the probes but do not prove complete equivalence for untested classes or hardware.
The model supports V2 and the V3 era draw/base instructions, the fixed V2 cache/mixer, and optional ALU shadows. It excludes the experimental scratch, hot-table and derive-class variants. It does not certify all of the approximately 9,769 source/document lines.
## Reproduced observations
| Probe | Observation | Limit |
|---|---|---|
| V2 lane communication | 909/1,000 accepted programs lack all five shuffle dimensions | Structural bound, no ASIC speedup measured |
| V3 lane communication | 887/1,000 accepted programs lack all five dimensions | One fixed era; not V4 with its shadow |
| Memory concentration | 312/2,048 hashes hit 0x0fffffff at one site, with zero-header binding | One accepted V2 program and site; not overall traffic |
| Shadow coverage | A deliberately mutated zeroing shadow is invisible to the acceptance report | Not a canonically generated program or chain exploit |
| Identity | Two eras share program id ef42100d5403c90d but compute different hashes | Separate expected-era checking can mitigate |
| Dispatch grouping | Starting a batch at 1 changes the modelled result for nonce 2 | Host code is absent; no physical GPU run |
| Naive rejection patch | 32/1,000 seeds exhaust 32 attempts | Hypothetical bad patch, not current behaviour |
All counts, seeds, vectors and detailed qualifications are in the accompanying JSON and runnable harness. CPU elapsed times in diagnostic logs are not mining-performance benchmarks.
## Findings and exact source locations
### A01 - The supplied snapshot is not the planned v6
The public class enum exposes V2, V3 and V4. Mutable per-lane state is eight 32-bit logical registers. V3 fixes reads to one 32-bit word (4 bytes); V4 adds a 256-instruction block repeated 27 times per iteration. These facts do not establish what is deployed or implemented elsewhere.
**Original source:** `igneum-pow-src/generator.rs:780-819`; `igneum-pow-src/generator.rs:850-858`; `igneum-pow-src/verify.rs:335-354`.
### A02 - Accepted short programs need not connect all 32 lanes
For XOR-shuffle masks drawn from 1,2,4,8,16, k distinct dimensions allow components of at most 2^k lanes. An independent census of 1,000 accepted V2 and 1,000 accepted V3 programs found full mask span in only 91 and 113 respectively. Missing dimensions establish a connectivity bound, not a measured ASIC speedup. Full span is necessary, not sufficient, for all-lane influence. The V3 run uses one fixed era; these are not V4-shadow results.
**Original source:** `igneum-pow-src/generator.rs:1230-1298`; `igneum-pow-src/accept.rs:276-304`; `igneum-pow-src/accept.rs:368-404`.
### A03 - Per-hash address diversity can conceal per-site concentration
An accepted V2 example, seed SHA256("igneum-review/139"), passes the transcribed acceptance checks. At iteration 5, instruction 51 (zero indexed), 312 of 2,048 sampled header-bound hashes read address 0x0fffffff when the exact V2 memory-hard dataset construction is used. Two additional initialisation contexts give 318 and 325 hits. This is one load site, not that fraction of all mining traffic, and it is not a demonstrated profitable caching attack. Proposed remedies are entropy-preserving state transitions and measured per-site/address-cache analysis.
**Original source:** `igneum-pow-src/accept.rs:289-317`; `igneum-pow-src/accept.rs:347-397`; `igneum-pow-src/generator.rs:124-147`.
### A04 - The acceptance interpreter omits the shadow that runtime executes
accept.rs runs only p.instrs; verify.rs also runs program.shadow. A deliberately mutated Program with a valid-operand, zeroing 256-instruction shadow still passes the base-only acceptance check in the model and produces 32 zero outputs. The fixture is NOT generated by the canonical generator and does NOT show that the network accepts attacker-supplied programs. It demonstrates that the acceptance report does not cover full shadow semantics. Either retire the excluded long-program path in the new class or test its complete execution.
**Original source:** `igneum-pow-src/accept.rs:178-207`; `igneum-pow-src/accept.rs:327-350`; `igneum-pow-src/verify.rs:370-395`.
### A05 - Header binding and final digest deserve independent cryptographic review
The seed/binding layer uses four salted FNV-1a runs with a final mixing step. The final result is a rotated-XOR fold into 64 bits, inserted into the top 64 bits of a 256-bit value. The target comparison is internally consistent: lane <= floor(T/2^192). This is not a demonstrated comparison bug, but it is not a 256-bit cryptographic final digest. Recommend reviewing a domain-separated cryptographic input/output envelope, including nonce and template binding, while keeping cheap inner operations where justified. Such a change needs new consensus versioning and benchmarks.
**Original source:** `igneum-pow-src/seed.rs:34-49`; `igneum-pow-src/bind.rs:47-83`; `igneum-pow-src/verify.rs:390-395`.
### A06 - Different V3 eras can have the same program_id
The V3 and base-rung V4 identity path excludes the era parameters. The independent model produces two V3 programs with id ef42100d5403c90d and different era-dependent hashes. This is structural aliasing, not a brute-force collision. packcheck can compare a separately supplied expected era, which mitigates that path when used. Its directory check reads metadata, not kernel content. Use distinct, full semantic program, dataset and work identities; validate or regenerate executable artifacts against trusted inputs.
**Original source:** `igneum-pow-src/generator.rs:1049-1065`; `igneum-pow-src/packcheck.rs:166-185`; `igneum-pow-src/packcheck.rs:215-229`; `igneum-pow-src/packcheck.rs:263-272`.
### A07 - CUDA dispatch needs explicit canonical-group constraints
The bound CUDA wrapper validates block shape but not 32-aligned baseNonce. The canonical verifier aligns nonce groups to a 32 boundary. CPU emulation of the kernel grouping gives nonce 2 = e5f5f4b14e9c87ae for a batch starting at 1, versus canonical 7342f96cbedb41d1. No GPU was run, and the omitted host may already prevent this. Add wrapper/host guards and tests for alignment, nonce-low rollover and refresh of nonce-high init words.
**Original source:** `igneum-pow-src/emit.rs:1219-1243`; `igneum-pow-src/emit.rs:1260-1270`; `igneum-pow-src/bind.rs:99-119`.
### A08 - An operation described as bijective is not always bijective
Mad permits src2 == dst. For src == 1, dst += src*dst is 2*dst modulo 2^32. Distinct old destinations 0 and 2^31 both map to 0. This contradicts the unconditional bijective-in-dst description of injects(), but is not alone a lottery exploit. Restrict aliases or revise the invariant; separately examine destructive operations and address-state entropy.
**Original source:** `igneum-pow-src/generator.rs:124-147`; `igneum-pow-src/generator.rs:1266-1282`; `igneum-pow-src/verify.rs:403-475`.
### A09 - A naive stronger rejection test can exhaust all candidates
In a hypothetical patch requiring all five XOR-shuffle dimensions while retaining the existing draw distribution and 32-attempt limit, 32 of 1,000 test seeds exhaust all attempts. This is NOT a failure observed under the unmodified acceptance rule. Construct essential invariants into the generator rather than naively adding a high-rejection check or using an unbounded consensus loop.
**Original source:** `igneum-pow-src/generator.rs:1377-1404`.
## Proposed next candidate - not a production patch
1. Freeze the actual intended source and activation manifest; establish what is v6 and what has been retired.
2. Construct live state and lane-connectivity properties by design; do not rely only on a random instruction count or statistical rejection.
3. Analyse per-site addresses across nonces and headers, whole-dataset working sets and caching/recomputation alternatives. A permutation cannot restore entropy already collapsed to one value.
4. Use a common instruction semantics core for acceptance, reference execution and instrumentation. Keep CPU verification costs bounded.
5. Review a domain-separated cryptographic envelope and full-width final result. Bind expensive work to the job/nonce before finalisation; a cheap final hash alone must not allow intermediate-work reuse.
6. Introduce explicit semantic identities and trusted artifact validation. Keep work identity distinct from reusable program/dataset identity.
7. Fix dispatch preconditions and test all backends for canonical results. Target filtering, compilation caching and asynchronous staging are candidate byte-preserving miner optimisations, subject to inspecting the missing host implementation.
8. Compare every candidate with tuned commodity GPUs and a redesigned multi-epoch adversary at complete-board cost. No speedup or resistance multiplier is forecast by this review.
Changes to program generation, binding, digest, operation semantics or dataset layout change the consensus function. They require a versioned transition, new vectors and compatibility tests. Host guards and byte-exact implementation optimisation should not silently change accepted hashes.
## Native follow-up acceptance tests
- Reproduce the exact corpus counts and vectors in the original Rust crate and independently check their source semantics.
- Add generated-program dependency/communication analysis and adversarial algebraic simplification, not just mask coverage.
- Census per-site address distributions over independent holdout seeds, multiple headers/nonces, actual memory-hard datasets and every proposed size; measure effective cache and bandwidth cost.
- Mutation-test the acceptance checker against all activated execution components. Keep canonical-generator tests separate from hostile fixtures.
- Check every semantic identity component for accidental omission and every pack input for substitution, stale state and mismatched compiled artifacts.
- Compare CUDA/Metal/OpenCL/CPU results at aligned/unaligned starts, tails, epoch transitions and 32-bit nonce rollover; preserve active-lane shuffle semantics.
- Reassess GPU register spills, occupancy, wall energy, CPU-verifier latency and sustained mining/proving coexistence on the final configuration.
- Rerun the 2.0 multi-epoch hardware and economic gates with no assumed chip retirement. An algorithm review cannot establish customer demand, network security, retention or category rank.
## External primary-source context
These references support the general engineering context, not the numerical observations about Igneum.
- RFC 9923, FNV Non-Cryptographic Hash Algorithm, section 1.2: https://www.rfc-editor.org/rfc/rfc9923.html#section-1.2
- ProgPoW reference design, program generation and cryptographic encapsulation: https://github.com/ifdefelse/ProgPOW
- NVIDIA CUDA Best Practices Guide, register/occupancy and memory optimisation: https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/index.html
- RandomX design, device binding and easy-program selection: https://github.com/tevador/RandomX/blob/master/doc/design.md
## Source excerpts
Line numbers below refer to the unmodified files inside the supplied archive. These excerpts are included so the findings are auditable without assuming this report's interpretation. The source manifest records every file's SHA-256.
### `igneum-pow-src/generator.rs:780-819`
```text
780 | pub const GENERATOR_VERSION_V3: u32 = 3;
781 |
782 | /// Generator version of a class v4 program (Counter ASIC 3.0, 6 October 2026, PROPOSED: `program_id(4, seed, attempt)`).
783 | pub const GENERATOR_VERSION_V4: u32 = 4;
784 |
785 | /// The program class of an epoch (Counter ASIC 2.0, 5 October 2026, `docs/plans/counter-asic-2-rollout.md`): one
786 | /// height switch in the node, `program_class_v3_activation_daa`, rounded up to an epoch boundary, decides which
787 | /// class an epoch's program is drawn from. V2 is the lottery hash as adopted on 4 October 2026, byte for byte.
788 | /// V3 is generator version 3: its program id carries `generator = 3` and its load class is [`V3_CLASS`].
789 | /// V4 (Counter ASIC 3.0, 6 October 2026, the candidate `mx8+sh256x27` behind `program_class_v4_activation_daa`) is
790 | /// generator version 4: its program id carries `generator = 4` and its load class is [`V4_CLASS`].
791 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
792 | pub enum ProgramClass {
793 | #[default]
794 | V2,
795 | V3,
796 | V4,
797 | }
798 |
799 | /// The load class of program class v3, decided 5 October 2026 (Counter ASIC 2.0, `docs/plans/counter-asic-2-status.md`
800 | /// "22:00 decided", `docs/plans/mixer-x4.md`): [`LoadClass::MX4`], version 2 loads (the width stays 4 bytes, the
801 | /// per-load mix and the scratch share are out), the mixer applied 4 times per round and the cache growth rule. The
802 | /// placeholder of the seam (w16) is replaced here; nothing else in the seam names the class.
803 | /// Composed on 5 October 2026 (branch ca2-era): the era layout of `docs/plans/era-layout.md` is drawn inside this class by
804 | /// [`generate_from_seed_bytes_program_class`] (`LoadClass::era(V3_CLASS, era, &V3_ALLOWED)`); here `era` is `None`.
805 | pub const V3_CLASS: LoadClass = LoadClass { era: None, hot: None, ..LoadClass::MX8 };
806 |
807 | /// The load class of program class v4 (Counter ASIC 3.0 item 8, `docs/analysis/latency-shadow-2026-10-06.md`, the
808 | /// candidate of 6 October 2026, gated by `docs/plans/counter-asic-3-node.md`): class v3 plus the latency-shadow block
809 | /// of 256 ALU instructions run 27 times per iteration ("mx8+sh256x27", 55,296 shadow instructions per hash). The
810 | /// base program, the 16 loads, the item construction, the cache growth rule and the era draw are class v3's, draw
811 | /// for draw, so a v4 epoch's day cache and dataset are the v3 day's. Composed with the era exactly as V3 is.
812 | pub const V4_CLASS: LoadClass = LoadClass { shadow: Some(ShadowClass { instrs: V4_SHADOW_INSTRS, reps: V4_SHADOW_REPS }), ..V3_CLASS };
813 |
814 | /// The shadow block size of class v4 at every rung of the latency ladder (`docs/design/latency-ladder.md`): 256
815 | /// instructions. The ladder moves the pass count alone.
816 | pub const V4_SHADOW_INSTRS: u16 = 256;
817 |
818 | /// The shadow passes of class v4 at rung 0 of the latency ladder: 27 (`mx8+sh256x27`, about 102,100 counted ops).
819 | pub const V4_SHADOW_REPS: u16 = 27;
```
### `igneum-pow-src/generator.rs:850-858`
```text
850 | /// The width set class v3's era draw chooses from: 4 bytes only (the read-width decision of 5 October 2026; the
851 | /// draw is consumed, so widening the set at genesis keeps the derivation).
852 | pub const V3_ALLOWED: [u8; 1] = [1];
853 |
854 | /// The program of an era class (generator version 3): `base` with the era parameters drawn from `era_bytes`
855 | /// over the width set `allowed`, generator 3 stamped and the era bytes recorded (`docs/plans/era-layout.md`).
856 | /// The chain's path is this with `base = V3_CLASS` and `allowed = V3_ALLOWED`.
857 | pub fn generate_era(seed_string: &str, seed_bytes: &[u8], base: LoadClass, era_bytes: &[u8], allowed: &[u8]) -> Program {
858 | generate_era_generator(seed_string, seed_bytes, base, era_bytes, allowed, era_generator_of(&base))
```
### `igneum-pow-src/generator.rs:124-147`
```text
124 | /// An injecting op: bijective in `dst` and bringing another register (or the dataset) in. The acceptance
125 | /// rule's part (b) requires one such write per register.
126 | pub fn injects(self) -> bool {
127 | matches!(self, Op::Add | Op::Sub | Op::Xor | Op::Mad | Op::Shfl | Op::Load | Op::WLoad | Op::Scratch | Op::Hot)
128 | }
129 |
130 | /// A memory operation: the fresh-source rule, the acceptance tests and the load count treat the scratch
131 | /// read-modify-write and the hot-table load as loads (each is one of the program's 128 memory operations).
132 | pub fn is_load(self) -> bool {
133 | matches!(self, Op::Load | Op::WLoad | Op::Scratch | Op::Hot)
134 | }
135 | }
136 |
137 | /// One instruction. Every field is drawn for every instruction whether the op uses it or not, so the
138 | /// draw stream is identical for every op.
139 | #[derive(Clone, Copy, Debug, PartialEq, Eq)]
140 | pub struct Instr {
141 | pub op: Op,
142 | /// Destination register 0..7.
143 | pub dst: u8,
144 | /// Source register 0..7, never equal to `dst`.
145 | pub src: u8,
146 | /// Second source (mad only).
147 | pub src2: u8,
```
### `igneum-pow-src/generator.rs:1049-1065`
```text
1049 | /// The program id: FNV-1a 64 over `"igneum-program/" || generator_le32 || seed words as little-endian bytes
1050 | /// || attempt_le32`. Written into every pack so a version 1 program, or another attempt of the same seed,
1051 | /// can never be mistaken for this one.
1052 | pub fn program_id(&self) -> u64 {
1053 | // Latency ladder (docs/design/latency-ladder.md section 7): a class v4 program above rung 0 carries its shadow
1054 | // size in the id (`program_id_class`, the "shadow/" bytes), so two rungs of one seed never share an id and a
1055 | // pack of another rung is refused as a pack of another class is. Rung 0 keeps `program_id(4, seed, attempt)`
1056 | // byte for byte, so every v4 id written before the ladder stands.
1057 | let v4_rung_0 = self.generator == GENERATOR_VERSION_V4 && LoadClass { era: None, ..self.class } == V4_CLASS;
1058 | if self.class.is_v2() || self.generator == GENERATOR_VERSION_V3 || v4_rung_0 {
1059 | // Spec 01 section 1.4.6: a class v3 program's id is `program_id(3, seed, attempt)`, a class v4 program's
1060 | // `program_id(4, seed, attempt)` (Counter ASIC 3.0); the generator version in the preimage separates
1061 | // them from every version 2 program of the same seed
1062 | program_id(self.generator, &self.seed, self.attempt)
1063 | } else {
1064 | program_id_class(self.generator, &self.seed, self.attempt, &self.class)
1065 | }
```
### `igneum-pow-src/generator.rs:1266-1310`
```text
1266 | eligible[rng.below(n as u64) as usize]
1267 | }
1268 | } else {
1269 | let a = rng.below(7);
1270 | if a >= dst {
1271 | a + 1
1272 | } else {
1273 | a
1274 | }
1275 | };
1276 | let b = rng.below(8);
1277 | let imm = rng.next() as u32;
1278 | let imm2 = rng.next() as u32;
1279 | let rot = 1 + rng.below(31) as u32;
1280 | let bit = rng.below(32);
1281 | let mask = 1u8 << rng.below(5);
1282 | // Version 2 loads take no width roll, so a mixer class with version 2 loads draws the version 2 program
1283 | let width = if class.takes_width_roll() { class.width_for_roll(rng.below(100)) } else { 1 };
1284 | let width = if op == Op::Load { width } else { 1 };
1285 | // Era layout, layer 8: two window draws per instruction (drawn on every slot, used on a load slot).
1286 | let (win, off) = if class.era.is_some() {
1287 | let k = rng.below(3) as u8;
1288 | let o = (rng.next() as u32 & ((1u32 << k) - 1)) as u8;
1289 | if op == Op::Load {
1290 | (k, o)
1291 | } else {
1292 | (0, 0)
1293 | }
1294 | } else {
1295 | (0, 0)
1296 | };
1297 | if op.is_load() {
1298 | fresh[src as usize] = false;
1299 | }
1300 | fresh[dst as usize] = true;
1301 | instrs.push(Instr { op, dst: dst as u8, src: src as u8, src2: b as u8, imm, imm2, rot, bit: bit as u8, mask, width, win, off });
1302 | }
1303 | // (3) The latency-shadow block (Counter ASIC 3.0 item 8): drawn after the base program from the same stream, so
1304 | // the 64 instructions above are the class's without the shadow, draw for draw. Every slot is an ALU slot: the
1305 | // op from the non-load table, the source as on an ALU slot, the same per-instruction draws (the width roll and
1306 | // the era windows included when the class takes them, drawn and ignored) so the stream shape is the program's.
1307 | let mut shadow = Vec::new();
1308 | if let Some(sh) = class.shadow {
1309 | for _ in 0..sh.instrs {
1310 | let mut roll = rng.below(75);
```
### `igneum-pow-src/generator.rs:1377-1404`
```text
1377 | /// The program of a seed: the first accepted candidate over attempts `0, 1, 2, ...`, at most [`MAX_ATTEMPTS`].
1378 | /// This is what the chain calls (`Epoch::from_seed_bytes`) with the 32-byte epoch seed, and what the packs call
1379 | /// with the UTF-8 of a seed string.
1380 | pub fn try_generate_from_seed_bytes(seed_string: &str, seed_bytes: &[u8]) -> Result<Program, Exhausted> {
1381 | try_generate_class(seed_string, seed_bytes, LoadClass::V2)
1382 | }
1383 |
1384 | /// [`try_generate_from_seed_bytes`] for a load class.
1385 | pub fn try_generate_class(seed_string: &str, seed_bytes: &[u8], class: LoadClass) -> Result<Program, Exhausted> {
1386 | let mut last = None;
1387 | for attempt in 0..MAX_ATTEMPTS {
1388 | let p = candidate_class(seed_string, seed_bytes, attempt, class);
1389 | match check(&p) {
1390 | Ok(_) => return Ok(p),
1391 | Err(r) => last = Some(r),
1392 | }
1393 | }
1394 | Err(Exhausted { seed_string: seed_string.to_string(), attempts: MAX_ATTEMPTS, last: last.unwrap() })
1395 | }
1396 |
1397 | /// [`try_generate_from_seed_bytes`], treating exhaustion as the consensus fault it is.
1398 | pub fn generate_from_seed_bytes(seed_string: &str, seed_bytes: &[u8]) -> Program {
1399 | try_generate_from_seed_bytes(seed_string, seed_bytes).unwrap_or_else(|e| panic!("{e}"))
1400 | }
1401 |
1402 | /// [`generate_from_seed_bytes`] for a load class.
1403 | pub fn generate_from_seed_bytes_class(seed_string: &str, seed_bytes: &[u8], class: LoadClass) -> Program {
1404 | try_generate_class(seed_string, seed_bytes, class).unwrap_or_else(|e| panic!("{e}"))
```
### `igneum-pow-src/accept.rs:178-205`
```text
178 | /// Returns the first lane-constant load site, if any.
179 | fn run_unit(p: &Program, unit: usize, base: u32, acc: &mut Acc, lane_addrs: &mut [u32]) -> Result<(), Reject> {
180 | let seed = &p.seed;
181 | let mask: u32 = (1u32 << ACCEPT_DATASET_LOG2) - 1;
182 | let (d0, d1) = (seed[0], seed[1]);
183 | let (h0, h1) = (seed[2], seed[3]);
184 | let hot_words = p.hot_words();
185 | let loads = p.loads_per_hash();
186 | let mut r = [[0u32; LANES]; 8];
187 | for lane in 0..LANES {
188 | let nonce = base.wrapping_add(lane as u32);
189 | for i in 0..8 {
190 | let mut x = nonce ^ seed[i];
191 | x = x.wrapping_add(0x9e3779b9u32.wrapping_mul(i as u32 + 1));
192 | x = splitmix32(x);
193 | r[i][lane] = x ^ seed[(i + 1) & 7];
194 | }
195 | }
196 | let mut idx = [0u32; LANES];
197 | let mut nload = 0usize;
198 | let mut scratch = if p.has_scratch() { Some(ScratchModel::new(p.class.scratch_slots_per_lane())) } else { None };
199 | let slot_mask = p.class.scratch_slot_mask();
200 | let era = p.class.era;
201 | for it in 0..ITERATIONS {
202 | let sel = r[0];
203 | for (k, ins) in p.instrs.iter().enumerate() {
204 | let d = ins.dst as usize;
205 | let a = ins.src as usize;
```
### `igneum-pow-src/accept.rs:276-317`
```text
276 | for lane in 0..LANES {
277 | r[d][lane] = src[lane].wrapping_mul(src2[lane]).wrapping_add(r[d][lane]);
278 | }
279 | }
280 | Op::Shfl => {
281 | let src = r[a];
282 | let m = ins.mask as usize;
283 | for lane in 0..LANES {
284 | r[d][lane] ^= src[lane ^ m];
285 | }
286 | }
287 | Op::Load => {
288 | // Read-width experiment: a load of `width` words reads from the aligned address and folds every
289 | // word (verify::fold_words); width 1 is the lottery hash's xor of one word.
290 | let width = ins.width as usize;
291 | let align = !(ins.width as u32 - 1);
292 | for lane in 0..LANES {
293 | idx[lane] = load_index(era.as_ref(), ins, r[a][lane], mask, ACCEPT_DATASET_LOG2) & align;
294 | }
295 | if idx.iter().all(|&x| x == idx[0]) {
296 | return Err(Reject::LaneConstantSite { iteration: it as u8, instr: k as u8, unit: unit as u8 });
297 | }
298 | for lane in 0..LANES {
299 | if width == 1 {
300 | r[d][lane] ^= dataset_elem(idx[lane], d0, d1);
301 | } else {
302 | let mut w = [0u32; 16];
303 | for j in 0..width {
304 | w[j] = dataset_elem(idx[lane] + j as u32, d0, d1);
305 | }
306 | r[d][lane] = fold_words(r[d][lane], &w[..width]);
307 | }
308 | lane_addrs[lane * loads + nload] = idx[lane];
309 | }
310 | nload += 1;
311 | }
312 | Op::Hot => {
313 | // Hot table: the stand-in is dataset_elem keyed by seed words 2 and 3; the address is tagged with
314 | // bit 30 so a hot word and a dataset word at one index count as two addresses.
315 | for lane in 0..LANES {
316 | idx[lane] = hot_index(r[a][lane], hot_words);
317 | }
```
### `igneum-pow-src/accept.rs:327-404`
```text
327 | Op::WLoad => {
328 | let b = (r[a][0] & mask) & !31;
329 | for lane in 0..LANES {
330 | idx[lane] = b + lane as u32;
331 | r[d][lane] ^= dataset_elem(idx[lane], d0, d1);
332 | lane_addrs[lane * loads + nload] = idx[lane];
333 | }
334 | nload += 1;
335 | }
336 | }
337 | }
338 | }
339 | for i in 0..8 {
340 | for lane in 0..LANES {
341 | let v = r[i][lane];
342 | acc.and_acc[i] &= v;
343 | acc.or_acc[i] |= v;
344 | acc.saturated += (v == 0 || v == u32::MAX) as u32;
345 | }
346 | }
347 | for lane in 0..LANES {
348 | let lo = r[0][lane] ^ r[1][lane].rotate_left(7) ^ r[2][lane].rotate_left(14) ^ r[3][lane].rotate_left(21);
349 | let hi = r[4][lane] ^ r[5][lane].rotate_left(9) ^ r[6][lane].rotate_left(18) ^ r[7][lane].rotate_left(27);
350 | let h = ((hi as u64) << 32) | lo as u64;
351 | for j in 0..64 {
352 | acc.bit_ones[j] += ((h >> j) & 1) as u32;
353 | }
354 | let sl = &mut lane_addrs[lane * loads..(lane + 1) * loads];
355 | sl.sort_unstable();
356 | let mut distinct = 0u64;
357 | for k in 0..loads {
358 | // scratch slots carry bit 31 (variant 5) and are not dataset addresses
359 | if sl[k] & 0x8000_0000 == 0 && (k == 0 || sl[k] != sl[k - 1]) {
360 | distinct += 1;
361 | }
362 | }
363 | acc.distinct_sum += distinct;
364 | }
365 | Ok(())
366 | }
367 |
368 | /// Part (c).
369 | pub fn check_dynamic(p: &Program) -> Result<AcceptReport, Reject> {
370 | let loads = p.loads_per_hash();
371 | let mut acc = Acc { and_acc: [u32::MAX; 8], or_acc: [0; 8], saturated: 0, bit_ones: [0; 64], distinct_sum: 0 };
372 | let mut lane_addrs = vec![0u32; LANES * loads];
373 | for (unit, &base) in accept_base_nonces(&p.seed).iter().enumerate() {
374 | run_unit(p, unit, base, &mut acc, &mut lane_addrs)?;
375 | }
376 | for reg in 0..8 {
377 | let bits = (acc.and_acc[reg] | !acc.or_acc[reg]).count_ones();
378 | if bits != 0 {
379 | return Err(Reject::ConstantBit { reg: reg as u8, bits: bits as u8 });
380 | }
381 | }
382 | if acc.saturated >= MAX_SATURATED {
383 | return Err(Reject::Saturated { count: acc.saturated });
384 | }
385 | let half = (ACCEPT_HASHES / 2) as u32;
386 | let mut bias_max = 0u32;
387 | for (bit, &ones) in acc.bit_ones.iter().enumerate() {
388 | let d = ones.abs_diff(half);
389 | if d > BIAS_TOLERANCE {
390 | return Err(Reject::OutputBias { bit: bit as u8, ones });
391 | }
392 | bias_max = bias_max.max(d);
393 | }
394 | if acc.distinct_sum <= min_distinct_sum(loads - p.scratch_ops_per_hash()) {
395 | return Err(Reject::DistinctAddresses { sum: acc.distinct_sum });
396 | }
397 | Ok(AcceptReport { distinct_sum: acc.distinct_sum, saturated: acc.saturated, bias_max })
398 | }
399 |
400 | /// The whole rule: (a), (b), then (c).
401 | pub fn check(p: &Program) -> Result<AcceptReport, Reject> {
402 | check_static(p)?;
403 | check_dynamic(p)
404 | }
```
### `igneum-pow-src/verify.rs:335-398`
```text
335 | pub fn interpret_warp_scratch(
336 | program: &Program,
337 | seed: &[u32; 8],
338 | base_nonce: u32,
339 | ds: &DatasetSource,
340 | trace: bool,
341 | ) -> (WarpResult, Vec<ScratchEvent>) {
342 | let mask = ds.mask;
343 | let log2 = ds.log2_words;
344 | let era = program.class.era;
345 | let layout = program.class.layout();
346 | let mut r = [[0u32; LANES]; 8];
347 | for lane in 0..LANES {
348 | let nonce = base_nonce.wrapping_add(lane as u32);
349 | for i in 0..8 {
350 | let mut x = nonce ^ seed[i];
351 | x = x.wrapping_add(0x9e3779b9u32.wrapping_mul(i as u32 + 1));
352 | x = splitmix32(x);
353 | r[i][lane] = x ^ seed[(i + 1) & 7];
354 | }
355 | }
356 | let mut items_derived = 0usize;
357 | let mut idx = [0u32; LANES];
358 | let mut val = [0u32; LANES];
359 | let mut scratch = if program.has_scratch() { Some(ScratchModel::new(program.class.scratch_slots_per_lane())) } else { None };
360 | if trace {
361 | if let Some(m) = scratch.as_mut() {
362 | m.trace = Some(Vec::new());
363 | }
364 | }
365 | let slot_mask = program.class.scratch_slot_mask();
366 | if program.has_hot() {
367 | let h = ds.hot.as_ref().expect("a hot-table program needs the epoch's hot table on the dataset source");
368 | assert_eq!(h.n_words(), program.hot_words(), "the hot table's size is the class's");
369 | }
370 | for _ in 0..ITERATIONS {
371 | let sel = r[0];
372 | for ins in &program.instrs {
373 | step(ins, &mut r, &sel, mask, log2, era.as_ref(), layout, ds, &mut idx, &mut val, &mut items_derived);
374 | if ins.op == Op::Scratch {
375 | let m = scratch.as_mut().expect("a scratch op needs a scratch class");
376 | let (d, a) = (ins.dst as usize, ins.src as usize);
377 | for lane in 0..LANES {
378 | let slot = r[a][lane] & slot_mask;
379 | r[d][lane] = m.rmw(&program.seed, base_nonce, lane, slot, r[d][lane]);
380 | }
381 | }
382 | }
383 | // Latency-shadow block (Counter ASIC 3.0 item 8): the block runs `reps` times after instruction 63 with the
384 | // iteration's `sel`; it is empty on every class without a shadow, so version 2 and class v3 run nothing here.
385 | for _ in 0..program.shadow_reps() {
386 | for ins in &program.shadow {
387 | step(ins, &mut r, &sel, mask, log2, era.as_ref(), layout, ds, &mut idx, &mut val, &mut items_derived);
388 | }
389 | }
390 | }
391 | let mut hashes = [0u64; LANES];
392 | for lane in 0..LANES {
393 | let lo = r[0][lane] ^ r[1][lane].rotate_left(7) ^ r[2][lane].rotate_left(14) ^ r[3][lane].rotate_left(21);
394 | let hi = r[4][lane] ^ r[5][lane].rotate_left(9) ^ r[6][lane].rotate_left(18) ^ r[7][lane].rotate_left(27);
395 | hashes[lane] = ((hi as u64) << 32) | lo as u64;
396 | }
397 | let events = scratch.and_then(|m| m.trace).unwrap_or_default();
398 | (WarpResult { hashes, items_derived }, events)
```
### `igneum-pow-src/seed.rs:34-49`
```text
34 |
35 | /// The 32-byte seed (8 x u32) from arbitrary bytes: FNV-1a 64 with four salts, each finalised with the
36 | /// murmur-style mix `h ^= h >> 33; h *= 0xff51afd7ed558ccd; h ^= h >> 33`; low word then high word.
37 | pub fn seed_words_from_bytes(bytes: &[u8]) -> [u32; 8] {
38 | let mut words = [0u32; 8];
39 | for salt in 0..4u64 {
40 | let basis = 0xcbf29ce484222325u64 ^ salt.wrapping_mul(0x9E3779B97F4A7C15);
41 | let mut h = fnv1a64_with_basis(basis, bytes);
42 | h ^= h >> 33;
43 | h = h.wrapping_mul(0xff51afd7ed558ccd);
44 | h ^= h >> 33;
45 | words[2 * salt as usize] = h as u32;
46 | words[2 * salt as usize + 1] = (h >> 32) as u32;
47 | }
48 | words
49 | }
```
### `igneum-pow-src/bind.rs:47-83`
```text
47 | /// `"igneum-block/" || H || nonce_hi_le32`, the bytes the init words are derived from.
48 | pub fn block_init_bytes(header_prehash: &[u8; 32], nonce: u64) -> [u8; 49] {
49 | let mut b = [0u8; 49];
50 | b[..13].copy_from_slice(BLOCK_TAG);
51 | b[13..45].copy_from_slice(header_prehash);
52 | b[45..49].copy_from_slice(&nonce_hi(nonce).to_le_bytes());
53 | b
54 | }
55 |
56 | /// The init words `I` for a header and a 64-bit nonce (only the high 32 bits of the nonce matter).
57 | pub fn block_init_words(header_prehash: &[u8; 32], nonce: u64) -> [u32; 8] {
58 | seed_words_from_bytes(&block_init_bytes(header_prehash, nonce))
59 | }
60 |
61 | /// Interim day seed bytes: `"igneum-day/" || day_le64`.
62 | pub fn day_bytes(day_index: u64) -> [u8; 19] {
63 | let mut b = [0u8; 19];
64 | b[..11].copy_from_slice(DAY_TAG);
65 | b[11..19].copy_from_slice(&day_index.to_le_bytes());
66 | b
67 | }
68 |
69 | /// Day index of a header timestamp in milliseconds.
70 | #[inline]
71 | pub fn day_index(timestamp_ms: u64) -> u64 {
72 | timestamp_ms / DAY_MS
73 | }
74 |
75 | /// The 256-bit pow value as little-endian bytes: the lane hash in bytes 24..32, zero elsewhere.
76 | pub fn pow256_from_lane(lane: u64) -> [u8; 32] {
77 | let mut b = [0u8; 32];
78 | b[24..32].copy_from_slice(&lane.to_le_bytes());
79 | b
80 | }
81 |
82 | /// The 64-bit target from a little-endian 256-bit target: its top 64 bits.
83 | pub fn target64_from_le256(target: &[u8; 32]) -> u64 {
```
### `igneum-pow-src/bind.rs:99-119`
```text
99 |
100 | impl Epoch {
101 | /// The 32 bound hashes of the aligned warp that contains `nonce`: lane `l` is the hash of
102 | /// `(nonce_hi << 32) | ((lane_nonce & !31) + l)`.
103 | pub fn hash_warp_bound(&self, header_prehash: &[u8; 32], nonce: u64) -> [u64; LANES] {
104 | self.interpret_warp_bound(header_prehash, nonce).hashes
105 | }
106 |
107 | pub fn interpret_warp_bound(&self, header_prehash: &[u8; 32], nonce: u64) -> WarpResult {
108 | let init = block_init_words(header_prehash, nonce);
109 | interpret_warp_init(&self.program, &init, lane_nonce(nonce) & !31, &self.dataset)
110 | }
111 |
112 | /// The 32 bound hashes for already-derived init words (what a GPU worker computes per dispatch).
113 | pub fn hash_warp_init(&self, init: &[u32; 8], base_lane_nonce: u32) -> [u64; LANES] {
114 | interpret_warp_init(&self.program, init, base_lane_nonce, &self.dataset).hashes
115 | }
116 |
117 | /// The bound 64-bit lane hash of one header nonce.
118 | pub fn hash_bound(&self, header_prehash: &[u8; 32], nonce: u64) -> u64 {
119 | self.hash_warp_bound(header_prehash, nonce)[(lane_nonce(nonce) & 31) as usize]
```
### `igneum-pow-src/emit.rs:1219-1243`
```text
1219 | s.push_str(&format!("__global__ void igneum_hash_bound(const uint32_t* ds, uint64_t* out, uint32_t baseNonce, uint32_t mask, IgneumInitWords iw{hot_args}{scratch_args}) {{\n"));
1220 | if p.has_scratch() {
1221 | s.push_str(&persistent_prologue(CoreDialect::Cuda, p.class.scratch_words_per_lane()));
1222 | } else {
1223 | s.push_str(" uint32_t gid = blockIdx.x * blockDim.x + threadIdx.x;\n");
1224 | }
1225 | s.push_str(" uint32_t nonce = baseNonce + gid;\n");
1226 | s.push_str(" uint32_t r0, r1, r2, r3, r4, r5, r6, r7;\n");
1227 | if p.has_wide() {
1228 | s.push_str(" uint32_t lane = threadIdx.x & 31u;\n uint32_t wmask = mask & ~31u;\n");
1229 | }
1230 | for i in 0..8 {
1231 | s.push_str(&format!(
1232 | " {{ uint32_t x = nonce ^ iw.w[{i}]; x += 0x9e3779b9u * {}u; x = splitmix32(x); r{i} = x ^ iw.w[{}]; }}\n",
1233 | i + 1,
1234 | (i + 1) & 7
1235 | ));
1236 | }
1237 | s.push_str(&format!("\n for (uint32_t it = 0u; it < {ITERATIONS}u; ++it) {{\n uint32_t sel = r0;\n"));
1238 | s.push_str(&cuda_instr_lines(p, dataset_log2));
1239 | s.push_str(&shadow_block(p, CoreDialect::Cuda));
1240 | s.push_str(" }\n");
1241 | s.push_str(" uint32_t lo = r0 ^ rotl_imm(r1, 7u) ^ rotl_imm(r2, 14u) ^ rotl_imm(r3, 21u);\n");
1242 | s.push_str(" uint32_t hi = r4 ^ rotl_imm(r5, 9u) ^ rotl_imm(r6, 18u) ^ rotl_imm(r7, 27u);\n");
1243 | s.push_str(" out[gid] = ((uint64_t)hi << 32) | (uint64_t)lo;\n");
```
### `igneum-pow-src/emit.rs:1260-1270`
```text
1260 | } else {
1261 | s.push_str(
1262 | "cudaError_t igneum_launch_hash_bound(const uint32_t* ds, uint64_t* out, uint32_t baseNonce, uint32_t mask,\n",
1263 | );
1264 | s.push_str(&format!(" IgneumInitWords iw,{hot_decl} uint32_t nonces, uint32_t blockWarps) {{\n"));
1265 | s.push_str(" if (blockWarps == 0u || blockWarps > 32u) return cudaErrorInvalidValue;\n");
1266 | s.push_str(" uint32_t block = 32u * blockWarps;\n");
1267 | s.push_str(" if (nonces == 0u || (nonces % block) != 0u) return cudaErrorInvalidValue;\n");
1268 | s.push_str(&format!(" igneum_hash_bound<<<nonces / block, block>>>(ds, out, baseNonce, mask, iw{hot_pass});\n"));
1269 | s.push_str(" return cudaGetLastError();\n");
1270 | s.push_str("}\n");
```
### `igneum-pow-src/packcheck.rs:166-185`
```text
166 |
167 | /// [`verify_pack_texts`] that also demands a program class and, for class v3 and v4, the era seed the chain is on
168 | /// (Counter ASIC 2.0, 5 October 2026; class v4 Counter ASIC 3.0, 6 October 2026). `want_class` `None` accepts any
169 | /// class; `want_era` `None` skips the era. A pack whose `IGNEUM_GENERATOR` is not 2, 3 or 4 is refused whatever is wanted.
170 | pub fn verify_pack_texts_chain(
171 | program_h: &str,
172 | seeds_txt: Option<&str>,
173 | want_epoch: &[u8],
174 | want_day: &[u8],
175 | want_class: Option<ProgramClass>,
176 | want_era: Option<&[u8]>,
177 | ) -> Result<PackIdentity, PackFault> {
178 | let generator = define_u32(program_h, "IGNEUM_GENERATOR").unwrap_or(1);
179 | let Some(class) = ProgramClass::from_generator(generator) else {
180 | return Err(PackFault::WrongClass(format!("IGNEUM_GENERATOR {generator} is not a generator version this software runs (2, 3 or 4)")));
181 | };
182 | // IGNEUM_PROGRAM_CLASS, when present, must name the class the generator version names
183 | if let Some(named) = define_str(program_h, "IGNEUM_PROGRAM_CLASS") {
184 | if ProgramClass::parse(&named) != Some(class) {
185 | return Err(PackFault::Disagree(format!("IGNEUM_PROGRAM_CLASS {named:?} does not match IGNEUM_GENERATOR {generator}")));
```
### `igneum-pow-src/packcheck.rs:208-225`
```text
208 | if let (Some(want), true) = (want_era, class.has_era()) {
209 | let want_hex = hex(want);
210 | match &era_hex {
211 | Some(h) if *h == want_hex => {}
212 | Some(h) => return Err(PackFault::WrongClass(format!("the pack's era seed {} is not the era seed {} the job names", short(h), short(&want_hex)))),
213 | None => return Err(PackFault::WrongClass(format!("a class {} pack without IGNEUM_ERA_SEED_HEX; the job names an era seed", class.name()))),
214 | }
215 | }
216 | let seedw = define_words(program_h, "IGNEUM_SEEDW_INIT").ok_or_else(|| PackFault::Unreadable("program.h has no IGNEUM_SEEDW_INIT with 8 words".into()))?;
217 | let keyw = define_words(program_h, "IGNEUM_KEY_INIT").ok_or_else(|| PackFault::Unreadable("program.h has no IGNEUM_KEY_INIT with 8 words".into()))?;
218 | let attempt = define_u32(program_h, "IGNEUM_PROGRAM_ATTEMPT").unwrap_or(0);
219 | let mut epoch_hex = define_str(program_h, "IGNEUM_SEED_BYTES_HEX").unwrap_or_default();
220 | let mut day_hex = define_str(program_h, "IGNEUM_DAY_BYTES_HEX").unwrap_or_default();
221 | if let Some(s) = seeds_txt {
222 | let e = seeds_line(s, "epoch_seed_hex").ok_or_else(|| PackFault::Unreadable("seeds.txt has no epoch_seed_hex line".into()))?;
223 | let d = seeds_line(s, "day_seed_hex").ok_or_else(|| PackFault::Unreadable("seeds.txt has no day_seed_hex line".into()))?;
224 | if !epoch_hex.is_empty() && !epoch_hex.eq_ignore_ascii_case(&e) {
225 | return Err(PackFault::Disagree(format!("seeds.txt names epoch {} but program.h was generated for epoch {} (a pack half rewritten?)", short(&e), short(&epoch_hex))));
```
### `igneum-pow-src/packcheck.rs:263-272`
```text
263 | pub fn verify_pack_dir(dir: &Path, want_epoch: &[u8], want_day: &[u8]) -> Result<PackIdentity, PackFault> {
264 | verify_pack_dir_chain(dir, want_epoch, want_day, None, None)
265 | }
266 |
267 | /// [`verify_pack_texts_chain`] over a pack directory.
268 | pub fn verify_pack_dir_chain(dir: &Path, want_epoch: &[u8], want_day: &[u8], want_class: Option<ProgramClass>, want_era: Option<&[u8]>) -> Result<PackIdentity, PackFault> {
269 | let program_h = std::fs::read_to_string(dir.join("program.h")).map_err(|e| PackFault::Unreadable(format!("cannot read {}/program.h: {e}", dir.display())))?;
270 | let seeds = std::fs::read_to_string(dir.join("seeds.txt")).ok();
271 | verify_pack_texts_chain(&program_h, seeds.as_deref(), want_epoch, want_day, want_class, want_era)
272 | }
```

View file

@ -0,0 +1,181 @@
{
"edition": "Igneum 2.0 master / 2026-10-08",
"assets": [
{
"bundle_path": "01_strategy_source/IGNEUM 2.0.rtf",
"filename": "IGNEUM 2.0.rtf",
"bytes": 28017,
"sha256": "00ca1ec477aef0e967eadd6652a35355e17a1346d42396afec202fc99abb4475",
"description": "Original supplied strategy text"
},
{
"bundle_path": "02_tests/IGNEUM_2.0_Test_Registry.json",
"filename": "IGNEUM_2.0_Test_Registry.json",
"bytes": 204778,
"sha256": "573e0df9b4449d877eaf420279a53eef4497d3d9c67e12e62da7bd5e253a0417",
"description": "128 original cases and 17 proposed profiles"
},
{
"bundle_path": "03_legacy_review/IGNEUM_Algorithm_Review_Evidence.md",
"filename": "IGNEUM_Algorithm_Review_Evidence.md",
"bytes": 45434,
"sha256": "fe6f4c9242574cadbd88e769d4e2c17bbc81c027e8a5fca9599dbf452078d77d",
"description": "Exact R0 source"
},
{
"bundle_path": "03_legacy_review/IGNEUM_Algorithm_Review_Results.json",
"filename": "IGNEUM_Algorithm_Review_Results.json",
"bytes": 82485,
"sha256": "6a529f0ec8488669cfb3a8a5f702da73a092a1947a9280e7ec31d582b041d206",
"description": "Exact original observations and manifest"
},
{
"bundle_path": "03_legacy_review/IGNEUM_Algorithm_Review_Harness.zip",
"filename": "IGNEUM_Algorithm_Review_Harness.zip",
"bytes": 42759,
"sha256": "566d12f1bb67c99cda2c47a7c6eb4616479da80d320925979ff0052c5784cab5",
"description": "Original runnable diagnostic harness"
},
{
"bundle_path": "04_full_system/IGNEUM_V6_Full_System_Review.md",
"filename": "IGNEUM_V6_Full_System_Review.md",
"bytes": 210161,
"sha256": "1e62d89f743c3c3d4525f6321c53c05025e628080face878d82ff2f93ceb0213",
"description": "Exact R1 source"
},
{
"bundle_path": "04_full_system/IGNEUM_V6_Full_System_Review_Results.json",
"filename": "IGNEUM_V6_Full_System_Review_Results.json",
"bytes": 15353,
"sha256": "44c4931c657aac530f95f1a6d183943f817fd6f022afab2495555b4a1ece8c03",
"description": "Exact R1 recorded outputs"
},
{
"bundle_path": "04_full_system/IGNEUM_V6_Full_System_Review_Harness.zip",
"filename": "IGNEUM_V6_Full_System_Review_Harness.zip",
"bytes": 95385,
"sha256": "13fb83611921df0b1d3d7ed9f25bea4d2af404d6aa3f55fbf578e4bc5fad9261",
"description": "R1 original harness and source guards"
},
{
"bundle_path": "05_updated_stack/IGNEUM_Updated_Stack_Review.md",
"filename": "IGNEUM_Updated_Stack_Review.md",
"bytes": 163364,
"sha256": "3734c1d09075421976e5872baee7c00abe00d1e0a062ad56dded4a05df002a87",
"description": "Exact R2 source"
},
{
"bundle_path": "05_updated_stack/IGNEUM_Updated_Stack_Findings.json",
"filename": "IGNEUM_Updated_Stack_Findings.json",
"bytes": 43073,
"sha256": "9809e8284cdf841e051c6db4494af7e24159c96aa25e6c71635b411f9fac3b3e",
"description": "All 14 R2 findings, 44 closure requirements and outputs"
},
{
"bundle_path": "05_updated_stack/IGNEUM_Updated_Stack_Reproductions.zip",
"filename": "IGNEUM_Updated_Stack_Reproductions.zip",
"bytes": 16657,
"sha256": "21f8bbf15a12aa4836f16464fa0af7fab1e789d4bbacc331234c6f634dfdca9a",
"description": "R2 original reproduction pack"
},
{
"bundle_path": "06_master/IGNEUM_2.0_Master_Traceability.json",
"filename": "IGNEUM_2.0_Master_Traceability.json",
"bytes": 271435,
"sha256": "7ebf8d60cf0e4e3889ab5472b5b4f807d791739ef62f91ffaa74236792a4810e",
"description": "Original registry plus namespaced add-ons and mapping"
},
{
"bundle_path": "07_reviewed_sources/igneum-mining-algorithm-2026-10-08.zip",
"filename": "igneum-mining-algorithm-2026-10-08.zip",
"bytes": 148136,
"sha256": "94edb290005ecb8379cc599da90c877c7e25562f727cc4291069b4fbc149c7c1",
"description": "R0 reviewed source archive"
},
{
"bundle_path": "07_reviewed_sources/igneum-v6-freeze-tree-2026-10-08(1).zip",
"filename": "igneum-v6-freeze-tree-2026-10-08(1).zip",
"bytes": 696492,
"sha256": "f448981b2ceeab2e59e8bbd137a1a13ea1c730ecd9db72b9a89bd2bbf5d85acb",
"description": "Reviewed v6 freeze archive; one byte-identical copy"
},
{
"bundle_path": "07_reviewed_sources/igneum-proving-2026-10-08(1).zip",
"filename": "igneum-proving-2026-10-08(1).zip",
"bytes": 1612294,
"sha256": "9ccf4112e274f586392e8e4a4f98ece2e82b4990fe5f1ee89ae0ee0a151ce01e",
"description": "Reviewed proving archive; one byte-identical copy"
},
{
"bundle_path": "07_reviewed_sources/igneum-ember-2026-10-08(1).zip",
"filename": "igneum-ember-2026-10-08(1).zip",
"bytes": 78946,
"sha256": "c219211b49fccda65b151df230b10ded5971ac2a8c7d1e5847c0a68bffe4cf4a",
"description": "Reviewed Ember archive; one byte-identical copy"
},
{
"bundle_path": "07_reviewed_sources/igneum-node-dag-2026-10-08.zip",
"filename": "igneum-node-dag-2026-10-08.zip",
"bytes": 1519903,
"sha256": "ead2cf94092b7e27aab2e44d653b2d969cb2a52878ef12abf6e5d7a9e7421cff",
"description": "Reviewed node and DAG source"
},
{
"bundle_path": "07_reviewed_sources/igneum-mining-workers-2026-10-08.zip",
"filename": "igneum-mining-workers-2026-10-08.zip",
"bytes": 6090014,
"sha256": "808abcecf157a3716d1c72fa6e76c540bf5808af1c0e4dd0fd888576d546cade",
"description": "Reviewed workers, engine and pool source"
}
],
"source_pdf_pages": {
"strategy": 37,
"test_standard": 73
},
"sections": {
"A": {
"start": 19,
"pages": 37
},
"B": {
"start": 56,
"pages": 73
},
"C": {
"start": 129,
"pages": 11
},
"R2": {
"start": 140,
"pages": 59
},
"R1": {
"start": 199,
"pages": 72
},
"R0": {
"start": 271,
"pages": 18
},
"E": {
"start": 289,
"pages": 4
}
},
"total_pages": 292,
"editorial_note": "Source PDF pages preserved; source reports reflowed without substantive omissions; no new technical audit performed.",
"companion_original_documents": [
{
"bundle_path": "00_original_documents/IGNEUM_2.0_Plan.pdf",
"bytes": 257288,
"sha256": "418b3b9f68f96a413872fecb16f8508a40063891c70054c65e92e6e5f5fb70b6"
},
{
"bundle_path": "00_original_documents/IGNEUM_2.0_Test_and_Acceptance_Standard.pdf",
"bytes": 391678,
"sha256": "3b0767a1f2ed3312ad4b6ff277285754ee1f4dc64fa0a3a37dc07c42eaf23a8b"
}
],
"master_pdf_sha256": "c878ee4f80adf1da58fcc78fb91a8008de2e9fc44060bec143d020a780c5093e",
"master_light_pdf_sha256": "f54f12117fd10ebcd1cad83b41342cf2a21ba7e0cf0abfa100d48b101b02b18b"
}

View file

@ -0,0 +1,48 @@
IGNEUM 2.0 - Complete Master Edition: evidence companion
08 October 2026
The two 292-page master PDFs contain the same substantive material in
obsidian and light editions. This bundle provides the raw evidence for
viewers that do not expose PDF attachments.
CONTENTS
- Original supplied strategy RTF.
- Original 128-case test registry and 17 proposed acceptance profiles.
- All three complete review reports, original results/findings and
reproduction packages.
- Master traceability JSON preserving the original registry and adding
18 INT integration gates and 44 namespaced R2 closure requirements.
- Six distinct reviewed source archives (legacy algorithm plus five updated
source bundles). Byte-identical repeated uploads are represented once.
- Original strategy and test-standard PDFs for provenance.
- MANIFEST.json with SHA-256 hashes and page mapping.
READING ORDER
Master introduction: pages 1-18.
Original strategy: pages 19-55.
Original test standard: pages 56-128.
Additional closure requirements: pages 129-139.
R2 updated stack review: pages 140-198.
R1 full-system review: pages 199-270.
R0 historical algorithm review: pages 271-288.
Evidence inventory: pages 289-292.
STATUS AND LIMITS
This is a consolidation, not a new code audit or execution report. No
native node, SP1, GPU, production network or physical ASIC test was run for
this edition. Original probe results are preserved with their limitations.
A defect reproduced by a probe is not a passed acceptance gate.
Overlapping findings are not independent defects. The 128 cases, 18 INT
gates and 44 closure requirements overlap; do not count their sum as 190
independent tests. Numerical acceptance profiles remain proposed unless
separately approved. Missing or unrun evidence is not PASS.
The master explicitly preserves differing interpretations, particularly
around finality healing. It does not silently decide which account is
correct. Closure requires version-pinned source and native evidence.
Reproduction archives are source artifacts, not signed production releases.
Inspect their README, dependencies and commands before running them in an
isolated environment. Never run fault or payment tests against live funds.
No original font files are distributed in this bundle.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
# Igneum 2.0 test harness map
Generated from tools/ci/test-map.json by tools/ci/test-map-doc.mjs; edit the JSON, never this page. Registry: docs/plans/igneum-2.0-test-registry.json (128 cases).
Generated from tools/ci/test-map.json by tools/ci/test-map-doc.mjs; edit the JSON, never this page. Registry: docs/plans/igneum-2.0-test-registry.json (172 cases).
Rule: a case maps to a cell only where the cell's tests visibly answer it; coverage names what the cell proves and what remains; a mapped cell's green writes RUNNING, PASS only when coverage is full and the evidence file exists; an automated case with no cell reads NOT RUN with its reason, never PASS by inference.
@ -328,7 +328,51 @@ Rule: a case maps to a cell only where the cell's tests visibly answer it; cover
- LEAD-05 Measure control and dependency concentration: observation window
- LEAD-06 Complete the reliability observation window: observation window
- LEAD-08 Keep leadership claims valid after release: observation window
- REV-F01-1 Valid proof A, warm cache, carried statement B: refuse exactly as a cold node does.: F01 (P0, the external review): the regression's harness is the owner lane's (proving lane, node lane); not yet named in the map
- REV-F01-2 Run valid/invalid contexts in both orders, concurrently and across cache eviction/restart.: F01 (P0, the external review): the regression's harness is the owner lane's (proving lane, node lane); not yet named in the map
- REV-F01-3 Change payout, network, kind, program ID and activation context; no accepted misbinding.: F01 (P0, the external review): the regression's harness is the owner lane's (proving lane, node lane); not yet named in the map
- REV-F01-4 A native two-node test must agree on block validity and payouts despite different cache histories.: F01 (P0, the external review): the regression's harness is the owner lane's (proving lane, node lane); not yet named in the map
- REV-F02-1 Release build cannot activate test bypass.: F02 (P0, the external review): the regression's harness is the owner lane's (proving lane, CI steward); not yet named in the map
- REV-F02-2 Missing oracle or pinned keys prevents service readiness after enforcement activation.: F02 (P0, the external review): the regression's harness is the owner lane's (proving lane, CI steward); not yet named in the map
- REV-F02-3 Missing proof bytes retry without incorrectly marking a valid block permanently invalid.: F02 (P0, the external review): the regression's harness is the owner lane's (proving lane, CI steward); not yet named in the map
- REV-F03-1 Clean build from one manifest, including the pool and workers, with no unpublished vendor tree.: F03 (P0, the external review): the regression's harness is the owner lane's (CI steward, hash lane (ProgramClass::V6 on freeze), pool lane); not yet named in the map
- REV-F03-2 Same job context produces identical accepted work in node, CPU reference, CUDA, Metal, OpenCL and pool.: F03 (P0, the external review): the regression's harness is the owner lane's (CI steward, hash lane (ProgramClass::V6 on freeze), pool lane); not yet named in the map
- REV-F03-3 Cross every scheduled transition with old/new client behavior documented and identical rule identities.: F03 (P0, the external review): the regression's harness is the owner lane's (CI steward, hash lane (ProgramClass::V6 on freeze), pool lane); not yet named in the map
- REV-F04-1 Native multi-node 40/40/20 partition, equivocation and dust-valid mining on both sides past the window.: F04 (P0, the external review): the regression's harness is the owner lane's (node lane, reference apps, site (explorer)); not yet named in the map
- REV-F04-2 Proof that the chosen recovery guarantee matches the public finality claim; two valid contradictory certificates are a hard failure for strong finality.: F04 (P0, the external review): the regression's harness is the owner lane's (node lane, reference apps, site (explorer)); not yet named in the map
- REV-F04-3 Pause-only resume with historical backfill, missing historical data and all old keys returning.: F04 (P0, the external review): the regression's harness is the owner lane's (node lane, reference apps, site (explorer)); not yet named in the map
- REV-F04-4 Wallet, receipt and oracle consumers distinguish any weaker recovery state.: F04 (P0, the external review): the regression's harness is the owner lane's (node lane, reference apps, site (explorer)); not yet named in the map
- REV-F05-1 Native reference-vs-incremental expression equivalence across all source registers and real instruction updates.: F05 (P1, the external review): the regression's harness is the owner lane's (hash lane, adversary lane, floor lane 3); not yet named in the map
- REV-F05-2 Full-kernel output equivalence, registers, spills, wall power and accepted throughput across target GPUs.: F05 (P1, the external review): the regression's harness is the owner lane's (hash lane, adversary lane, floor lane 3); not yet named in the map
- REV-F05-3 Adversary physical design includes prefix caching/recomputation cost; no assumed full 63-read cost on every load.: F05 (P1, the external review): the regression's harness is the owner lane's (hash lane, adversary lane, floor lane 3); not yet named in the map
- REV-F06-1 Acceptance/reference/emitter evaluate the same activated schedule.: F06 (P1, the external review): the regression's harness is the owner lane's (hash lane, research lane D); not yet named in the map
- REV-F06-2 Full live-dataset census on unseen seeds and the complete v6 pack.: F06 (P1, the external review): the regression's harness is the owner lane's (hash lane, research lane D); not yet named in the map
- REV-F06-3 A failed experimental rule does not silently exhaust generation or bypass the intended resource requirement.: F06 (P1, the external review): the regression's harness is the owner lane's (hash lane, research lane D); not yet named in the map
- REV-F07-1 Same final v6 configuration: mine -> evict -> prove -> aggregate -> submit -> rebuild -> resume; no leaked reservations.: F07 (P1, the external review): the regression's harness is the owner lane's (app lane, fleet lane); not yet named in the map
- REV-F07-2 OOM, process crash and stale work recover without losing wallet state or silently consuming power.: F07 (P1, the external review): the regression's harness is the owner lane's (app lane, fleet lane); not yet named in the map
- REV-F07-3 16 GB+ simultaneous mode only after measured peak plus next-epoch headroom; smaller-card modes labelled separately.: F07 (P1, the external review): the regression's harness is the owner lane's (app lane, fleet lane); not yet named in the map
- REV-F08-1 Report every eligible job outcome, including expired work; a bounded list cannot hide losses.: F08 (P1, the external review): the regression's harness is the owner lane's (proving lane, fleet lane, site (ops page)); not yet named in the map
- REV-F08-2 Consumer tiers complete and receive payment for declared jobs before claims about income.: F08 (P1, the external review): the regression's harness is the owner lane's (proving lane, fleet lane, site (ops page)); not yet named in the map
- REV-F08-3 Sustained real workload across class transitions, with restart/retry and no publisher intervention.: F08 (P1, the external review): the regression's harness is the owner lane's (proving lane, fleet lane, site (ops page)); not yet named in the map
- REV-F09-1 Generate all pass/fail cells directly from the declared inequalities and inputs.: F09 (P1, the external review): the regression's harness is the owner lane's (research lane, floor lane 3, coordinator); not yet named in the map
- REV-F09-2 Independent feasibility and cost evaluation of the strongest modeled SRAM/hybrid opponent.: F09 (P1, the external review): the regression's harness is the owner lane's (research lane, floor lane 3, coordinator); not yet named in the map
- REV-F09-3 GPU owners and entrants remain viable under the approved scenario envelope without assuming chip absence or death.: F09 (P1, the external review): the regression's harness is the owner lane's (research lane, floor lane 3, coordinator); not yet named in the map
- REV-F10-1 Compare exact found nonce/hash sets with full-read mode, including all-hit overflow and zero-hit cases.: F10 (P2, the external review): the regression's harness is the owner lane's (worker lane (new)); not yet named in the map
- REV-F10-2 Test stale jobs, high-32 rollover, tail batches and asynchronous buffer reuse.: F10 (P2, the external review): the regression's harness is the owner lane's (worker lane (new)); not yet named in the map
- REV-F10-3 Compare production throughput and wall energy, not only the isolated kernel timer.: F10 (P2, the external review): the regression's harness is the owner lane's (worker lane (new)); not yet named in the map
- REV-F11-1 Goal switch from an efficiency prior can explore higher core/power when policy allows.: F11 (P2, the external review): the regression's harness is the owner lane's (Ember lane (new)); not yet named in the map
- REV-F11-2 Driver, workload, dataset, compiler and device changes invalidate certification while retaining optional hints.: F11 (P2, the external review): the regression's harness is the owner lane's (Ember lane (new)); not yet named in the map
- REV-F11-3 Thermal/error/late-share events revert safely, including process or machine crash.: F11 (P2, the external review): the regression's harness is the owner lane's (Ember lane (new)); not yet named in the map
- REV-F12-1 Crash before/after broadcast, response timeout, restart before snapshot: no duplicate payment or silent debt loss.: F12 (P1, the external review): the regression's harness is the owner lane's (pool lane (new)); not yet named in the map
- REV-F12-2 Same signed transaction retried, fee replacement reconciled by intent, not a new payment.: F12 (P1, the external review): the regression's harness is the owner lane's (pool lane (new)); not yet named in the map
- REV-F12-3 Receipt reorg and finality pause leave correct pending obligations.: F12 (P1, the external review): the regression's harness is the owner lane's (pool lane (new)); not yet named in the map
- REV-F13-1 Repeated authorization rejected or atomically replaces and cleans prior state.: F13 (P1, the external review): the regression's harness is the owner lane's (pool lane (new)); not yet named in the map
- REV-F13-2 Oversized unterminated frame rejected within fixed memory/time budget.: F13 (P1, the external review): the regression's harness is the owner lane's (pool lane (new)); not yet named in the map
- REV-F13-3 Slow clients and invalid share floods cannot grow unbounded member, nonce or outgoing state.: F13 (P1, the external review): the regression's harness is the owner lane's (pool lane (new)); not yet named in the map
- REV-F14-1 Public build has no default arbitrary remote execution and a documented least-privilege boundary.: F14 (P1, the external review): the regression's harness is the owner lane's (app lane, relay lane); not yet named in the map
- REV-F14-2 Automatic updates off remains off for urgent manifests until explicit action.: F14 (P1, the external review): the regression's harness is the owner lane's (app lane, relay lane); not yet named in the map
- REV-F14-3 A compromised fleet/update signing key cannot silently acquire wallet access or activate a consensus change.: F14 (P1, the external review): the regression's harness is the owner lane's (app lane, relay lane); not yet named in the map
## Count
109 automated cases: 56 mapped to a cell, 58 NOT RUN with a reason.
153 automated cases: 56 mapped to a cell, 102 NOT RUN with a reason.

File diff suppressed because it is too large Load diff

View file

@ -112,6 +112,15 @@ static void checkSource(EmuProg* p, bool defaultDevice, bool noCuda) {
if (bad.empty() && p->headers.count("memhard.h")) bad = firstUnannotated("memhard.h", p->headers["memhard.h"], noCuda);
if (!bad.empty()) { p->log = "emu-nvrtc: " + bad + ": A function without execution space annotations (__host__/__device__/__global__) is considered a host function, and host functions are not allowed in JIT mode (pass -default-device or -DIGNEUM_NO_CUDA)"; p->ok = false; return; }
}
// The select pass (8 October 2026): the worker's own text, no pack involved; it is recorded as module 3 and run by
// d_launch as a host loop (in reverse index order, so the hits land out of nonce order and the worker's sort is exercised)
if (p->name == "igneum_select.cu") {
if (p->src.find("__global__ void igneum_select(") == std::string::npos || p->src.find("atomicAdd(count, 1u)") == std::string::npos) { p->log = "emu-nvrtc: igneum_select.cu is not the select pass this stand-in knows"; p->ok = false; return; }
p->pack = 3; p->ok = true;
std::printf("info emu-nvrtc: igneum_select.cu source check PASS: %zu bytes, the select pass runs as a host loop\n", p->src.size());
std::fflush(stdout);
return;
}
for (int which = 1; which <= 2; ++which) {
std::string dir = packDir(which);
if (dir.empty()) continue;
@ -159,7 +168,7 @@ static nvrtcResult e_compile(nvrtcProgram prog, int numOptions, const char* cons
}
static nvrtcResult e_logSize(nvrtcProgram prog, size_t* n) { *n = ((EmuProg*)prog)->log.size() + 1; return NVRTC_SUCCESS; }
static nvrtcResult e_log(nvrtcProgram prog, char* out) { std::strcpy(out, ((EmuProg*)prog)->log.c_str()); return NVRTC_SUCCESS; }
static std::string image(EmuProg* p) { return std::string("EMU-IMAGE:") + (p->pack == 2 ? "B" : "A"); }
static std::string image(EmuProg* p) { return std::string("EMU-IMAGE:") + (p->pack == 3 ? "S" : p->pack == 2 ? "B" : "A"); }
static nvrtcResult e_imgSize(nvrtcProgram prog, size_t* n) { *n = image((EmuProg*)prog).size() + 1; return NVRTC_SUCCESS; }
static nvrtcResult e_img(nvrtcProgram prog, char* out) { std::strcpy(out, image((EmuProg*)prog).c_str()); return NVRTC_SUCCESS; }
static nvrtcResult e_addName(nvrtcProgram prog, const char* e) { ((EmuProg*)prog)->exprs.push_back(e); return NVRTC_SUCCESS; }
@ -210,12 +219,13 @@ static CUresult d_htod(CUdeviceptr dst, const void* src, size_t n) { std::memcpy
static CUresult d_modLoad(CUmodule* m, const void* img) {
const char* s = (const char*)img;
if (std::strncmp(s, "EMU-IMAGE:", 10) != 0) return CUDA_ERROR_INVALID_IMAGE;
*m = (CUmodule)(uintptr_t)(s[10] == 'B' ? 2 : 1);
*m = (CUmodule)(uintptr_t)(s[10] == 'S' ? 3 : s[10] == 'B' ? 2 : 1);
return CUDA_SUCCESS;
}
static CUresult d_modUnload(CUmodule) { return CUDA_SUCCESS; }
static CUresult d_getFn(CUfunction* f, CUmodule m, const char* name) {
int pack = (int)(uintptr_t)m, idx;
if (pack == 3) { if (std::strcmp(name, "igneum_select") != 0) return CUDA_ERROR_NOT_FOUND; *f = (CUfunction)(uintptr_t)7; return CUDA_SUCCESS; }
if (std::strcmp(name, "igneum_cache_fill") == 0) idx = 1;
else if (std::strcmp(name, "igneum_build") == 0) idx = 2;
else if (std::strcmp(name, "igneum_hash_bound") == 0) idx = 3;
@ -255,6 +265,22 @@ static CUresult d_launch(CUfunction f, unsigned gx, unsigned, unsigned, unsigned
}
case 6: emu_launch(emu_pack_b::igneum_hash_bound, gx, bx, dptr<const uint32_t>(params, 0), dptr<uint64_t>(params, 1), arg<uint32_t>(params, 2), arg<uint32_t>(params, 3), arg<emu_pack_b::IgneumInitWords>(params, 4)); return CUDA_SUCCESS;
#endif
case 7: {
// the select pass (8 October 2026): the kernel text's semantics as a host loop, threads visited in reverse so the
// hit table fills out of nonce order (a GPU's atomics give no order either) and the worker's sort is exercised
const uint64_t* out = dptr<const uint64_t>(params, 0); uint32_t n = arg<uint32_t>(params, 1); uint64_t target = arg<uint64_t>(params, 2);
uint32_t* count = dptr<uint32_t>(params, 3); uint64_t* hits = dptr<uint64_t>(params, 4); uint32_t maxHits = arg<uint32_t>(params, 5); uint64_t* sentinel = dptr<uint64_t>(params, 6);
uint64_t threads = (uint64_t)gx * bx;
for (uint64_t t = threads; t-- > 0;) {
uint32_t i = (uint32_t)t;
if (i >= n) continue;
uint64_t h = out[i];
if (h <= target) { uint32_t k = (*count)++; if (k < maxHits) { hits[2u * k] = i; hits[2u * k + 1u] = h; } }
if (i < 32u) sentinel[i] = h;
if (i == 0u) { sentinel[32] = out[n / 2u]; sentinel[33] = out[n - 1u]; }
}
return CUDA_SUCCESS;
}
default: return CUDA_ERROR_INVALID_HANDLE;
}
}

View file

@ -0,0 +1,83 @@
#!/usr/bin/env bash
# The select pass equivalence check (8 October 2026, review B finding F10): the found nonce and hash set of
# --readback select equals the full read-back's, line for line, over the cases the finding names, on one worker
# binary (the CPU emulation from test.sh, or a real GPU build). Known-failed first: IGNEUM_READBACK_FAULT_TEST=1 makes
# the worker skip the counter reset from the second chunk on, so stale hits come back again; the comparison must flag
# that run before the real one counts.
# select_fault_detected the fault run differs from the full read (the check can fail)
# found_set_equals_full_zero_hits z1: target 0, two chunks, no found line in either mode
# found_set_equals_full_partial_hits p1: target 03ff..., about 16 hits per chunk, four chunks
# found_set_equals_full_all_hit_overflow a1: target ffff..., every nonce a hit, every chunk over IG_MAX_HITS (full fallback)
# found_set_equals_full_tail_batch t1: 1120 nonces at 1024 per chunk and 64 per block: a 96-nonce tail with a 32-lane launch
# found_set_equals_full_high32_rollover r1: a job from 2^32 - 32 across the high-word step
# found_set_equals_full_stale_job_queued s1, s2: two job lines queued at once, the second served after the first
# buffer_reuse_hits_then_zero b1 then b2: a hit chunk followed by a zero-target chunk prints nothing stale
# mismatch_error_then_job m1 (no pair for its seeds) errors the same way, c1 after it is served
# Usage: select-check.sh <pack A dir> <out dir> <worker command...> (the command carries --serve --pack <pack A> etc.)
set -euo pipefail
PACK_A="$1"; OUT="$2"; shift 2
mkdir -p "$OUT"
seedline() { sed -n "s/^$2 //p" "$1/seeds.txt"; }
A_EPOCH="$(seedline "$PACK_A" epoch_seed_hex)"; A_DAY="$(seedline "$PACK_A" day_seed_hex)"
[ -n "$A_EPOCH" ] && [ -n "$A_DAY" ] || { echo "FAIL: no seeds.txt in $PACK_A"; exit 1; }
PRE="00000000000000000000000000000000000000000000000000000000000000f1"
OTHER="1111111111111111111111111111111111111111111111111111111111111111"
PART="03ffffffffffffff"
script() {
echo "job z1 $PRE 0000000000000000 0 2048 $A_EPOCH $A_DAY"
echo "job p1 $PRE $PART 2048 4096 $A_EPOCH $A_DAY"
echo "job a1 $PRE ffffffffffffffff 8192 2048 $A_EPOCH $A_DAY"
echo "job t1 $PRE $PART 16384 1120 $A_EPOCH $A_DAY"
echo "job r1 $PRE $PART 4294967264 2048 $A_EPOCH $A_DAY"
echo "job s1 $PRE $PART 32768 2048 $A_EPOCH $A_DAY"
echo "job s2 $PRE $PART 40960 2048 $A_EPOCH $A_DAY"
echo "job b1 $PRE $PART 49152 1024 $A_EPOCH $A_DAY"
echo "job b2 $PRE 0000000000000000 50176 1024 $A_EPOCH $A_DAY"
echo "job m1 $PRE $PART 0 1024 $OTHER $A_DAY"
echo "job c1 $PRE $PART 51200 1024 $A_EPOCH $A_DAY"
echo "quit"
}
run() { # <mode: full|select|fault> <log>
local mode="$1" log="$2"; shift 2
case "$mode" in
full) script | IGNEUM_READBACK=full "$@" > "$log" 2>&1 || true ;;
select) script | IGNEUM_READBACK=select "$@" > "$log" 2>&1 || true ;;
fault) script | IGNEUM_READBACK=select IGNEUM_READBACK_FAULT_TEST=1 "$@" > "$log" 2>&1 || true ;;
esac
}
# the lines both modes must agree on: found, need, error, and done with its wall time stripped
norm() { grep -E '^(found|need|error|done) ' "$1" | sed -E 's/^(done [^ ]+ [0-9]+) [0-9.]+$/\1/'; }
WORKER=("$@")
run full "$OUT/rb-full.log" "${WORKER[@]}"
run select "$OUT/rb-select.log" "${WORKER[@]}"
run fault "$OUT/rb-fault.log" "${WORKER[@]}"
norm "$OUT/rb-full.log" > "$OUT/rb-full.norm"
norm "$OUT/rb-select.log" > "$OUT/rb-select.norm"
norm "$OUT/rb-fault.log" > "$OUT/rb-fault.norm"
grep -q '^ready .* readback full ' "$OUT/rb-full.log" || { echo "FAIL: the full run has no 'readback full' ready line"; exit 1; }
grep -q '^ready .* readback select ' "$OUT/rb-select.log" || { echo "FAIL: the select run has no 'readback select' ready line"; exit 1; }
[ "$(grep -c '^done ' "$OUT/rb-full.norm")" = 10 ] || { echo "FAIL: the full run should finish 10 jobs"; grep -E '^(error|done)' "$OUT/rb-full.log"; exit 1; }
echo "== select_fault_detected (known-failed first: the counter is not reset from the second chunk on)"
if cmp -s "$OUT/rb-full.norm" "$OUT/rb-fault.norm"; then echo "FAIL: the fault run equals the full read; the comparison cannot fail"; exit 1; fi
echo "flagged: $(diff "$OUT/rb-full.norm" "$OUT/rb-fault.norm" | grep -c '^>') stale or extra lines in the fault run"
echo "== found_set_equals_full_* (every case, line for line)"
if ! cmp -s "$OUT/rb-full.norm" "$OUT/rb-select.norm"; then echo "FAIL: select differs from the full read:"; diff "$OUT/rb-full.norm" "$OUT/rb-select.norm" | head -20; exit 1; fi
count() { grep -c "^found $1 " "$2" || true; }
[ "$(count z1 "$OUT/rb-select.norm")" = 0 ] || { echo "FAIL: zero_hits: z1 found lines"; exit 1; }
[ "$(count p1 "$OUT/rb-select.norm")" -gt 0 ] || { echo "FAIL: partial_hits: p1 found nothing"; exit 1; }
[ "$(count a1 "$OUT/rb-select.norm")" = 2048 ] || { echo "FAIL: all_hit_overflow: a1 should find 2048"; exit 1; }
grep -q '^done t1 1120' "$OUT/rb-select.norm" || { echo "FAIL: tail_batch: t1 did not finish 1120"; exit 1; }
[ "$(count t1 "$OUT/rb-select.norm")" -gt 0 ] || { echo "FAIL: tail_batch: t1 found nothing"; exit 1; }
grep -q '^done r1 2048' "$OUT/rb-select.norm" || { echo "FAIL: high32_rollover: r1 did not finish"; exit 1; }
awk '$1 == "found" && $2 == "r1" && $3 + 0 >= 4294967296 { n++ } END { exit n > 0 ? 0 : 1 }' "$OUT/rb-select.norm" || { echo "FAIL: high32_rollover: no found nonce at or past 2^32"; exit 1; }
grep -q '^done s1 2048' "$OUT/rb-select.norm" && grep -q '^done s2 2048' "$OUT/rb-select.norm" || { echo "FAIL: stale_job_queued: s1 or s2 did not finish"; exit 1; }
[ "$(count b1 "$OUT/rb-select.norm")" -gt 0 ] && [ "$(count b2 "$OUT/rb-select.norm")" = 0 ] || { echo "FAIL: buffer_reuse_hits_then_zero: b1 $(count b1 "$OUT/rb-select.norm") found, b2 $(count b2 "$OUT/rb-select.norm")"; exit 1; }
grep -q '^error m1 ' "$OUT/rb-select.norm" && grep -q '^done c1 1024' "$OUT/rb-select.norm" || { echo "FAIL: mismatch_error_then_job: m1 did not error or c1 was not served"; exit 1; }
grep -q 'full fallbacks' "$OUT/rb-select.log" || { echo "FAIL: no transfers line in the select run"; exit 1; }
fb="$(sed -n 's/.*(select, \([0-9]*\) full fallbacks).*/\1/p' "$OUT/rb-select.log" | tail -1)"
[ "$fb" = 2 ] || { echo "FAIL: all_hit_overflow: expected 2 full fallbacks (a1's two chunks), the select run reports '$fb'"; exit 1; }
echo "PASS: select = full over $(grep -c '^found ' "$OUT/rb-select.norm") found lines and 10 jobs (zero hits, partial, all-hit overflow with $fb full fallbacks, a 96-nonce tail, the high-32 rollover, two queued jobs, hits then zero, a mismatch error then a job); the fault run was flagged first"
echo "transfers: $(grep 'transfers per chunk' "$OUT/rb-select.log" | tail -1 | sed 's/^info //')"
echo "transfers: $(grep 'transfers per chunk' "$OUT/rb-full.log" | tail -1 | sed 's/^info //')"

View file

@ -64,3 +64,6 @@ echo "flagged as NVRTC does: $(grep -o 'program.h([0-9]*): [^:]*' "$OUT/check-no
echo "== --serve: two jobs on A, prepare B, a job on B (swap), a job on A after the swap (error), quit"
"$HERE/serve-check.sh" "$OUT/pack-a" "$OUT/pack-b" "$OUT/serve.log" "$OUT/igneum-worker-cuda-emu" --serve --pack "$OUT/pack-a" --batch-log2 13
echo "NVRTC source check PASS for $(grep -c 'source check PASS' "$OUT/serve.log") files in --serve, $(grep -c 'source check PASS' "$OUT/check-a.log") in --check"
echo "== the select pass (8 October 2026): --readback select equals the full read-back, known-failed first (emu/select-check.sh)"
"$HERE/select-check.sh" "$OUT/pack-a" "$OUT/select" "$OUT/igneum-worker-cuda-emu" --serve --pack "$OUT/pack-a" --batch-log2 10 --block-warps 2

90
proto-cuda/nvrtc/serve-bench.sh Executable file
View file

@ -0,0 +1,90 @@
#!/usr/bin/env bash
# Serving-mode measurement of igneum-worker-cuda (8 October 2026, review B finding F10): the production number beside the
# kernel timer, never interchangeable with it. The worker runs --serve exactly as the miner drives it: a stub pool here
# feeds jobs of 2^B nonces (one queued behind the one in flight, as the miner pipelines them), reads every found line
# (the miner's submit point) and every done line, and quits after N jobs. The clock starts before the worker process
# starts, so the setup (NVRTC compile, cache, dataset, self-test) is in the production number; the steady number runs
# from the ready line. Power is nvidia-smi's board reading at 4 Hz over the same windows (the pod exposes no host
# rail); the worker's own CPU seconds (utime + stime from /proc) stand in for the host-side cost of the read-back and
# the scan. The kernel timer is --bench on the same pack and batch, printed beside the rows.
# serve-bench.sh <worker> <pack dir> <out dir> <run name> <readback: full|select> [jobs 32] [batch-log2 24] [device 0]
# Pid files: every process this script starts writes <out dir>/pids/<run name>.<what>.pid (IGNEUM_PID_DIR overrides the directory) (the fleet's rule: a job is
# stopped through its pid file, never by a name pattern). Output: <out dir>/<run name>.row (one line, key=value) plus the
# worker log, the stub's timeline and the power samples.
set -euo pipefail
W="$1"; PACK="$2"; OUT="$3"; RUN="$4"; MODE="$5"; JOBS="${6:-32}"; B="${7:-24}"; DEV="${8:-0}"
[ "$MODE" = full ] || [ "$MODE" = select ] || { echo "readback must be full or select" >&2; exit 2; }
PIDDIR="${IGNEUM_PID_DIR:-$OUT/pids}" # the fleet keeps pid files under one directory per pod
mkdir -p "$PIDDIR"
LOG="$OUT/$RUN.worker.log"; TL="$OUT/$RUN.timeline"; SMI="$OUT/$RUN.smi"; ROW="$OUT/$RUN.row"
: > "$TL"
now() { date +%s.%N; }
count=$((1 << B))
target="00000fffffffffff" # one hit per 2^20 nonces: 16 found lines per 2^24-nonce job, the serving shape (a block target is far tighter)
seedline() { sed -n "s/^$2 //p" "$1/seeds.txt"; }
EPOCH="$(seedline "$PACK" epoch_seed_hex)"; DAY="$(seedline "$PACK" day_seed_hex)"
[ -n "$EPOCH" ] && [ -n "$DAY" ] || { echo "no seeds.txt in $PACK" >&2; exit 2; }
jobline() { printf 'job %d %064x %s %d %d %s %s\n' "$1" "$1" "$target" $(( $1 * count )) "$count" "$EPOCH" "$DAY"; }
# power sampler first (its own pid file)
nvidia-smi -i "$DEV" --query-gpu=timestamp,power.draw,clocks.sm,temperature.gpu --format=csv,noheader,nounits -lms 250 > "$SMI" 2>/dev/null &
echo $! > "$PIDDIR/$RUN.smi.pid"
sleep 1
t0=$(now)
coproc WK { IGNEUM_READBACK="$MODE" "$W" --serve --pack "$PACK" --device "$DEV" --batch-log2 "$B" --race off --readback "$MODE" 2>&1; }
wkpid=$WK_PID # bash clears WK_PID when the coproc ends; the copy outlives it
echo "$wkpid" > "$PIDDIR/$RUN.worker.pid"
sent=0; done_n=0; found=0; tready=""; tlast=""; cpu=""
: > "$LOG"
while IFS= read -r line <&"${WK[0]}"; do
t=$(now)
echo "$line" >> "$LOG"
case "$line" in
ready*) tready=$t; echo "$t ready" >> "$TL"; jobline $sent >&"${WK[1]}"; sent=$((sent + 1)); jobline $sent >&"${WK[1]}"; sent=$((sent + 1)) ;;
found*) found=$((found + 1)); echo "$t submit $line" >> "$TL" ;;
done*) done_n=$((done_n + 1)); tlast=$t; echo "$t $line" >> "$TL"
if [ "$sent" -lt "$JOBS" ]; then jobline $sent >&"${WK[1]}"; sent=$((sent + 1)); fi
if [ "$done_n" -ge "$JOBS" ]; then
cpu=$(awk '{ printf "%.2f", ($14 + $15) / 100.0 }' "/proc/$wkpid/stat" 2>/dev/null || echo "")
echo "quit" >&"${WK[1]}"
fi ;;
error*) echo "$t $line" >> "$TL" ;;
esac
done
wait "$wkpid" 2>/dev/null || true
t1=$(now)
sleep 1
kill "$(cat "$PIDDIR/$RUN.smi.pid")" 2>/dev/null || true
[ -n "$tready" ] && [ -n "$tlast" ] || { echo "FAIL: no ready or no done line; see $LOG"; tail -5 "$LOG"; exit 1; }
[ "$done_n" = "$JOBS" ] || { echo "FAIL: $done_n of $JOBS jobs done; see $LOG"; exit 1; }
# the power samples over the two windows: nvidia-smi stamps local time "YYYY/MM/DD HH:MM:SS.mmm"
meanw() { # <from epoch s> <to epoch s>: mean W of the samples inside the window, and their count
awk -F', *' -v a="$1" -v b="$2" 'BEGIN { n = 0; s = 0 }
{ gsub("/", "-", $1); split($1, d, " "); cmd = "date -d \"" d[1] " " d[2] "\" +%s.%N"; cmd | getline ts; close(cmd); if (ts >= a && ts <= b) { s += $2; n++ } }
END { if (n) printf "%.1f %d", s / n, n; else printf "0 0" }' "$SMI"
}
read -r wProd nProd <<< "$(meanw "$t0" "$tlast")"
read -r wSteady nSteady <<< "$(meanw "$tready" "$tlast")"
hashes=$(( JOBS * count ))
wallProd=$(awk -v a="$t0" -v b="$tlast" 'BEGIN { printf "%.3f", b - a }')
wallSteady=$(awk -v a="$tready" -v b="$tlast" 'BEGIN { printf "%.3f", b - a }')
setup=$(awk -v a="$t0" -v b="$tready" 'BEGIN { printf "%.3f", b - a }')
mhsProd=$(awk -v h="$hashes" -v w="$wallProd" 'BEGIN { printf "%.2f", h / w / 1e6 }')
mhsSteady=$(awk -v h="$hashes" -v w="$wallSteady" 'BEGIN { printf "%.2f", h / w / 1e6 }')
jProd=$(awk -v w="$wProd" -v s="$wallProd" -v n="$JOBS" 'BEGIN { printf "%.1f", w * s / n }')
jSteady=$(awk -v w="$wSteady" -v s="$wallSteady" -v n="$JOBS" 'BEGIN { printf "%.1f", w * s / n }')
mhwProd=$(awk -v m="$mhsProd" -v w="$wProd" 'BEGIN { if (w > 0) printf "%.4f", m / w; else printf "n/a" }')
mhwSteady=$(awk -v m="$mhsSteady" -v w="$wSteady" 'BEGIN { if (w > 0) printf "%.4f", m / w; else printf "n/a" }')
card=$(nvidia-smi -i "$DEV" --query-gpu=name --format=csv,noheader | tr ' ' '_')
transfers=$(grep 'transfers per chunk' "$LOG" | tail -1 | sed 's/^info //')
ready=$(grep '^ready ' "$LOG" | head -1)
{
printf 'card=%s mode=%s jobs=%d batch_log2=%d hashes=%d found=%d setup_s=%s ' "$card" "$MODE" "$JOBS" "$B" "$hashes" "$found" "$setup"
printf 'prod_wall_s=%s prod_mhs=%s prod_mean_w=%s prod_j_per_batch=%s prod_mh_per_w=%s samples=%s ' "$wallProd" "$mhsProd" "$wProd" "$jProd" "$mhwProd" "$nProd"
printf 'steady_wall_s=%s steady_mhs=%s steady_mean_w=%s steady_j_per_batch=%s steady_mh_per_w=%s samples=%s ' "$wallSteady" "$mhsSteady" "$wSteady" "$jSteady" "$mhwSteady" "$nSteady"
printf 'worker_cpu_s=%s worker_sha=%s\n' "${cpu:-n/a}" "$(sha256sum "$W" | cut -c1-16)"
printf 'ready: %s\n' "$ready"
printf 'transfers: %s\n' "$transfers"
} > "$ROW"
cat "$ROW"

View file

@ -1038,6 +1038,7 @@ struct Options {
std::string pack, arch = "auto";
std::string race = "on", pinned, tuningPath;
int raceBenchMs = 2000, raceBudgetS = 120, raceRounds = 0; // rounds 0 = 1 in --serve, 3 in --race
bool readbackFull = false; // --readback full / IGNEUM_READBACK=full: the pre-select path, 8 bytes per nonce over the bus
};
static void usage() {
@ -1060,7 +1061,9 @@ static void usage() {
" --race-budget-s N a race stops compiling and timing after this (default 120; base is kept)\n"
" --race-rounds N interleaved rounds, best per variant (default 1 in --serve, 3 in --race)\n"
" --variant <name> use this variant without a race (also from the tuning file)\n"
" --tuning <file> the per-card tuning file (default: IGNEUM_TUNING_FILE from the environment)\n", WORKER_VERSION);
" --tuning <file> the per-card tuning file (default: IGNEUM_TUNING_FILE from the environment)\n"
" --readback select|full in --serve: the GPU selects the hits and 34 sentinel words come back (default), or every\n"
" 8-byte result comes back and the host scans it (the path before 8 October 2026; also IGNEUM_READBACK=full)\n", WORKER_VERSION);
}
static Options parseArgs(int argc, char** argv) {
@ -1087,6 +1090,7 @@ static Options parseArgs(int argc, char** argv) {
else if (a == "--batch-log2") o.batchLog2 = std::atoi(next().c_str());
else if (a == "--block-warps") o.blockWarps = std::atoi(next().c_str());
else if (a == "--arch") o.arch = next();
else if (a == "--readback") { std::string v = next(); if (v == "full") o.readbackFull = true; else if (v == "select") o.readbackFull = false; else { std::printf("--readback must be select or full\n"); std::exit(2); } }
else if (a == "--no-prepare") { /* accepted for symmetry with the other workers; prepare is always on here */ }
else if (a == "-h" || a == "--help") { usage(); std::exit(0); }
else { std::printf("unknown argument %s\n", argv[i]); usage(); std::exit(2); }
@ -1098,6 +1102,7 @@ static Options parseArgs(int argc, char** argv) {
if (o.raceBudgetS < 5 || o.raceBudgetS > 540) { std::printf("--race-budget-s must be between 5 and 540 (the prepare lead is 600 DAA)\n"); std::exit(2); }
if (o.raceRounds == 0) o.raceRounds = o.raceOnly ? 3 : 1;
if (o.tuningPath.empty()) if (const char* t = std::getenv("IGNEUM_TUNING_FILE")) o.tuningPath = t;
if (const char* rb = std::getenv("IGNEUM_READBACK")) if (std::strcmp(rb, "full") == 0) o.readbackFull = true;
if (o.pack.empty() && !o.memprobe) { std::printf("--pack <dir> is required (igneum-miner export-pack <node> <dir> writes one)\n"); std::exit(2); }
while (o.pack.size() > 1 && (o.pack.back() == '/' || o.pack.back() == '\\')) o.pack.pop_back();
return o;
@ -1144,19 +1149,95 @@ static std::string findPackFor(const std::string& root, const std::string& epoch
static std::string parentDir(const std::string& p) { size_t i = p.find_last_of("/\\"); return i == std::string::npos ? "." : p.substr(0, i); }
static uint64_t fnv1a64Bytes(const void* p, size_t n);
// The select pass (8 October 2026, review B finding F10), the shape proto-opencl/host.c has carried since 5 October 2026.
// Before it every chunk read back 8 bytes per nonce (128 MiB for a 2^24-nonce dispatch) over the bus and the host scanned
// them under the GPU mutex. Now a tiny kernel built here by NVRTC (no pack involved, nothing in the hash path changes)
// writes the hits (index, hash word) behind an atomic counter plus 34 sentinel words (the first 32 outputs, the middle
// and the last), and the host reads back a few hundred bytes. The hash word printed is the kernel's own out[i], never
// recomputed. The found lines are printed in nonce order from the sorted hits, as the full scan printed them. A chunk
// with more hits than the table holds (a target that loose is a test or the P01 campaign, not a block) falls back to
// the full read of that chunk. The sentinels feed the stale-output detector the OpenCL worker has: two consecutive
// chunks never share the signature (different nonces or init words give different outputs), so a repeat means the
// kernel did not run. --readback full or IGNEUM_READBACK=full keeps the old path for a comparison; the select pass
// that fails to build falls back to it with an info line.
#define IG_MAX_HITS 256u
static const char* SELECT_SRC =
"// igneum-worker-cuda: the select pass (8 October 2026); the same shape as proto-opencl/host.c's igneum_select\n"
"extern \"C\" __global__ void igneum_select(const unsigned long long* out, unsigned int n, unsigned long long target, unsigned int* count,\n"
" unsigned long long* hits, unsigned int maxHits, unsigned long long* sentinel) {\n"
" unsigned int i = blockIdx.x * blockDim.x + threadIdx.x;\n"
" if (i < n) {\n"
" unsigned long long h = out[i];\n"
" if (h <= target) { unsigned int k = atomicAdd(count, 1u); if (k < maxHits) { hits[2u * k] = (unsigned long long)i; hits[2u * k + 1u] = h; } }\n"
" if (i < 32u) sentinel[i] = h;\n"
" if (i == 0u) { sentinel[32] = out[n / 2u]; sentinel[33] = out[n - 1u]; }\n"
" }\n"
"}\n";
struct SelectPass {
bool on = false;
CUmodule mod = nullptr;
CUfunction fn = nullptr;
CUdeviceptr dCount = 0, dHits = 0, dSentinel = 0;
uint32_t hCount = 0;
uint64_t hHits[IG_MAX_HITS * 2];
uint64_t hSentinel[34];
unsigned long long bytesDown = 0, fullFallbacks = 0;
double kernelMsSum = 0, selectMsSum = 0, readMsSum = 0, scanMsSum = 0;
unsigned long chunks = 0;
};
static void releaseSelect(Ctx& c, SelectPass& s) {
if (s.dCount) c.drv.memFree(s.dCount);
if (s.dHits) c.drv.memFree(s.dHits);
if (s.dSentinel) c.drv.memFree(s.dSentinel);
if (s.mod) c.drv.moduleUnload(s.mod);
s.dCount = s.dHits = s.dSentinel = 0; s.mod = nullptr; s.fn = nullptr; s.on = false;
}
// Builds the pass; on any failure the worker keeps the full read-back (an info line says why), never an error.
static void buildSelect(Ctx& c, SelectPass& s) {
Compiled cs;
std::string err;
if (!rtcCompile(c, SELECT_SRC, "igneum_select.cu", "", "", {}, cs, err)) { info("the select pass did not build (" + err + "); using the full read-back"); return; }
CUresult r = c.drv.moduleLoadData(&s.mod, cs.image.data());
if (r == CUDA_SUCCESS) r = c.drv.moduleGetFunction(&s.fn, s.mod, "igneum_select");
if (r == CUDA_SUCCESS) r = c.drv.memAlloc(&s.dCount, 4);
if (r == CUDA_SUCCESS) r = c.drv.memAlloc(&s.dHits, (size_t)IG_MAX_HITS * 2u * 8u);
if (r == CUDA_SUCCESS) r = c.drv.memAlloc(&s.dSentinel, 34u * 8u);
if (r != CUDA_SUCCESS) { info("the select pass did not load (" + c.err(r) + "); using the full read-back"); releaseSelect(c, s); return; }
s.on = true;
}
static int runServe(Ctx& c, const Options& o, Pair* cur) {
const uint32_t batch = 1u << o.batchLog2;
CUdeviceptr dOut = 0;
std::string err;
if (c.drv.memAlloc(&dOut, (size_t)batch * 8u) != CUDA_SUCCESS) { emit("error 0 cuMemAlloc out buffer"); return 2; }
std::vector<uint64_t> hOut(batch);
SelectPass sel;
if (!o.readbackFull) buildSelect(c, sel);
// IGNEUM_READBACK_FAULT_TEST=1 (test only): from the second chunk on the counter is not reset, so the hits of the
// previous chunk come back again; the equivalence check must flag it (the known-failed line of emu/select-check.sh).
const bool faultTest = std::getenv("IGNEUM_READBACK_FAULT_TEST") != nullptr;
uint64_t prevSig = 0; bool havePrevSig = false;
unsigned long jobsSeen = 0;
Pair* prepared = nullptr;
Pair* old = nullptr;
PrepareTask* task = nullptr;
std::string prepareRoot; // the parent of the last prepare's pack directory: where the miner writes its packs
emit(fmt("ready cuda %s pack %s dataset-log2 %u batch %u regs %d prepare 1 path nvrtc %d.%d driver %d.%d arch %s variant %s race %s worker %s",
c.name.c_str(), cur->seedString.c_str(), cur->datasetLog2, batch, cur->regs, c.rtcMajor, c.rtcMinor, c.driverVersion / 1000, (c.driverVersion % 100) / 10, c.archOpt.c_str(), cur->variant.c_str(), c.race.c_str(), WORKER_VERSION));
emit(fmt("ready cuda %s pack %s dataset-log2 %u batch %u regs %d prepare 1 path nvrtc %d.%d driver %d.%d arch %s variant %s race %s readback %s worker %s",
c.name.c_str(), cur->seedString.c_str(), cur->datasetLog2, batch, cur->regs, c.rtcMajor, c.rtcMinor, c.driverVersion / 1000, (c.driverVersion % 100) / 10, c.archOpt.c_str(), cur->variant.c_str(), c.race.c_str(), sel.on ? "select" : "full", WORKER_VERSION));
info(fmt("readback %s (per dispatch of %u nonces: %s)", sel.on ? "select" : "full", batch, sel.on ? "the hits and 34 sentinel words come back; the GPU scans" : "8 bytes per nonce come back and the host scans them"));
info(fmt("first pack %s: %s", cur->dir.c_str(), pairSummary(cur).c_str()));
auto statsLine = [&]() {
if (sel.chunks == 0) return;
info(fmt("transfers per chunk: down %.0f B (%s, %llu full fallbacks); mean per chunk: kernel+sync %.2f ms, select %.3f ms, read-back %.3f ms, scan %.3f ms; %lu jobs %lu chunks",
(double)sel.bytesDown / (double)sel.chunks, sel.on ? "select" : "full", sel.fullFallbacks,
sel.kernelMsSum / (double)sel.chunks, sel.selectMsSum / (double)sel.chunks, sel.readMsSum / (double)sel.chunks, sel.scanMsSum / (double)sel.chunks, jobsSeen, sel.chunks));
};
if (!cur->raceLine.empty()) emit(cur->raceLine);
std::string line;
while (std::getline(std::cin, line)) {
@ -1273,18 +1354,78 @@ static int runServe(Ctx& c, const Options& o, Pair* cur) {
uint32_t block = 32u * (uint32_t)cur->blockWarps;
uint32_t main = chunk - (chunk % block);
CUresult r = CUDA_SUCCESS;
bool useSelect = sel.on;
double c0 = wallMs(), kernelMs = 0, selectMs = 0, readMs = 0, scanMs = 0, r0;
if (useSelect && !(faultTest && sel.chunks > 0)) {
const uint32_t zero = 0;
r = c.drv.memcpyHtoD(sel.dCount, &zero, 4);
if (r != CUDA_SUCCESS) { emit("error " + jobId + " dispatch failed: select count reset: " + c.err(r)); failed = true; break; }
}
if (main > 0 && !launchHash(c, cur, dOut, lo, iw, main, block, nullptr, err)) { emit("error " + jobId + " dispatch failed: " + err); failed = true; break; }
if (main < chunk) {
for (uint32_t off = main; off < chunk && !failed; off += 32u) if (!launchHash(c, cur, dOut + (CUdeviceptr)off * 8u, lo + off, iw, 32u, 32u, nullptr, err)) { emit("error " + jobId + " dispatch failed: " + err); failed = true; }
if (failed) break;
}
r = c.drv.streamSynchronize(nullptr);
if (r == CUDA_SUCCESS) r = c.drv.memcpyDtoH(hOut.data(), dOut, (size_t)chunk * 8u);
if (r != CUDA_SUCCESS) { emit("error " + jobId + " dispatch failed: " + c.err(r)); failed = true; break; }
for (uint32_t i = 0; i < chunk; ++i) if (hOut[i] <= target) {
uint64_t nonce = ((uint64_t)hi << 32) | (uint64_t)(uint32_t)(lo + i);
std::printf("found %s %llu %016llx\n", jobId.c_str(), (unsigned long long)nonce, (unsigned long long)hOut[i]);
kernelMs = wallMs() - c0;
if (r == CUDA_SUCCESS && useSelect) {
// the select pass on the same (null) stream, after every launch of the chunk, over the whole chunk including its tail
r0 = wallMs();
uint32_t n = chunk, maxHits = IG_MAX_HITS;
unsigned long long t = target;
void* args[7] = { &dOut, &n, &t, &sel.dCount, &sel.dHits, &maxHits, &sel.dSentinel };
r = c.drv.launchKernel(sel.fn, (n + 255u) / 256u, 1, 1, 256, 1, 1, 0, nullptr, args, nullptr);
if (r == CUDA_SUCCESS) r = c.drv.streamSynchronize(nullptr);
selectMs = wallMs() - r0;
}
r0 = wallMs();
if (r == CUDA_SUCCESS && useSelect) {
r = c.drv.memcpyDtoH(&sel.hCount, sel.dCount, 4);
sel.bytesDown += 4;
if (r == CUDA_SUCCESS) {
if (sel.hCount > IG_MAX_HITS) {
// more hits than the table holds: this chunk takes the full path (the test target case)
++sel.fullFallbacks;
useSelect = false;
} else {
if (sel.hCount) { r = c.drv.memcpyDtoH(sel.hHits, sel.dHits, (size_t)sel.hCount * 16u); sel.bytesDown += (unsigned long long)sel.hCount * 16u; }
if (r == CUDA_SUCCESS) { r = c.drv.memcpyDtoH(sel.hSentinel, sel.dSentinel, 34u * 8u); sel.bytesDown += 34u * 8u; }
}
}
}
if (r == CUDA_SUCCESS && !useSelect) { r = c.drv.memcpyDtoH(hOut.data(), dOut, (size_t)chunk * 8u); sel.bytesDown += (unsigned long long)chunk * 8u; }
if (r != CUDA_SUCCESS) { emit("error " + jobId + " dispatch failed: " + c.err(r)); failed = true; break; }
readMs = wallMs() - r0;
// Stale output: the first, middle and last words plus an FNV of the first 32. Two chunks never share it.
// On the select path the same 34 words come from the sentinel buffer the select pass wrote.
if (chunk >= 64) {
uint64_t sig = useSelect ? (fnv1a64Bytes(sel.hSentinel, 32u * 8u) ^ sel.hSentinel[32] ^ sel.hSentinel[33])
: (fnv1a64Bytes(hOut.data(), 32u * 8u) ^ hOut[chunk / 2] ^ hOut[chunk - 1]);
if (havePrevSig && sig == prevSig) { emit(fmt("error %s worker fault: the output buffer is unchanged since the previous dispatch (signature %016llx): the kernel did not run; exiting 3 so the miner restarts the worker", jobId.c_str(), (unsigned long long)sig)); std::exit(3); }
prevSig = sig; havePrevSig = true;
}
r0 = wallMs();
if (useSelect) {
// nonce order, as the full scan printed them (the miner submits the first found of a job)
uint32_t nHits = sel.hCount;
for (uint32_t a = 1; a < nHits; ++a) {
uint64_t ki = sel.hHits[2 * a], kh = sel.hHits[2 * a + 1];
uint32_t b = a;
for (; b > 0 && sel.hHits[2 * (b - 1)] > ki; --b) { sel.hHits[2 * b] = sel.hHits[2 * (b - 1)]; sel.hHits[2 * b + 1] = sel.hHits[2 * (b - 1) + 1]; }
sel.hHits[2 * b] = ki; sel.hHits[2 * b + 1] = kh;
}
for (uint32_t a = 0; a < nHits; ++a) {
uint64_t nonce = ((uint64_t)hi << 32) | (uint64_t)(uint32_t)(lo + (uint32_t)sel.hHits[2 * a]);
std::printf("found %s %llu %016llx\n", jobId.c_str(), (unsigned long long)nonce, (unsigned long long)sel.hHits[2 * a + 1]);
}
} else {
for (uint32_t i = 0; i < chunk; ++i) if (hOut[i] <= target) {
uint64_t nonce = ((uint64_t)hi << 32) | (uint64_t)(uint32_t)(lo + i);
std::printf("found %s %llu %016llx\n", jobId.c_str(), (unsigned long long)nonce, (unsigned long long)hOut[i]);
}
}
scanMs = wallMs() - r0;
sel.kernelMsSum += kernelMs; sel.selectMsSum += selectMs; sel.readMsSum += readMs; sel.scanMsSum += scanMs; ++sel.chunks;
std::fflush(stdout);
hashes += chunk;
remaining -= chunk;
@ -1292,9 +1433,12 @@ static int runServe(Ctx& c, const Options& o, Pair* cur) {
}
if (failed) continue;
emit(fmt("done %s %llu %.2f", jobId.c_str(), (unsigned long long)hashes, wallMs() - t0));
if (++jobsSeen % 200 == 0) statsLine();
if (switched && old) { releasePair(c, old); old = nullptr; info("dropped the previous pair (its program, cache and dataset)"); }
}
statsLine();
if (task) { task->thread.join(); if (task->result) releasePair(c, task->result); delete task; }
releaseSelect(c, sel);
c.drv.memFree(dOut);
if (old) releasePair(c, old);
if (prepared) releasePair(c, prepared);

View file

@ -10,6 +10,10 @@
| the red watcher fires on cancelled and timed-out runs too (`ci-red.yml`, `red-watch.mjs`) | The watcher's `if` missing any of failure, cancelled, timed_out, or the conclusion not handed to the record step (the self-test reads the workflow file); the line names the kind: CI red, CI cancelled, CI timed out. | 7 October 2026 |
| gh's active account is the stored Igneum entry (`gh-account-check.sh`, in Igneum's own gh directory `~/.config/gh-igneum` through `gh-env.sh`, never the founder's) | A push or a landing from this Mac while Igneum's gh directory names any other account as active, or none (the refusal names the one step: the founder or main stores the Igneum token there with `GH_CONFIG_DIR=~/.config/gh-igneum gh auth login --with-token`; no lane does); skipped with a line while `github-suspended` stands. RULE: no lane switches gh accounts on this Mac, ever; the second owner's login belongs to other projects and must never touch Igneum; the stored entry's name is in ~/.config/igneum/gh-user, never in the repository. | 7 October 2026, 21:41 UK: a lane switched gh to the other login during the suspension; nobody could say which |
| F02 (Review B): the proof-rule test bypass cannot reach a release build (`proof-rule-bypass-check.sh`; a cell of the node matrix) | An env read of IGNEUM_TEST_SKIP_PROOF_RULE with no cfg(test) or cfg(feature) guard in the 12 lines above, or under a feature in the crate's default features; a release igneumd carrying the bypass string. Red on every node sha until the proving lane's change (the read under a non-default feature or cfg(test)) lands | 8 Oct 2026 |
| the test map merges structurally at a landing (`test-map-merge.py`) and the harness page regenerates from the merged map (`merge-to-master.sh`) | Nothing by itself: two lanes adding cells collided as text and the regenerated page lost rule 26's race; the merge now keeps master's cells plus the branch's, minus what the branch removed and master left, and regenerates the page | 8 Oct 2026 |
| a push that lost the ref race retries without re-running the hook (`merge-to-master.sh` `push_race`, 12 tries) | Nothing by itself: under one landing a minute a 70-second hook per try never won master's compare-and-swap (Review B's landing lost three in a row); once the hook has passed on the first try and the rejection is a ref race, later tries push --no-verify (both parents gated) | 8 Oct 2026 |
| the REV suite is generated from an external review's findings.json and dispatch.md (`review-suite.mjs`; one case per required regression, NOT RUN, the owner from the dispatch table) | A registry whose REV suite differs from the generator's output (--check) | 8 Oct 2026 |
| a registry landing carries its batches (`tools/ci/batches/<run id>.json`; `merge-to-master.sh` replays them onto master's copy at the merge) | Nothing by itself: the registry is a hot file, and a branch whose own copy of it was recorded during a seven-minute gate lost the race to another lane's rows three times in a row (8 Oct 2026, 19:1x UK). A branch that adds batch files is merged with master's registry, every added batch replayed through `test-record.mjs --record` (idempotent), and the evidence rules run on the merged result; rule 26 does not bind the registry path for such a branch | 8 Oct 2026 |
| the registry's evidence rules (`registry-evidence-check.sh`, called by `merge-to-master.sh` after rule 26) | A landing that sets a case's run_status to PASS without an evidence_path that exists (in the tree at the landing, or on a build box over ssh; a box that does not answer is a line, not a refusal); a landing that changes a file under docs/analysis/ or a path a registry row names without moving that row's `updated` (the row and its evidence move together, GOV-04); a PASS whose evidence record pins another manifest than the registry's pinned_manifest_sha (stale evidence reads NOT RUN, GOV-08); a run_status written while the registry carries no approval block (thresholds before results, GOV-02) | 8 Oct 2026 |
| the acceptance layer (`test-record.mjs`, `test-map.json`, `test-map-doc.mjs`; the founder's Test and Acceptance Standard, docs/plans/igneum-2.0-test-registry.json) | An automated case of the registry with no cell in the map and no NOT RUN reason; a map naming an unknown case; a stale harness-map page (generated from the JSON); the recorder's self-test: a run batch writes run_status, run_id, evidence_path, updated and the evidence record to the mapped cases only, never an accept text, and a case with no harness reads NOT RUN with its reason, never PASS by inference | 8 Oct 2026 |

View file

@ -0,0 +1,52 @@
{
"run_id": "201-7cfa422a-aa0e0f45",
"manifest_sha": "7cfa422a",
"cut_tip": "aa0e0f45 (miner; cells on 9c844503, the crate identical); node 7cfa422a on the key-succession pairing",
"evidence_dir": "build-1:/srv/artefacts/tas/201-7cfa422a-aa0e0f45",
"boxes": [
"build-2",
"build-4"
],
"cells": [
{
"cell": "suite:pow",
"status": "RUNNING",
"evidence": "build-1:/srv/artefacts/tas/201-7cfa422a-aa0e0f45/miner-9c844503/box2-pow.log"
},
{
"cell": "suite:app",
"status": "RUNNING",
"evidence": "build-1:/srv/artefacts/tas/201-7cfa422a-aa0e0f45/miner-9c844503/box2-app.log"
},
{
"cell": "suite:core",
"status": "RUNNING",
"evidence": "build-1:/srv/artefacts/tas/201-7cfa422a-aa0e0f45/node-7cfa422a/box4-core.log"
},
{
"cell": "suite:consensus",
"status": "RUNNING",
"evidence": "build-1:/srv/artefacts/tas/201-7cfa422a-aa0e0f45/node-7cfa422a/box4-consensus.log"
},
{
"cell": "suite:exec",
"status": "RUNNING",
"evidence": "build-1:/srv/artefacts/tas/201-7cfa422a-aa0e0f45/node-7cfa422a/box4-exec.log"
},
{
"cell": "suite:miner",
"status": "RUNNING",
"evidence": "build-1:/srv/artefacts/tas/201-7cfa422a-aa0e0f45/node-7cfa422a/box4-miner.log"
},
{
"cell": "suite:p2p-flows",
"status": "RUNNING",
"evidence": "build-1:/srv/artefacts/tas/201-7cfa422a-aa0e0f45/node-7cfa422a/box4-p2p-flows.log"
},
{
"cell": "check:freeze",
"status": "RUNNING",
"evidence": "docs/plans/igneum-2.0-f0-manifest.md; packaging/pow-freeze.txt; build-1:/srv/artefacts/tas/201-7cfa422a-aa0e0f45/node-7cfa422a/box4-kaspad-check.log"
}
]
}

View file

@ -11,6 +11,12 @@
"status": "PASS",
"evidence": "sim/finality-attacks-results/v4-pause-fix.md",
"note": "the node lane's rule v4 harness on real nodes (tools/finality-attacks/v3.mjs split50 and split70, three igneumd on the 60x file, six voters, WARM 230 SPLIT 420 HEAL 400 BPS 1): the pause-only run on the fixed pair 12424341 (the pause fix 6872db13) locks nothing during the split and resumes after the heal (lock 7 at the reconnect, 25 by the window's end), 0 conflicting certificates, 0 disagreeing locks; the recovery runs lock exactly the side above half of the anchored table (54.62 percent this time, 51.26 the other way on bee41b5e) with every node on that chain after the heal; split70 as before; rule v3's known-failed line fails as it must (both sides lock alone, 7 disagreeing locks). The first v4-pause run on bee41b5e read the node fault (a permanent pause past one window with the recovery off) that the fix closed. Rows: sim/results_v2.md, Rule v4, the two real-node tables; the seven result files beside this evidence path."
},
{
"cell": "harness:finality-sim",
"status": "RUNNING",
"evidence": "sim/results_v2.md",
"note": "FIN-08 through the finality-sim cell: the node lane's real-node rows (the Rule v4 section's two real-node tables and sim/finality-attacks-results/) join the simulator's scenario N rows; the combined-boundary and adversarial-schedule runs on real nodes (the 40/40/20 case past the window with equivocation and dust-valid mining, the pause-only alternative with backfill) are tomorrow's daylight plan on build-7/8/9; RUNNING until those rows are in"
}
]
}

View file

@ -74,7 +74,11 @@ the acceptance recorder: a run batch writes the registry's live fields and never
the test map: every automated case of the registry maps to a cell or carries a NOT RUN reason; the map's ids exist
the harness map page is generated from tools/ci/test-map.json and current
P01 part A, the million-vector driver: a clean run is PASS, one wrong hash or one unanswered nonce is FAIL naming it (self-test, a fake worker)
the proving outcome ledger (review B F08): every claimed job ends in one outcome; the report's self-test reads a log and a state file to known numbers
the registry's evidence rules: a PASS names evidence that exists, a touched evidence file moves with its row, stale evidence never reads PASS, a run_status needs the approval (self-test)
F02 (Review B): the proof-rule test bypass cannot reach a release build: an env read with no cfg guard or under a default feature is red; a release binary carrying the bypass string is red (self-test)
the test map merges structurally at a landing: master's cells plus the branch's, minus what the branch removed and master left (self-test)
the REV suite is generated from Review B's findings and dispatch and matches them (self-test, then the tree)
the public ledger (docs/ledger-public.md) is what docs/fud-ledger.md generates: one row per item, no commit ids, times or team names (self-test first)
the ledger page reads both entry heading forms (M1 and AP-F8-1) so no in-house pass row is dropped from /ledger (known-failed first)
every workflow job carries timeout-minutes (site 15, changes 10, pow 60, sims 45; the hung-job class of 7 October 2026)

View file

@ -43,3 +43,4 @@ docs/design/app-audit-2026-10-08.md
docs/ledger-public-pre-2.0.md
# 8 October 2026: the Devnet 3 proving pipeline record (the fleet lane: box names, the operations record, the external review quoted)
docs/analysis/proving-pipeline-2026-10-08.md
docs/analysis/proving-outcome-ledger.md

View file

@ -26,7 +26,7 @@ MERGE_PID_FILE="$(git rev-parse --git-dir)/igneum-merge.pid"
printf '%s %s %s\n' "$$" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "${IGNEUM_MERGE_TITLE:-untitled}" > "$MERGE_PID_FILE" 2>/dev/null || true
trap 'rm -f "$MERGE_PID_FILE"' EXIT
. tools/ci/gh-env.sh # every gh call here reads Igneum's own gh directory, never the founder's (8 October 2026)
BRANCH="$(git rev-parse --abbrev-ref HEAD)"; TRIES=6; CI_WAIT_MIN="${CI_WAIT_MIN:-25}"; FIXES_MASTER=0; SELF_TEST=0; REMOTE="${MERGE_REMOTE:-origin}"
BRANCH="$(git rev-parse --abbrev-ref HEAD)"; TRIES=12; CI_WAIT_MIN="${CI_WAIT_MIN:-25}"; FIXES_MASTER=0; SELF_TEST=0; REMOTE="${MERGE_REMOTE:-origin}"; NOVERIFY=""
while [ $# -gt 0 ]; do case "$1" in --tries) TRIES="$2"; shift 2 ;; --ci-wait) CI_WAIT_MIN="$2"; shift 2 ;; --fixes-master) FIXES_MASTER=1; shift ;; --remote) REMOTE="$2"; shift 2 ;; --self-test) SELF_TEST=1; shift ;; -*) echo "unknown option $1" >&2; exit 2 ;; *) BRANCH="$1"; shift ;; esac; done
# --remote <name>: land on another remote's master (a box mirror, build@<box>:/srv/igneum.git, while GitHub is unreachable; main's
# ruling of 7 October 2026, 19:5x UK). CI runs on GitHub only, so the CI rule binds the origin remote; on a mirror the box gate
@ -107,21 +107,59 @@ mirror_master() { # <sha> [landed-remote-url]: the landed master to every othe
# merge_with_batches <tip> <sha> <message>: in the current worktree (at <tip>), the merge of <sha>; when the branch added batch files,
# the registry takes master's copy and every batch is replayed onto it, then the evidence rules run on the result; returns 1 on a
# conflict outside the registry or a red evidence rule
# push_race <push output>: 0 when a rejected push lost only the ref's compare-and-swap (another landing moved master between the
# fetch and the push), 1 when the hook refused or something else failed. On a race the merge is rebuilt on the new tip; the hook
# already passed on the first try and both parents are gated (the branch fully, master's tip on its own landing), so the retry
# pushes with --no-verify: under tonight's landing rate (one every minute, 8 October 2026, 20:0x UK) a 70-second hook per try
# never wins the swap
push_race() { case "$1" in *"REFUSED"*|*" RED "*) return 1 ;; *"cannot lock ref"*|*"failed to update ref"*|*"fetch first"*|*"non-fast-forward"*) return 0 ;; *) return 1 ;; esac; }
merge_with_batches() {
local tip="$1" sha="$2" msg="$3" conflicts f
local MAP_CHANGED="${MAP_CHANGED:-0}" MAP_PATH="${MAP_PATH:-tools/ci/test-map.json}" PAGE_PATH="${PAGE_PATH:-docs/plans/igneum-2.0-test-harness-map.md}" BATCHES="${BATCHES:-}" NOTES="${NOTES:-}" REGISTRY_PATH="${REGISTRY_PATH:-docs/plans/igneum-2.0-test-registry.json}"
if git "${AUTHOR[@]}" merge -q --no-ff --no-commit "$sha" >/dev/null 2>&1; then :; else
conflicts=$(git diff --name-only --diff-filter=U)
if [ -z "$BATCHES" ] || [ "$conflicts" != "$REGISTRY_PATH" ]; then git merge --abort 2>/dev/null; return 1; fi
local c ok=1
for c in $conflicts; do
case "$c" in
"$REGISTRY_PATH") [ -n "$BATCHES" ] || ok=0 ;; # rebuilt from master's copy below
"$PAGE_PATH") [ "$MAP_CHANGED" = 1 ] || ok=0 ;; # regenerated from the merged map below
"$MAP_PATH") [ "$MAP_CHANGED" = 1 ] || ok=0 ;; # merged structurally below (tools/ci/test-map-merge.py)
*) ok=0 ;;
esac
done
if [ "$ok" != 1 ]; then git merge --abort 2>/dev/null; return 1; fi
[ -n "$BATCHES" ] || git checkout -q "$tip" -- "$REGISTRY_PATH" 2>/dev/null || true
fi
if [ "$MAP_CHANGED" = 1 ] && git diff --name-only --diff-filter=U 2>/dev/null | grep -qx "$MAP_PATH"; then
local base3; base3=$(git merge-base "$tip" "$sha")
git show "$base3:$MAP_PATH" > /tmp/map-base.$$ ; git show "$tip:$MAP_PATH" > /tmp/map-master.$$ ; git show "$sha:$MAP_PATH" > /tmp/map-branch.$$
python3 tools/ci/test-map-merge.py /tmp/map-base.$$ /tmp/map-master.$$ /tmp/map-branch.$$ "$MAP_PATH" || { echo "merge-to-master: the map does not merge structurally" >&2; git merge --abort 2>/dev/null; return 1; }
rm -f /tmp/map-base.$$ /tmp/map-master.$$ /tmp/map-branch.$$; git add "$MAP_PATH"; echo "merge-to-master: merged $MAP_PATH structurally (master's cells plus the branch's)"
fi
if [ -n "$BATCHES" ]; then
git checkout -q "$tip" -- "$REGISTRY_PATH" # master's copy, never the branch's
if [ -f tools/ci/review-suite.mjs ] && [ -f docs/analysis/review-2026-10-08-b/findings.json ]; then
node tools/ci/review-suite.mjs --findings docs/analysis/review-2026-10-08-b/findings.json --dispatch docs/analysis/review-2026-10-08-b/dispatch.md --prefix REV --write >/dev/null || { echo "merge-to-master: the REV suite does not regenerate on the merged tree" >&2; git merge --abort 2>/dev/null; return 1; }
echo "merge-to-master: regenerated the REV suite on master's registry"
fi
for f in $BATCHES; do
[ "$f" = . ] && continue
git checkout -q "$sha" -- "$f"
node tools/ci/test-record.mjs --record "$f" >/dev/null || { echo "merge-to-master: the batch $f does not replay onto master's registry" >&2; git merge --abort 2>/dev/null; return 1; }
echo "merge-to-master: replayed $f onto master's registry"
done
for f in ${NOTES:-}; do
git checkout -q "$sha" -- "$f"
node tools/ci/test-record.mjs --note-file "$f" >/dev/null || { echo "merge-to-master: the note $f does not apply" >&2; git merge --abort 2>/dev/null; return 1; }
echo "merge-to-master: replayed the note $f"
done
git add -A
fi
if [ "$MAP_CHANGED" = 1 ] && [ -f tools/ci/test-map-doc.mjs ]; then
git checkout -q "$tip" -- "$PAGE_PATH" 2>/dev/null || true # start from master's page; the generator overwrites it from the merged map
node tools/ci/test-map-doc.mjs >/dev/null || { echo "merge-to-master: the harness map page does not regenerate from the merged map" >&2; git merge --abort 2>/dev/null; return 1; }
echo "merge-to-master: regenerated $PAGE_PATH from the merged map"; git add -A
fi
git "${AUTHOR[@]}" commit -q -m "$msg" || return 1
if [ -n "$BATCHES" ]; then
bash tools/ci/registry-evidence-check.sh "$tip" HEAD || { echo "merge-to-master: REFUSED by the registry's evidence rules on the merged registry (above)" >&2; return 1; }
@ -208,7 +246,7 @@ success 4 u push run
# the merge takes master's copy and replays the batch, so both rows land (the hot-file race, 8 October 2026, 19:1x UK)
rb="$d/rb"; mkdir -p "$rb" && ( cd "$rb" && git init -q -b master . && mkdir -p docs/plans tools/ci/batches && cp "$ROOT/tools/ci/test-record.mjs" tools/ci/ && cp "$ROOT/tools/ci/registry-evidence-check.sh" tools/ci/ && cp "$ROOT/tools/ci/docs-only-check.sh" tools/ci/ 2>/dev/null
printf '{"approval":"yes","suites":[{"code":"X","tests":[{"id":"X-1","method":"Automated","accept":"a"},{"id":"X-2","method":"Automated","accept":"b"}]}]}\n' > docs/plans/igneum-2.0-test-registry.json
printf '{"cells":{"c1":{"command":"x","box_class":"b","fixtures":[],"cases":["X-1"]},"c2":{"command":"y","box_class":"b","fixtures":[],"cases":["X-2"]}},"not_run":{}}\n' > tools/ci/test-map.json
printf '{"title":"t","registry":"docs/plans/igneum-2.0-test-registry.json","rule":"r","cells":{"c1":{"command":"x","box_class":"b","fixtures":[],"cases":["X-1"]},"c2":{"command":"y","box_class":"b","fixtures":[],"cases":["X-2"]}},"not_run":{}}\n' > tools/ci/test-map.json
git add -A && git -c user.name=t -c user.email=t@t commit -q -m base && git tag base
git checkout -q -b branch; printf '{"run_id":"r-branch","manifest_sha":"m","cells":[{"cell":"c1","status":"RUNNING","evidence":"tools/ci/test-map.json"}]}\n' > tools/ci/batches/r-branch.json
node tools/ci/test-record.mjs --record tools/ci/batches/r-branch.json >/dev/null && git add -A && git -c user.name=t -c user.email=t@t commit -q -m "batch r-branch"
@ -217,7 +255,24 @@ success 4 u push run
out=$( cd "$rb" && TIP=$(git rev-parse master) && SHA=$(git rev-parse branch) && BASE=$(git merge-base "$TIP" "$SHA") && BATCHES=$(git diff --name-only --diff-filter=A "$BASE" "$SHA" -- tools/ci/batches/ | grep -E '\.json$') && REGISTRY_PATH=docs/plans/igneum-2.0-test-registry.json && AUTHOR=(-c user.name=t -c user.email=t@t) && export TEST_RECORD_ROOT="$rb" && merge_with_batches "$TIP" "$SHA" "merge with replay" 2>&1 && python3 -c "
import json; d=json.load(open('docs/plans/igneum-2.0-test-registry.json')); t={c['id']:c.get('run_id') for s in d['suites'] for c in s['tests']}; print('rows', t)" )
case "$out" in *"'X-1': 'r-branch'"*"'X-2': 'r-master'"*|*"'X-2': 'r-master'"*"'X-1': 'r-branch'"*) ;; *) echo "self-test failed: the batch replay did not land both the branch's row and master's row: $out"; fails=1 ;; esac
[ "$fails" = 0 ] && echo "self-test passed: a GitHub remote is refused with exit 2 while the suspension marker stands and a mirror remote is not; the CI rule binds a GitHub remote only; a landed master is fast-forwarded to every mirror and a refused mirror push is a line, not a failure; a green branch run merges; a red one is refused; an unknown read is waited through and refused only at the deadline; a queued run is waited for with the clock; an unrun branch is pushed once and then waited for; a red master refuses every merge but the declared fix; a registry landing's batches replay onto master's copy at the merge"
# the page race: the branch adds cell c3 to the map (page regenerated), master adds c4 (page regenerated); the merge regenerates the page with both
( cd "$rb" && cp "$ROOT/tools/ci/test-map-doc.mjs" "$ROOT/tools/ci/test-map-merge.py" tools/ci/ && mkdir -p docs/plans && git checkout -q master && node tools/ci/test-map-doc.mjs >/dev/null 2>&1; git add -A; git -c user.name=t -c user.email=t@t commit -q -m page0; git tag pbase
git checkout -q -b pb; python3 -c "import json; m=json.load(open('tools/ci/test-map.json')); m['cells']['c3']={'command':'z','box_class':'b','fixtures':[],'cases':['X-1']}; json.dump(m,open('tools/ci/test-map.json','w'))"; node tools/ci/test-map-doc.mjs >/dev/null 2>&1; git add -A; git -c user.name=t -c user.email=t@t commit -q -m c3
git checkout -q master; python3 -c "import json; m=json.load(open('tools/ci/test-map.json')); m['cells']['c4']={'command':'w','box_class':'b','fixtures':[],'cases':['X-2']}; json.dump(m,open('tools/ci/test-map.json','w'))"; node tools/ci/test-map-doc.mjs >/dev/null 2>&1; git add -A; git -c user.name=t -c user.email=t@t commit -q -m c4 ) >/dev/null 2>&1 || { echo "self-test failed: the page-race fixture did not build"; fails=1; }
out=$( cd "$rb" && TIP=$(git rev-parse master) && SHA=$(git rev-parse pb) && BASE=$(git merge-base "$TIP" "$SHA") && BATCHES="" && MAP_CHANGED=1 && MAP_PATH=tools/ci/test-map.json && PAGE_PATH=docs/plans/igneum-2.0-test-harness-map.md && REGISTRY_PATH=docs/plans/igneum-2.0-test-registry.json && AUTHOR=(-c user.name=t -c user.email=t@t) && export TEST_RECORD_ROOT="$rb" && merge_with_batches "$TIP" "$SHA" "merge with page" 2>&1 && grep -c -E '^### c[34]$' docs/plans/igneum-2.0-test-harness-map.md )
case "$out" in *regenerated*2) ;; *) echo "self-test failed: the merge did not regenerate the page with both sides' cells: $out"; fails=1 ;; esac
# both at once: the branch records a batch (its registry copy conflicts with master's) and adds a map cell; the page must regenerate
# after the registry is rebuilt, never from a copy with conflict markers (8 October 2026, 20:24 UK: the REV landing lost to this)
( cd "$rb" && git checkout -q -b both pbase && printf '{"run_id":"r-both","manifest_sha":"m","cells":[{"cell":"c1","status":"RUNNING","evidence":"tools/ci/test-map.json"}]}\n' > tools/ci/batches/r-both.json && node tools/ci/test-record.mjs --record tools/ci/batches/r-both.json >/dev/null && python3 -c "import json; m=json.load(open('tools/ci/test-map.json')); m['cells']['c5']={'command':'v','box_class':'b','fixtures':[],'cases':['X-1']}; json.dump(m,open('tools/ci/test-map.json','w'))" && node tools/ci/test-map-doc.mjs >/dev/null 2>&1; git add -A; git -c user.name=t -c user.email=t@t commit -q -m both ) >/dev/null 2>&1
out=$( cd "$rb" && TIP=$(git rev-parse master) && SHA=$(git rev-parse both) && BASE=$(git merge-base "$TIP" "$SHA") && BATCHES=$(git diff --name-only --diff-filter=A "$BASE" "$SHA" -- tools/ci/batches/ | grep -E '\.json$') && MAP_CHANGED=1 && MAP_PATH=tools/ci/test-map.json && PAGE_PATH=docs/plans/igneum-2.0-test-harness-map.md && REGISTRY_PATH=docs/plans/igneum-2.0-test-registry.json && AUTHOR=(-c user.name=t -c user.email=t@t) && export TEST_RECORD_ROOT="$rb" && merge_with_batches "$TIP" "$SHA" "merge both" 2>&1 && grep -c -E '^### c[45]$' docs/plans/igneum-2.0-test-harness-map.md && python3 -c "import json; d=json.load(open('docs/plans/igneum-2.0-test-registry.json')); print('ok' if any(c.get('run_id')=='r-both' for s in d['suites'] for c in s['tests']) else 'no-batch')" )
case "$out" in *replayed*2*ok*) ;; *) echo "self-test failed: a landing with both a batch and a map change did not land both (the page before the registry rebuild?): $out"; fails=1 ;; esac
push_race "To x
! [remote rejected] HEAD -> master (failed to update ref)
remote: error: cannot lock ref 'refs/heads/master': is at a but expected b" || { echo "self-test failed: a lost compare-and-swap was not read as a race"; fails=1; }
push_race "pre-push gate: REFUSED. master takes only a commit whose own ci run is green" && { echo "self-test failed: a hook refusal was read as a race"; fails=1; }
push_race " RED 3s no conflict markers in tracked files
error: failed to push some refs" && { echo "self-test failed: a red check was read as a race"; fails=1; }
[ "$fails" = 0 ] && echo "self-test passed: a GitHub remote is refused with exit 2 while the suspension marker stands and a mirror remote is not; the CI rule binds a GitHub remote only; a landed master is fast-forwarded to every mirror and a refused mirror push is a line, not a failure; a green branch run merges; a red one is refused; an unknown read is waited through and refused only at the deadline; a queued run is waited for with the clock; an unrun branch is pushed once and then waited for; a red master refuses every merge but the declared fix; a registry landing's batches replay onto master's copy at the merge; a map change regenerates the harness page at the merge; a push that lost the ref race retries without re-running the hook"
exit $fails
fi
if [ "$SELF_TEST" != 1 ] && github_suspended_refusal "$REMOTE"; then exit 2; fi
@ -250,7 +305,16 @@ bash tools/ci/rule24-crate-gate.sh "$BASE" "$SHA" || { echo "merge-to-master: RE
# copy is never what lands and rule 26 does not bind the registry path for such a branch (the evidence rules run on the merged result)
REGISTRY_PATH="${REGISTRY_PATH:-docs/plans/igneum-2.0-test-registry.json}"
BATCHES=$(git diff --name-only --diff-filter=A "$BASE" "$SHA" -- tools/ci/batches/ | grep -E '\.json$' || true)
NOTES=$(git diff --name-only --diff-filter=A "$BASE" "$SHA" -- tools/ci/notes/ | grep -E '\.json$' || true) # {suite, text}, replayed through test-record.mjs --note-file
REVGEN=0; git diff --quiet "$BASE" "$SHA" -- tools/ci/review-suite.mjs docs/analysis/review-2026-10-08-b/findings.json docs/analysis/review-2026-10-08-b/dispatch.md 2>/dev/null || REVGEN=1 # the REV suite regenerates on the merged tree
[ -n "$NOTES" ] || [ "$REVGEN" = 1 ] && BATCHES="${BATCHES:-.}" # the registry is rebuilt from master's copy whenever any transform rides
RULE26_SKIP_PATHS=""; [ -n "$BATCHES" ] && RULE26_SKIP_PATHS="$REGISTRY_PATH"
# the harness map page is generated from tools/ci/test-map.json (test-map-doc.mjs); a branch that changed the map regenerated the
# whole page, and master's page moves under every lane (8 October 2026, 19:5x UK: the enforced-proving lane lost rule 26's race
# twice on the page alone). The merge regenerates the page from the MERGED map, as it replays batches onto master's registry.
MAP_PATH="${MAP_PATH:-tools/ci/test-map.json}"; PAGE_PATH="${PAGE_PATH:-docs/plans/igneum-2.0-test-harness-map.md}"
MAP_CHANGED=0; git diff --quiet "$BASE" "$SHA" -- "$MAP_PATH" 2>/dev/null || MAP_CHANGED=1
[ "$MAP_CHANGED" = 1 ] && RULE26_SKIP_PATHS="$RULE26_SKIP_PATHS $PAGE_PATH"
# rule 26 (8 October 2026, 17:5x UK): a site/ or docs/ path another lane landed since the branch point is merged, never replaced
RULE26_SKIP_PATHS="$RULE26_SKIP_PATHS" bash tools/ci/rule26-no-revert.sh "$BASE" "$SHA" "$REMOTE/master" || { echo "merge-to-master: REFUSED by rule 26 (above)" >&2; exit 1; }
# the registry's evidence rules (8 October 2026, 18:4x UK): a PASS carries existing evidence, a touched evidence file moves with its
@ -262,14 +326,16 @@ for i in $(seq 1 "$TRIES"); do
W=$(mktemp -d "${TMPDIR:-/tmp}/merge-to-master.XXXXXX"); rmdir "$W"
git worktree add -q --detach "$W" "$TIP"
if ( cd "$W" && merge_with_batches "$TIP" "$SHA" "Merge $BRANCH ${SHA:0:8} into master ($VERDICT)" ); then
if ( cd "$W" && git push -q "$REMOTE" HEAD:master ); then # on a GitHub remote the hook asks ci-state about ${SHA:0:8} once more
pushout=$( cd "$W" && git push "$REMOTE" HEAD:master ${NOVERIFY:+--no-verify} 2>&1 ) && pushed=1 || pushed=0
[ "$pushed" = 1 ] || { printf '%s\n' "$pushout" | grep -E 'REFUSED| RED |rejected|error' | head -4 | cut -c1-160; }
if [ "$pushed" = 1 ]; then # on a GitHub remote the hook asks ci-state about ${SHA:0:8} once more
git worktree remove --force "$W"; git fetch -q "$REMOTE" master
echo "merge-to-master: pushed on try $i: $REMOTE/master $(git log -1 --format='%h %ci' "$REMOTE/master") $(TZ=Europe/London date '+%H:%M %Z')"
# the landed master to every other mirror, whichever remote took the landing (8 October 2026, 14:0x UK: a box landing never fanned
# out, so build-3 and build-4 cut branches from a tip 23 hours old)
mirror_master "$(git rev-parse "$REMOTE/master")" "$(git remote get-url "$REMOTE" 2>/dev/null)"; exit 0
fi
echo "merge-to-master: try $i: the push was rejected (master moved or the hook was red); again"
if push_race "$pushout"; then NOVERIFY=1; echo "merge-to-master: try $i: master moved under the push (the ref's compare-and-swap lost); the hook passed, the next try pushes without re-running it"; else echo "merge-to-master: try $i: the push was refused by the hook or failed; again" >&2; fi
else
echo "merge-to-master: the merge of $BRANCH onto ${TIP:0:8} does not apply cleanly; resolve on the branch (git merge origin/master) and retry" >&2
git worktree remove --force "$W"; exit 1

View file

@ -0,0 +1,4 @@
{
"suite": "FIN",
"text": "F04 (Review B), the founder's ruling 8 October 19:57 UK: the recovery lock is kept and is always labelled 'recovery', never 'final', on every surface (the checkpoint field, the explorer, receipts, the light client, the oracle, the site)"
}

View file

@ -0,0 +1,4 @@
{
"suite": "OPS",
"text": "F14 (Review B), the founder's ruling 8 October 19:57 UK: the public miner ships from 2.0.2 without remote jobs; our own fleet runs the lab build with its own signing root"
}

View file

@ -0,0 +1,4 @@
{
"suite": "UX",
"text": "F14 (Review B), the founder's ruling 8 October 19:57 UK: the public miner ships from 2.0.2 without remote jobs; our own fleet runs the lab build with its own signing root"
}

View file

@ -173,7 +173,11 @@ tree_checks() {
run "the test map: every automated case of the registry maps to a cell or carries a NOT RUN reason; the map's ids exist" node tools/ci/test-record.mjs --check
run "the harness map page is generated from tools/ci/test-map.json and current" node tools/ci/test-map-doc.mjs --check
run "P01 part A, the million-vector driver: a clean run is PASS, one wrong hash or one unanswered nonce is FAIL naming it (self-test, a fake worker)" python3 tools/ci/p01-vectors.py --self-test
run "the proving outcome ledger (review B F08): every claimed job ends in one outcome; the report's self-test reads a log and a state file to known numbers" python3 tools/fleet/prover-outcomes.py --self-test
run "the registry's evidence rules: a PASS names evidence that exists, a touched evidence file moves with its row, stale evidence never reads PASS, a run_status needs the approval (self-test)" bash tools/ci/registry-evidence-check.sh --self-test
run "F02 (Review B): the proof-rule test bypass cannot reach a release build: an env read with no cfg guard or under a default feature is red; a release binary carrying the bypass string is red (self-test)" bash tools/ci/proof-rule-bypass-check.sh --self-test
run "the test map merges structurally at a landing: master's cells plus the branch's, minus what the branch removed and master left (self-test)" python3 tools/ci/test-map-merge.py --self-test
run "the REV suite is generated from Review B's findings and dispatch and matches them (self-test, then the tree)" bash tools/ci/review-suite-check.sh
run "the public ledger (docs/ledger-public.md) is what docs/fud-ledger.md generates: one row per item, no commit ids, times or team names (self-test first)" bash -c 'node tools/ledger/export-public.mjs --self-test && node tools/ledger/export-public.mjs --check'
run "the ledger page reads both entry heading forms (M1 and AP-F8-1) so no in-house pass row is dropped from /ledger (known-failed first)" node tools/ledger-page.mjs --self-test
run "every workflow job carries timeout-minutes (site 15, changes 10, pow 60, sims 45; the hung-job class of 7 October 2026)" bash tools/ci/workflow-timeouts-check.sh --self-test

View file

@ -0,0 +1,65 @@
#!/usr/bin/env bash
# F02 (Review B, 8 October 2026): the proof-rule test bypass (IGNEUM_TEST_SKIP_PROOF_RULE, read in the node fork's
# consensus/src/pipeline/body_processor/body_validation_in_context.rs) must never be compiled into a release build. Two rules:
# source: every occurrence of the bypass name in a .rs file of the fork sits in a file under tests/ or inside an item guarded by
# #[cfg(test)] or #[cfg(feature = "<f>")] / cfg!(feature = "<f>") where <f> is NOT in the crate's default features
# (the guard must appear in the 12 lines above the occurrence, inside the same item)
# binary: a release node binary (igneumd), when given, does not contain the bypass name as a string (strings | grep)
# tools/ci/proof-rule-bypass-check.sh <fork tree> [<release igneumd>] exit 0 clean, 1 red (every occurrence named), 2 bad args
# tools/ci/proof-rule-bypass-check.sh --self-test
set -euo pipefail
NAME="${PROOF_RULE_BYPASS_NAME:-IGNEUM_TEST_SKIP_PROOF_RULE}"
HERE="$(cd "$(dirname "$0")" && pwd -P)"; ME="$HERE/$(basename "$0")"
source_rule() { # <tree> -> prints "red <file>:<line> <why>" lines; returns 1 when any
local tree="$1" rc=0 f n guard feat crate_toml defaults
while IFS=: read -r f n _; do
[ -n "$f" ] || continue
case "$f" in */tests/*|*/benches/*) continue ;; esac
case "$(sed -n "${n}p" "$f")" in *"//"*"$NAME"*) if ! sed -n "${n}p" "$f" | grep -qE "env::var|env!\(|\"$NAME\""; then continue; fi ;; esac
guard=$(awk -v n="$n" 'NR>=n-12 && NR<n' "$f" | grep -oE '#\[cfg\(test\)\]|cfg\(feature *= *"[^"]+"\)|cfg!\(feature *= *"[^"]+"\)|cfg\(any\([^)]*feature *= *"[^"]+"[^)]*\)\)' | tail -1 || true)
if [ -z "$guard" ]; then echo "red $f:$n $NAME read with no cfg(test) or cfg(feature) guard in the 12 lines above"; rc=1; continue; fi
case "$guard" in '#[cfg(test)]') continue ;; esac
feat=$(printf '%s' "$guard" | grep -oE 'feature *= *"[^"]+"' | head -1 | sed -E 's/.*"([^"]+)"/\1/')
crate_toml=$(d="$(dirname "$f")"; while [ "$d" != / ] && [ ! -f "$d/Cargo.toml" ]; do d=$(dirname "$d"); done; echo "$d/Cargo.toml")
defaults=$(awk '/^\[features\]/{f=1;next} /^\[/{f=0} f && /^default *=/' "$crate_toml" 2>/dev/null || true)
case "$defaults" in *"\"$feat\""*) echo "red $f:$n guarded by feature \"$feat\", which is in the crate's default features ($crate_toml)"; rc=1 ;; esac
done < <(grep -rn --include='*.rs' -F "$NAME" "$tree" 2>/dev/null || true)
return $rc
}
binary_rule() { # <igneumd> -> 1 when the string is inside
local bin="$1"
if strings "$bin" 2>/dev/null | grep -qF "$NAME"; then echo "red $bin carries the string $NAME: the bypass is compiled in"; return 1; fi
return 0
}
if [ "${1:-}" = --self-test ]; then
d=$(mktemp -d); trap 'rm -rf "$d"' EXIT; fails=0
mk() { mkdir -p "$d/$1/src"; printf '[package]\nname = "c"\nversion = "0.1.0"\n[features]\ndefault = [%s]\ntest-bypass = []\n' "$2" > "$d/$1/Cargo.toml"; printf '%s\n' "$3" > "$d/$1/src/lib.rs"; }
mk unguarded '' 'fn f() -> bool { std::env::var("IGNEUM_TEST_SKIP_PROOF_RULE").is_ok() }'
mk guarded '' '#[cfg(feature = "test-bypass")]
fn f() -> bool { std::env::var("IGNEUM_TEST_SKIP_PROOF_RULE").is_ok() }
#[cfg(not(feature = "test-bypass"))]
fn f() -> bool { false }'
mk default-feature '"test-bypass"' '#[cfg(feature = "test-bypass")]
fn f() -> bool { std::env::var("IGNEUM_TEST_SKIP_PROOF_RULE").is_ok() }'
mk cfgtest '' '#[cfg(test)]
mod t { fn f() -> bool { std::env::var("IGNEUM_TEST_SKIP_PROOF_RULE").is_ok() } }'
mk comment-only '' '// the bypass IGNEUM_TEST_SKIP_PROOF_RULE is gone from this crate
fn f() -> bool { false }'
out=$(bash "$ME" "$d/unguarded" 2>&1) && { echo "self-test failed: an unguarded env read passed"; fails=1; }; case "$out" in *"no cfg(test) or cfg(feature) guard"*) ;; *) echo "self-test failed: the unguarded read was not named: $out"; fails=1 ;; esac
bash "$ME" "$d/guarded" >/dev/null 2>&1 || { echo "self-test failed: a read under a non-default feature was refused: $(bash "$ME" "$d/guarded" 2>&1)"; fails=1; }
out=$(bash "$ME" "$d/default-feature" 2>&1) && { echo "self-test failed: a read under a DEFAULT feature passed"; fails=1; }; case "$out" in *"default features"*) ;; *) echo "self-test failed: the default feature was not named: $out"; fails=1 ;; esac
bash "$ME" "$d/cfgtest" >/dev/null 2>&1 || { echo "self-test failed: a read under #[cfg(test)] was refused"; fails=1; }
bash "$ME" "$d/comment-only" >/dev/null 2>&1 || { echo "self-test failed: a comment naming the bypass was refused"; fails=1; }
printf 'ELF\0\0igneumd IGNEUM_TEST_SKIP_PROOF_RULE\0' > "$d/bad.bin"; printf 'ELF\0\0igneumd clean\0' > "$d/good.bin"
out=$(bash "$ME" "$d/guarded" "$d/bad.bin" 2>&1) && { echo "self-test failed: a binary carrying the bypass string passed"; fails=1; }; case "$out" in *"compiled in"*) ;; *) echo "self-test failed: the binary was not named: $out"; fails=1 ;; esac
bash "$ME" "$d/guarded" "$d/good.bin" >/dev/null 2>&1 || { echo "self-test failed: a clean binary was refused"; fails=1; }
[ "$fails" = 0 ] && echo "self-test passed: an env read of the bypass with no guard, or under a default feature, is red and named; a read under cfg(test) or a non-default feature passes; a comment passes; a release binary carrying the bypass string is red, a clean one passes"
exit $fails
fi
[ $# -ge 1 ] || { echo "usage: proof-rule-bypass-check.sh <fork tree> [<release igneumd>] | --self-test" >&2; exit 2; }
rc=0; out=$(source_rule "$1") || rc=1
[ -n "${2:-}" ] && { bout=$(binary_rule "$2") || rc=1; out="$out${bout:+
$bout}"; }
printf '%s\n' "$out" | sed -n 's/^red /proof-rule-bypass: RED: /p' | grep . || true
[ "$rc" = 0 ] && echo "proof-rule-bypass: every $NAME read is under cfg(test) or a non-default feature${2:+; the release binary carries no bypass string}"
exit $rc

5
tools/ci/review-suite-check.sh Executable file
View file

@ -0,0 +1,5 @@
#!/usr/bin/env bash
# The REV suite of the registry matches Review B's findings and dispatch (tools/ci/review-suite.mjs --check), self-test first.
set -euo pipefail
node tools/ci/review-suite.mjs --self-test >/dev/null
node tools/ci/review-suite.mjs --findings docs/analysis/review-2026-10-08-b/findings.json --dispatch docs/analysis/review-2026-10-08-b/dispatch.md --prefix REV --check

75
tools/ci/review-suite.mjs Normal file
View file

@ -0,0 +1,75 @@
#!/usr/bin/env node
// The REV suite of the acceptance registry: one case per required regression of an external review's findings.json, the owner from
// its dispatch.md table, the finding id and priority carried as fields, status NOT RUN until a lane records a run through the batch
// tools (main through the coordinator, 8 October 2026, 19:5x UK: Review B's 44 regressions). The registry's one structural edit
// per review: generated here, never by hand; --check refuses a registry whose REV suite differs from the generator's output.
//
// node tools/ci/review-suite.mjs --findings <findings.json> --dispatch <dispatch.md> --prefix REV [--write | --check]
// node tools/ci/review-suite.mjs --self-test
import fs from 'node:fs'; import path from 'node:path';
const args = process.argv.slice(2); const arg = (n) => { const i = args.indexOf(n); return i >= 0 ? args[i + 1] : undefined; };
const ROOT = process.env.TEST_RECORD_ROOT || path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..');
const REG = process.env.TEST_REGISTRY || path.join(ROOT, 'docs/plans/igneum-2.0-test-registry.json');
function owners(dispatchMd) { // "| F01 title | P0 | owner a, owner b | ..." -> {F01: "owner a, owner b"}
const out = {};
for (const line of dispatchMd.split('\n')) {
const m = line.match(/^\|\s*(F\d+)\b[^|]*\|\s*([^|]*)\|\s*([^|]*)\|/); if (m) out[m[1]] = m[3].trim();
}
return out;
}
function suite(findings, dispatchMd, prefix, sourceNote) {
const own = owners(dispatchMd); const tests = [];
for (const f of findings.findings || []) {
(f.required_regressions || []).forEach((line, i) => {
tests.push({ id: `${prefix}-${f.id}-${i + 1}`, title: line, setup: `The regression ${f.id} requires (review finding ${f.id}: ${f.title}).`, steps: [line],
accept: line, evidence: 'The run record of the regression as its owner lane records it through tools/ci/test-record.mjs.', priority: String(f.priority || '').split(' ')[0] || 'P1',
profile: 'P00', cadence: 'Every release candidate', method: 'Automated + independent review', status: 'NOT RUN', source: [f.id], gate: 'Review findings closed',
owner: own[f.id] || 'unassigned', manual_page: null, finding: f.id, finding_title: f.title, finding_priority: f.priority, owner_lane: own[f.id] || 'unassigned', run_status: 'NOT RUN' });
});
}
return { code: prefix, title: `${prefix}: the external review's required regressions`, source: sourceNote, gate: 'Review findings closed', owner: 'the owner lanes per the dispatch table', fixtures: ['F0', 'F5'],
summary: `${tests.length} regressions from ${(findings.findings || []).length} findings; each reads NOT RUN until its owner lane records a run`, tests };
}
function merge(reg, s) { // replace the suite of the same code, keeping live fields of cases that already exist
const old = (reg.suites || []).find((x) => x.code === s.code); const live = new Map((old?.tests || []).map((t) => [t.id, t]));
for (const t of s.tests) { const o = live.get(t.id); if (o) for (const k of ['run_status', 'run_id', 'evidence_path', 'updated', 'evidence_record']) if (k in o) t[k] = o[k]; }
reg.suites = [...(reg.suites || []).filter((x) => x.code !== s.code), s]; return reg;
}
if (args.includes('--self-test')) {
let fails = 0;
const findings = { findings: [{ id: 'F01', title: 'A', priority: 'P0 - blocker', required_regressions: ['r one', 'r two'] }, { id: 'F02', title: 'B', priority: 'P1 - x', required_regressions: ['r three'] }] };
const dispatch = '| Finding | Priority | Owner | Default |\n|---|---|---|---|\n| F01 A | P0 | lane x, lane y | d |\n| F02 B | P1 | lane z | d |\n';
const s = suite(findings, dispatch, 'REV', 'test');
if (!(s.tests.length === 3 && s.tests[0].id === 'REV-F01-1' && s.tests[2].id === 'REV-F02-1')) { console.log(`self-test failed: the ids are not <prefix>-<finding>-<n>: ${s.tests.map((t) => t.id)}`); fails = 1; }
if (!(s.tests[0].title === 'r one' && s.tests[0].accept === 'r one')) { console.log('self-test failed: the title and accept are not the regression line verbatim'); fails = 1; }
if (!(s.tests[0].owner_lane === 'lane x, lane y' && s.tests[2].owner_lane === 'lane z')) { console.log(`self-test failed: owners not read from the dispatch table: ${s.tests.map((t) => t.owner_lane)}`); fails = 1; }
if (!(s.tests[0].finding === 'F01' && s.tests[0].priority === 'P0' && s.tests[0].run_status === 'NOT RUN' && s.tests[0].method.includes('Automated'))) { console.log('self-test failed: finding, priority, NOT RUN or method missing'); fails = 1; }
const reg = { suites: [{ code: 'GOV', tests: [] }, { code: 'REV', tests: [{ id: 'REV-F01-1', run_status: 'RUNNING', run_id: 'r9' }] }] };
const m = merge(JSON.parse(JSON.stringify(reg)), s); const rev = m.suites.find((x) => x.code === 'REV');
if (!(m.suites.length === 2 && rev.tests.length === 3 && rev.tests[0].run_status === 'RUNNING' && rev.tests[0].run_id === 'r9' && rev.tests[1].run_status === 'NOT RUN')) { console.log('self-test failed: a regenerated suite did not keep the existing case\'s live fields or dropped another suite'); fails = 1; }
const map = { cells: { c1: { cases: ['REV-F01-1'] } }, not_run: { 'REV-F02-1': 'old' } }; const n = mapReasons(map, s);
if (!(n === 2 && !('REV-F01-1' in map.not_run) && /lane z/.test(map.not_run['REV-F02-1']) && /lane x/.test(map.not_run['REV-F01-2']))) { console.log(`self-test failed: the map's NOT RUN reasons: ${JSON.stringify(map.not_run)} n=${n}`); fails = 1; }
if (!fails) console.log('self-test passed: one case per required regression with the id <prefix>-<finding>-<n>, the line verbatim as title and accept, the owner from the dispatch table, finding and priority carried, NOT RUN; regenerating keeps live fields and the other suites; every unmapped case gets a NOT RUN reason naming its owner lane in the map, a mapped one loses it');
process.exit(fails);
}
const findings = JSON.parse(fs.readFileSync(arg('--findings'), 'utf8')); const dispatch = fs.readFileSync(arg('--dispatch'), 'utf8'); const prefix = arg('--prefix') || 'REV';
const s = suite(findings, dispatch, prefix, `${path.relative(ROOT, arg('--findings'))} and ${path.relative(ROOT, arg('--dispatch'))}`);
const reg = JSON.parse(fs.readFileSync(REG, 'utf8'));
if (args.includes('--check')) {
const cur = (reg.suites || []).find((x) => x.code === prefix); const want = merge(JSON.parse(JSON.stringify(reg)), s).suites.find((x) => x.code === prefix);
const canon = (o) => JSON.stringify(o, (k, v) => (v && typeof v === 'object' && !Array.isArray(v)) ? Object.fromEntries(Object.keys(v).sort().map((x) => [x, v[x]])) : v);
if (canon(cur) !== canon(want)) { console.error(`review-suite: the registry's ${prefix} suite differs from the generator's output; run --write and commit`); process.exit(1); }
console.log(`review-suite: the ${prefix} suite matches its findings (${s.tests.length} cases)`); process.exit(0);
}
const MAP = process.env.TEST_MAP || path.join(ROOT, 'tools/ci/test-map.json');
function mapReasons(map, s) { // every generated case with no cell reads NOT RUN with the owner lane named; a case a cell maps loses its reason
const mapped = new Set(Object.values(map.cells || {}).flatMap((c) => c.cases || [])); map.not_run = map.not_run || {}; let n = 0;
for (const t of s.tests) { if (mapped.has(t.id)) { delete map.not_run[t.id]; continue; } map.not_run[t.id] = `${t.finding} (${t.priority}, the external review): the regression's harness is the owner lane's (${t.owner_lane}); not yet named in the map`; n++; }
return n;
}
if (args.includes('--write')) {
fs.writeFileSync(REG, JSON.stringify(merge(reg, s), null, 2) + '\n');
let n = 0; if (fs.existsSync(MAP)) { const map = JSON.parse(fs.readFileSync(MAP, 'utf8')); n = mapReasons(map, s); fs.writeFileSync(MAP, JSON.stringify(map, null, 2) + '\n'); }
console.log(`review-suite: ${prefix} written, ${s.tests.length} cases from ${(findings.findings || []).length} findings; ${n} NOT RUN reasons in the map`); process.exit(0);
}
console.error('usage: review-suite.mjs --findings f --dispatch d [--prefix REV] --write|--check | --self-test'); process.exit(2);

44
tools/ci/test-map-merge.py Executable file
View file

@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""A structural three-way merge of tools/ci/test-map.json (8 October 2026, 20:0x UK): two lanes adding cells on adjacent lines
collide as text; as objects they do not. Result = master's map, plus every cell (and not_run entry) the branch added or changed
against the base, minus the cells the branch removed that master left as the base had them. Everything else of the map (title,
registry, rule) is master's. Usage: test-map-merge.py <base.json> <master.json> <branch.json> <out.json>; --self-test."""
import json, sys, tempfile, os
def merge(base, master, branch):
out = json.loads(json.dumps(master))
for key in ("cells", "not_run"):
b, m, r = base.get(key, {}), master.get(key, {}), branch.get(key, {})
res = dict(m)
for k, v in r.items():
if k not in b or b[k] != v:
res[k] = v
for k in b:
if k not in r and k in m and m[k] == b[k]:
del res[k]
out[key] = res
return out
def self_test():
fails = 0
base = {"title": "t", "cells": {"c1": {"a": 1}, "c2": {"a": 2}, "c9": {"a": 9}}, "not_run": {"X-5": "none"}}
master = {"title": "t2", "cells": {"c1": {"a": 1}, "c2": {"a": 2}, "c9": {"a": 9}, "c4": {"a": 4}}, "not_run": {"X-5": "none", "X-6": "m"}}
branch = {"title": "t", "cells": {"c1": {"a": 1}, "c2": {"a": 22}, "c3": {"a": 3}}, "not_run": {}} # adds c3, changes c2, removes c9, clears X-5
r = merge(base, master, branch)
want_cells = {"c1": {"a": 1}, "c2": {"a": 22}, "c4": {"a": 4}, "c3": {"a": 3}}
if r["cells"] != want_cells: print("self-test failed: cells:", r["cells"]); fails = 1
if r["not_run"] != {"X-6": "m"}: print("self-test failed: not_run:", r["not_run"]); fails = 1
if r["title"] != "t2": print("self-test failed: master's other fields not kept"); fails = 1
# master changed c9 too: the branch's removal does not win
master2 = json.loads(json.dumps(master)); master2["cells"]["c9"] = {"a": 99}
if "c9" not in merge(base, master2, branch)["cells"]: print("self-test failed: a cell master changed was removed by the branch"); fails = 1
if not fails: print("self-test passed: master's map plus the branch's added and changed cells and not_run entries, minus what the branch removed and master left alone; master's other fields kept")
return fails
if __name__ == "__main__":
if "--self-test" in sys.argv: sys.exit(self_test())
base, master, branch, out = (json.load(open(p)) for p in sys.argv[1:4]), None, None, None
b, m, r = base
res = merge(b, m, r)
with open(sys.argv[4], "w") as f: json.dump(res, f, indent=2); f.write("\n")
print(f"test-map-merge: {len(res.get('cells', {}))} cells, {len(res.get('not_run', {}))} NOT RUN reasons")

View file

@ -519,6 +519,50 @@
"LEAD-04": "observation window",
"LEAD-05": "observation window",
"LEAD-06": "observation window",
"LEAD-08": "observation window"
"LEAD-08": "observation window",
"REV-F01-1": "F01 (P0, the external review): the regression's harness is the owner lane's (proving lane, node lane); not yet named in the map",
"REV-F01-2": "F01 (P0, the external review): the regression's harness is the owner lane's (proving lane, node lane); not yet named in the map",
"REV-F01-3": "F01 (P0, the external review): the regression's harness is the owner lane's (proving lane, node lane); not yet named in the map",
"REV-F01-4": "F01 (P0, the external review): the regression's harness is the owner lane's (proving lane, node lane); not yet named in the map",
"REV-F02-1": "F02 (P0, the external review): the regression's harness is the owner lane's (proving lane, CI steward); not yet named in the map",
"REV-F02-2": "F02 (P0, the external review): the regression's harness is the owner lane's (proving lane, CI steward); not yet named in the map",
"REV-F02-3": "F02 (P0, the external review): the regression's harness is the owner lane's (proving lane, CI steward); not yet named in the map",
"REV-F03-1": "F03 (P0, the external review): the regression's harness is the owner lane's (CI steward, hash lane (ProgramClass::V6 on freeze), pool lane); not yet named in the map",
"REV-F03-2": "F03 (P0, the external review): the regression's harness is the owner lane's (CI steward, hash lane (ProgramClass::V6 on freeze), pool lane); not yet named in the map",
"REV-F03-3": "F03 (P0, the external review): the regression's harness is the owner lane's (CI steward, hash lane (ProgramClass::V6 on freeze), pool lane); not yet named in the map",
"REV-F04-1": "F04 (P0, the external review): the regression's harness is the owner lane's (node lane, reference apps, site (explorer)); not yet named in the map",
"REV-F04-2": "F04 (P0, the external review): the regression's harness is the owner lane's (node lane, reference apps, site (explorer)); not yet named in the map",
"REV-F04-3": "F04 (P0, the external review): the regression's harness is the owner lane's (node lane, reference apps, site (explorer)); not yet named in the map",
"REV-F04-4": "F04 (P0, the external review): the regression's harness is the owner lane's (node lane, reference apps, site (explorer)); not yet named in the map",
"REV-F05-1": "F05 (P1, the external review): the regression's harness is the owner lane's (hash lane, adversary lane, floor lane 3); not yet named in the map",
"REV-F05-2": "F05 (P1, the external review): the regression's harness is the owner lane's (hash lane, adversary lane, floor lane 3); not yet named in the map",
"REV-F05-3": "F05 (P1, the external review): the regression's harness is the owner lane's (hash lane, adversary lane, floor lane 3); not yet named in the map",
"REV-F06-1": "F06 (P1, the external review): the regression's harness is the owner lane's (hash lane, research lane D); not yet named in the map",
"REV-F06-2": "F06 (P1, the external review): the regression's harness is the owner lane's (hash lane, research lane D); not yet named in the map",
"REV-F06-3": "F06 (P1, the external review): the regression's harness is the owner lane's (hash lane, research lane D); not yet named in the map",
"REV-F07-1": "F07 (P1, the external review): the regression's harness is the owner lane's (app lane, fleet lane); not yet named in the map",
"REV-F07-2": "F07 (P1, the external review): the regression's harness is the owner lane's (app lane, fleet lane); not yet named in the map",
"REV-F07-3": "F07 (P1, the external review): the regression's harness is the owner lane's (app lane, fleet lane); not yet named in the map",
"REV-F08-1": "F08 (P1, the external review): the regression's harness is the owner lane's (proving lane, fleet lane, site (ops page)); not yet named in the map",
"REV-F08-2": "F08 (P1, the external review): the regression's harness is the owner lane's (proving lane, fleet lane, site (ops page)); not yet named in the map",
"REV-F08-3": "F08 (P1, the external review): the regression's harness is the owner lane's (proving lane, fleet lane, site (ops page)); not yet named in the map",
"REV-F09-1": "F09 (P1, the external review): the regression's harness is the owner lane's (research lane, floor lane 3, coordinator); not yet named in the map",
"REV-F09-2": "F09 (P1, the external review): the regression's harness is the owner lane's (research lane, floor lane 3, coordinator); not yet named in the map",
"REV-F09-3": "F09 (P1, the external review): the regression's harness is the owner lane's (research lane, floor lane 3, coordinator); not yet named in the map",
"REV-F10-1": "F10 (P2, the external review): the regression's harness is the owner lane's (worker lane (new)); not yet named in the map",
"REV-F10-2": "F10 (P2, the external review): the regression's harness is the owner lane's (worker lane (new)); not yet named in the map",
"REV-F10-3": "F10 (P2, the external review): the regression's harness is the owner lane's (worker lane (new)); not yet named in the map",
"REV-F11-1": "F11 (P2, the external review): the regression's harness is the owner lane's (Ember lane (new)); not yet named in the map",
"REV-F11-2": "F11 (P2, the external review): the regression's harness is the owner lane's (Ember lane (new)); not yet named in the map",
"REV-F11-3": "F11 (P2, the external review): the regression's harness is the owner lane's (Ember lane (new)); not yet named in the map",
"REV-F12-1": "F12 (P1, the external review): the regression's harness is the owner lane's (pool lane (new)); not yet named in the map",
"REV-F12-2": "F12 (P1, the external review): the regression's harness is the owner lane's (pool lane (new)); not yet named in the map",
"REV-F12-3": "F12 (P1, the external review): the regression's harness is the owner lane's (pool lane (new)); not yet named in the map",
"REV-F13-1": "F13 (P1, the external review): the regression's harness is the owner lane's (pool lane (new)); not yet named in the map",
"REV-F13-2": "F13 (P1, the external review): the regression's harness is the owner lane's (pool lane (new)); not yet named in the map",
"REV-F13-3": "F13 (P1, the external review): the regression's harness is the owner lane's (pool lane (new)); not yet named in the map",
"REV-F14-1": "F14 (P1, the external review): the regression's harness is the owner lane's (app lane, relay lane); not yet named in the map",
"REV-F14-2": "F14 (P1, the external review): the regression's harness is the owner lane's (app lane, relay lane); not yet named in the map",
"REV-F14-3": "F14 (P1, the external review): the regression's harness is the owner lane's (app lane, relay lane); not yet named in the map"
}
}

View file

@ -10,8 +10,10 @@
// node tools/ci/test-record.mjs --record <batch.json> batch: {run_id, manifest_sha, evidence_dir, cells:[{cell, status, evidence}]}
// each cell's case ids come from the map; the registry gains status/run/evidence/manifest
// node tools/ci/test-record.mjs --cases <cell> the case ids a matrix cell answers (for the matrix scripts' column)
// node tools/ci/test-record.mjs --note <suite code> "<text>" append a dated note to a suite's notes (a ruling, a review finding's
// disposition); never a case's accept text
// node tools/ci/test-record.mjs --self-test
import fs from 'node:fs'; import path from 'node:path';
import fs from 'node:fs'; import path from 'node:path'; import child_process from 'node:child_process';
const args = process.argv.slice(2); const arg = (n) => { const i = args.indexOf(n); return i >= 0 ? args[i + 1] : undefined; };
const ROOT = process.env.TEST_RECORD_ROOT || path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..');
const REG = process.env.TEST_REGISTRY || path.join(ROOT, 'docs/plans/igneum-2.0-test-registry.json');
@ -45,7 +47,9 @@ function record(reg, map, batch) {
touched.push(id);
}
}
for (const [id, reason] of Object.entries(map.not_run || {})) { const c = byId.get(id); if (c && (!c.run_status || c.run_status === 'NOT RUN')) { c.run_status = 'NOT RUN'; c.evidence_record = { reason, at: now }; c.updated = now; } }
// a NOT RUN row is stamped only when its status or reason changes (8 October 2026, 20:1x UK: re-stamping every NOT RUN row on every
// run made each lane's landing collide on 39 rows it never touched)
for (const [id, reason] of Object.entries(map.not_run || {})) { const c = byId.get(id); if (c && (!c.run_status || c.run_status === 'NOT RUN') && !(c.run_status === 'NOT RUN' && c.evidence_record?.reason === reason)) { c.run_status = 'NOT RUN'; c.evidence_record = { reason, at: now }; c.updated = now; } }
return touched;
}
const acceptSnapshot = (reg) => JSON.stringify(casesOf(reg).map((c) => { const o = { id: idOf(c) }; for (const k of ACCEPT_KEYS) if (k in c) o[k] = c[k]; return o; }));
@ -63,17 +67,31 @@ if (args.includes('--self-test')) {
if (acceptSnapshot(reg) !== before) { console.log('self-test failed: a record changed an accept text'); fails = 1; }
if (!(touched.length === 1 && reg.cases[0].run_status === 'PASS' && reg.cases[0].run_id === 'r1' && reg.cases[0].evidence_record.manifest_sha === 'abc' && reg.cases[0].evidence_path === '/e/pow.log' && reg.cases[0].updated)) { console.log(`self-test failed: the run was not written to the mapped case's live fields: ${JSON.stringify(reg.cases[0])}`); fails = 1; }
if (!(reg.cases[1].run_status === 'NOT RUN' && /corpus/.test(reg.cases[1].evidence_record.reason))) { console.log('self-test failed: an unmapped Automated case did not read NOT RUN with its reason'); fails = 1; }
const stamp1 = reg.cases[1].updated; record(reg, map, { run_id: 'r1b', manifest_sha: 'abc', evidence_dir: '/e', cells: [{ cell: 'pow', status: 'PASS', evidence: '/e/pow.log' }] });
if (reg.cases[1].updated !== stamp1) { console.log('self-test failed: an unchanged NOT RUN row was re-stamped on a later run'); fails = 1; }
if (reg.cases[2].run_status) { console.log('self-test failed: a manual case was given a run status'); fails = 1; }
const regS = { suites: [{ code: 'X', tests: [{ id: 'X-1', method: 'Automated', accept: 'a' }] }] }; if (casesOf(regS).length !== 1) { console.log('self-test failed: the suites/tests registry shape was not read'); fails = 1; }
const regN = { suites: [{ code: 'FIN', tests: [{ id: 'F-1', method: 'Automated', accept: 'keep' }] }] }; fs.writeFileSync(`${d}/regn.json`, JSON.stringify(regN));
const nr = child_process.spawnSync(process.execPath, [new URL(import.meta.url).pathname, '--note', 'FIN', 'the ruling'], { env: { ...process.env, TEST_REGISTRY: `${d}/regn.json`, TEST_MAP: `${d}/map.json` }, encoding: 'utf8' });
const regN2 = JSON.parse(fs.readFileSync(`${d}/regn.json`, 'utf8'));
if (!(nr.status === 0 && regN2.suites[0].notes?.length === 1 && regN2.suites[0].notes[0].text === 'the ruling' && regN2.suites[0].tests[0].accept === 'keep')) { console.log(`self-test failed: --note did not append a dated note to the suite and keep the accept text: ${nr.stdout} ${nr.stderr}`); fails = 1; }
let threw = false; try { record(reg, map, { run_id: 'r2', manifest_sha: 'x', cells: [{ cell: 'ghost', status: 'PASS' }] }); } catch { threw = true; }
if (!threw) { console.log('self-test failed: a batch naming a cell not in the map was accepted'); fails = 1; }
fs.rmSync(d, { recursive: true, force: true });
if (!fails) console.log('self-test passed: a complete map checks; an unknown case id and an unmapped Automated case are refused; a run batch writes run_status, run_id, evidence_path, updated and the evidence record to the mapped cases only, leaves every accept text byte-identical, gives an unmapped Automated case NOT RUN with its reason and a manual case nothing; a batch naming an unknown cell is refused');
if (!fails) console.log('self-test passed: a complete map checks; an unknown case id and an unmapped Automated case are refused; a run batch writes run_status, run_id, evidence_path, updated and the evidence record to the mapped cases only, leaves every accept text byte-identical, gives an unmapped Automated case NOT RUN with its reason and a manual case nothing; a batch naming an unknown cell is refused; --note appends a dated note to a suite and never touches an accept text; an unchanged NOT RUN row is not re-stamped');
process.exit(fails);
}
const reg = load(REG); const map = load(MAP);
if (args.includes('--check')) { const r = check(reg, map); for (const l of r.lines) console.error(`test-record: ${l}`); console.log(`test-record: ${r.automated} Automated cases, ${r.mapped} mapped to cells, ${r.notRun} NOT RUN with a reason${r.bad ? `, ${r.bad} problems` : ''}`); process.exit(r.bad ? 1 : 0); }
if (arg('--cases')) { console.log(((map.cells || {})[arg('--cases')]?.cases || []).join(',')); process.exit(0); }
if (arg('--note-file')) { const n = load(arg('--note-file')); args.push('--note', n.suite, n.text); }
if (arg('--note')) {
const code = arg('--note'); const text = args[args.indexOf('--note') + 2]; const suite = (reg.suites || []).find((s) => s.code === code);
if (!suite || !text) { console.error(`test-record: --note needs a suite code in the registry and a text (got ${code}, ${text ? 'text' : 'no text'})`); process.exit(2); }
const before = acceptSnapshot(reg); suite.notes = suite.notes || []; suite.notes.push({ at: new Date().toISOString(), text });
if (acceptSnapshot(reg) !== before) { console.error('test-record: REFUSED: the note would change an accept text'); process.exit(1); }
fs.writeFileSync(REG, JSON.stringify(reg, null, 2) + '\n'); console.log(`test-record: note ${suite.notes.length} on ${code}: ${text.slice(0, 80)}`); process.exit(0);
}
if (arg('--record')) {
const batch = load(arg('--record')); const before = acceptSnapshot(reg); const touched = record(reg, map, batch);
if (acceptSnapshot(reg) !== before) { console.error('test-record: REFUSED: the record would change an accept text'); process.exit(1); }

View file

@ -16,6 +16,14 @@ proving-v1, 272b025) and tools/proving-v1/pc2-segments.ps1 around the four binar
(sign-segment-record, igneum_submitSegmentRecord) once every shard is accepted and the statement equals the node's.
5. the paid state of every submitted segment polled each pass (igneum_getSegmentRecords); a state file for the
collector: /root/fleet/out/prover-state.json; every event a RESULT line in /root/fleet/out/prover.log.
6. the outcome ledger (review B F08, 8 October 2026): every claimed segment is an eligible job and ends in exactly one
outcome, paid, expired (its deadline passed with no paid record: unpaid after the submit, or held past it) or
cancelled (this prover gave it up for a cause: disk, export, cut, chain, timeout, shards, statement, sign,
refused); until then it is active. Each segment row carries outcome, cause, deadline, the margin at the claim,
the seconds spent (export, cut, chain; wasted unless paid) and the deadline miss in DAA; the state carries the
counters (outcomes, wasted_s by cause, deadline_misses) and every close is a RESULT outcome line.
tools/fleet/prover-outcomes.py reads the state files and the logs into the report (paid completions, missed
deadlines, wasted work by cause, accepted-proof throughput).
Env: LABEL (the key label, kept for the box's life), WALLET (payout), THRESHOLD (element threshold or empty),
MINER (keep|pause), RUN_HOURS (default 9).
@ -105,7 +113,23 @@ for _ in range(120):
say(f"RESULT node {stamp()} {w}")
miner_start()
state = {"passes": 0, "claimed": 0, "submitted": 0, "paid": 0, "paid_wei": 0, "shards_accepted": 0, "shards_refused": 0, "segment_refused": 0, "held": 0,
"last_segment_s": 0, "segments": [], "started": stamp(), "label": LABEL, "wallet": WALLET, "key": kh}
"last_segment_s": 0, "segments": [], "started": stamp(), "label": LABEL, "wallet": WALLET, "key": kh,
"outcomes": {"paid": 0, "active": 0, "expired": 0, "cancelled": 0}, "wasted_s": {}, "deadline_misses": 0}
OUTCOMES = ("paid", "expired", "cancelled")
def seg_row(first): return next((x for x in state["segments"] if x["first"] == first), None)
def close(first, outcome, cause=None, tip=None):
"""The outcome ledger's close (docstring point 6): one outcome per claimed segment, never a second one."""
x = seg_row(first)
if x is None or x.get("outcome") in OUTCOMES: return
spent = round(float(x.get("export_s", 0)) + float(x.get("cut_s", 0)) + float(x.get("chain_s", 0)), 1)
x["outcome"] = outcome; x["closed_at"] = stamp(); x["spent_s"] = spent
if cause: x["cause"] = cause
if outcome == "expired" and tip is not None and x.get("deadline") is not None: x["miss_daa"] = max(0, int(tip) - int(x["deadline"])); state["deadline_misses"] += 1
if outcome != "paid":
x["wasted_s"] = spent; k = cause or outcome; state["wasted_s"][k] = round(state["wasted_s"].get(k, 0) + spent, 1)
o = state["outcomes"]; o[outcome] = o.get(outcome, 0) + 1; o["active"] = max(0, state["claimed"] - o["paid"] - o["expired"] - o["cancelled"])
say(f"RESULT outcome {stamp()} segment {first}..{x.get('last')} {outcome}" + (f" cause={cause}" if cause else "") + f" spent={spent} s"
+ (f" miss={x['miss_daa']} DAA" if "miss_daa" in x else "") + f" ledger paid={o['paid']} active={o['active']} expired={o['expired']} cancelled={o['cancelled']}")
attempted = set(); submitted = {}; held = {} # held: first -> {body file, deadline, last}
last_seg_secs = 0; t_run0 = time.time()
def save_state():
@ -163,13 +187,13 @@ while (time.time() - t_run0) / 3600 < RUN_HOURS:
say(f"RESULT paid {stamp()} segment {first}..{submitted[first]['last']} wei={wei} ({wei/1e18:.4f} IGN) carrier={hexi(rec['paid'].get('carrierNumber'))} after {int(time.time()-submitted[first]['at'])} s")
for s in state["segments"]:
if s["first"] == first: s["paid_wei"] = wei; s["paid_at"] = stamp()
del submitted[first]
close(first, "paid"); del submitted[first]
elif rec is not None and tip > submitted[first]["deadline"] + 50:
say(f"RESULT unpaid {stamp()} segment {first} past its deadline unpaid; carried={len(rec.get('carried') or [])} pool={len(rec.get('pool') or [])}"); del submitted[first]
say(f"RESULT unpaid {stamp()} segment {first} past its deadline unpaid; carried={len(rec.get('carried') or [])} pool={len(rec.get('pool') or [])}"); close(first, "expired", "unpaid", tip); del submitted[first]
# held fresh records offered again
for first in list(held):
h = held[first]
if tip > h["deadline"]: say(f"RESULT held_expired {stamp()} segment {first} deadline passed"); del held[first]; continue
if tip > h["deadline"]: say(f"RESULT held_expired {stamp()} segment {first} deadline passed"); close(first, "expired", "held_expired", tip); del held[first]; continue
rr = submit("igneum_submitSegmentRecord", h["record"], h["proof_file"])
if rr and rr.get("accepted"):
state["submitted"] += 1; submitted[first] = {"last": h["last"], "at": time.time(), "deadline": h["deadline"]}; drop_export(first, "record accepted"); say(f"RESULT submitted {stamp()} segment {first}..{h['last']} record accepted on retry (held {int(time.time()-h['since'])} s)"); del held[first]
@ -198,9 +222,11 @@ while (time.time() - t_run0) / 3600 < RUN_HOURS:
if not picked: say(f"RESULT pass {p} {stamp()} {len(cands)} candidates, none usable; waiting"); time.sleep(15); continue
first, last = picked["first"], picked["last"]; attempted.add(first); state["claimed"] += 1
say(f"RESULT claim {stamp()} segment {first}..{last} ({len(picked['shards'])} shards, {'continuing' if prev_file else 'fresh'}) margin={picked['margin']} tip={tip} candidates={len(cands)} rank_by=fnv")
seg = {"first": first, "last": last, "claimed_at": stamp(), "shards": len(picked["shards"]), "fresh": prev_file is None}; state["segments"].append(seg)
seg = {"first": first, "last": last, "claimed_at": stamp(), "shards": len(picked["shards"]), "fresh": prev_file is None,
"deadline": picked["deadline"], "margin_daa": picked["margin"], "outcome": "active"}; state["segments"].append(seg)
state["outcomes"]["active"] = max(0, state["claimed"] - state["outcomes"]["paid"] - state["outcomes"]["expired"] - state["outcomes"]["cancelled"])
prune_exports(); free = disk_free_pct()
if free < DISK_MIN_FREE_PCT: say(f"RESULT skip {stamp()} segment {first}: disk {free:.1f}% free is under the {DISK_MIN_FREE_PCT:.0f}% floor, no export"); time.sleep(60); continue
if free < DISK_MIN_FREE_PCT: say(f"RESULT skip {stamp()} segment {first}: disk {free:.1f}% free is under the {DISK_MIN_FREE_PCT:.0f}% floor, no export"); close(first, "cancelled", "disk"); time.sleep(60); continue
d = f"{OUT}/segs/seg-{first}"; os.makedirs(d, exist_ok=True); t_seg0 = time.time()
# export
# a node whose EVM restarted at a chain block (0.3.13's exec restart rule) exports from that block, not genesis: the
@ -215,7 +241,7 @@ while (time.time() - t_run0) / 3600 < RUN_HOURS:
seg["export_from"] = start_blk
r = subprocess.run(["curl", "-s", "-m", "600", "-X", "POST", EVM, "-H", "Content-Type: application/json", "--data-binary", body, "-o", f"{d}/seq.json"])
try: json.dump(json.load(open(f"{d}/seq.json"))["result"], open(f"{d}/export.json", "w"))
except Exception as e: say(f"RESULT seg {first} export FAILED {str(e)[:100]}"); continue
except Exception as e: say(f"RESULT seg {first} export FAILED {str(e)[:100]}"); seg["export_s"] = round(time.time() - t, 1); close(first, "cancelled", "export"); continue
seg["export_s"] = round(time.time() - t, 1); os.remove(f"{d}/seq.json")
# cut
t = time.time(); fixtures = []
@ -224,7 +250,7 @@ while (time.time() - t_run0) / 3600 < RUN_HOURS:
rr = subprocess.run([EXPORT, f"{d}/export.json", str(b), f"{d}/block-{b}.json", "--source", f"fleet {LABEL} live devnet, segment-aligned prover"], capture_output=True, text=True, timeout=600)
if rr.returncode != 0: say(f"RESULT seg {first} cut {b} FAILED: {(rr.stdout + rr.stderr)[-200:]}"); ok = False; break
fixtures.append(f"{d}/block-{b}.json")
if not ok: continue
if not ok: seg["cut_s"] = round(time.time() - t, 1); close(first, "cancelled", "cut"); continue
seg["cut_s"] = round(time.time() - t, 1)
# chain
if MINER == "pause": miner_stop()
@ -244,7 +270,7 @@ while (time.time() - t_run0) / 3600 < RUN_HOURS:
seg["peak_mib"] = peak
open(f"{d}/chain.log", "w").write((rr.stdout if rr else "") + "\n" + (rr.stderr if rr else "TIMEOUT"))
if not rr or rr.returncode != 0 or not os.path.exists(f"{d}/chain-results.json"):
say(f"RESULT seg {first} chain FAILED {stamp()} rc={rr.returncode if rr else 'timeout'} wall={seg['chain_s']} s: {((rr.stderr if rr else '') or '')[-200:].strip()}"); seg["failed"] = "chain"; continue
say(f"RESULT seg {first} chain FAILED {stamp()} rc={rr.returncode if rr else 'timeout'} wall={seg['chain_s']} s: {((rr.stderr if rr else '') or '')[-200:].strip()}"); seg["failed"] = "chain"; close(first, "cancelled", "chain" if rr else "timeout"); continue
res = json.load(open(f"{d}/chain-results.json"))
recs = [s for blk in res.get("blocks", []) for s in blk.get("shard_records", [])]
say(f"RESULT seg {first} chain {stamp()} {len(recs)} shard records, chain_len {res.get('segment_chain_len')}, proof {res.get('segment_proof_bytes')} bytes, shards {res.get('shard_prove_seconds_total', 0):.1f} s, aggregation {res.get('aggregate_prove_seconds_total', 0):.1f} s, wall {seg['chain_s']} s, peak {peak:.0f} MiB")
@ -260,17 +286,17 @@ while (time.time() - t_run0) / 3600 < RUN_HOURS:
else: state["shards_refused"] += 1; say(f"RESULT seg {first} shard {rcd['number']}/{rcd['shard']} refused: {(reply or {}).get('reason', reply)}")
seg["shards_accepted"] = ok_shards
say(f"RESULT seg {first} shards {stamp()} accepted {ok_shards} of {len(recs)}")
if ok_shards != len(recs): seg["failed"] = "shards"; continue
if ok_shards != len(recs): seg["failed"] = "shards"; close(first, "cancelled", "shards"); continue
pv = res.get("segment_public_values", "")
strip = lambda h: (h[2:] if h.startswith("0x") else h); strip2 = lambda h: (strip(h)[:472] + strip(h)[536:]) if len(strip(h)) == 680 else strip(h)
if strip2(pv) != strip2(expected):
a, b = strip2(pv), strip2(expected); off = next((i for i in range(min(len(a), len(b))) if a[i] != b[i]), min(len(a), len(b)))
say(f"RESULT seg {first} FAILED: statement differs from the node's at hex offset {off} (lengths {len(a)} vs {len(b)}); ours ...{a[max(0,off-8):off+56]} node ...{b[max(0,off-8):off+56]}"); seg["failed"] = "statement"; continue
say(f"RESULT seg {first} FAILED: statement differs from the node's at hex offset {off} (lengths {len(a)} vs {len(b)}); ours ...{a[max(0,off-8):off+56]} node ...{b[max(0,off-8):off+56]}"); seg["failed"] = "statement"; close(first, "cancelled", "statement"); continue
last_hash = next(s["hash"] for s in picked["shards"] if s["number"] == last)
sg = subprocess.run([f"{B}/igneum-miner", "sign-segment-record", LABEL, CHAIN, str(first), str(last), last_hash, WALLET, pv, res["segment_proof_sha256"]], capture_output=True, text=True).stdout.strip().split("\n")[-1]
try: record = json.loads(sg).get("record")
except Exception: record = None
if not record: say(f"RESULT seg {first} segment sign FAILED: {sg[:120]}"); seg["failed"] = "sign"; continue
if not record: say(f"RESULT seg {first} segment sign FAILED: {sg[:120]}"); seg["failed"] = "sign"; close(first, "cancelled", "sign"); continue
reply = submit("igneum_submitSegmentRecord", record, res["segment_proof_file"])
seg_s = round(time.time() - t_seg0, 1); seg["end_to_end_s"] = seg_s
if reply and reply.get("accepted"):
@ -283,6 +309,7 @@ while (time.time() - t_run0) / 3600 < RUN_HOURS:
say(f"RESULT segment_refused {stamp()} segment {first}..{last}: {reason}; end to end {seg_s} s")
if "pending until" in reason or "does not chain" in reason:
held[first] = {"record": record, "proof_file": res["segment_proof_file"], "last": last, "deadline": picked["deadline"], "since": time.time()}; say(f"RESULT held {stamp()} segment {first} held for retry until DAA {picked['deadline']}")
else: close(first, "cancelled", "refused")
for b in range(first, last + 1):
try: os.remove(f"{d}/block-{b}.json")
except OSError: pass
@ -290,5 +317,6 @@ while (time.time() - t_run0) / 3600 < RUN_HOURS:
except OSError: pass
save_state()
miner_stop(); kill_server(); save_state()
_o = state["outcomes"]; say(f"RESULT ledger {stamp()} paid={_o['paid']} active={_o['active']} expired={_o['expired']} cancelled={_o['cancelled']} deadline_misses={state['deadline_misses']} wasted_s={json.dumps(state['wasted_s'], sort_keys=True)} active_segments={[x['first'] for x in state['segments'] if x.get('outcome') == 'active']}")
say(f"RESULT summary {stamp()} passes={state['passes']} claimed={state['claimed']} submitted={state['submitted']} paid={state['paid']} paid_wei={state['paid_wei']} shards_accepted={state['shards_accepted']} shards_refused={state['shards_refused']} segment_refused={state['segment_refused']}")
say(f"RESULT prover_done {stamp()}")

View file

@ -16,11 +16,11 @@ curl -s -m 6 -X POST -H 'Content-Type: application/json' --data '{"jsonrpc":"2.0
curl -s -m 6 -X POST -H 'Content-Type: application/json' --data '{"jsonrpc":"2.0","id":1,"method":"igneum_getProvingStatus","params":[]}' http://127.0.0.1:26790/ | python3 -c 'import sys,json; r=sys.stdin.read(); d=json.loads(r).get("result",{}) if r.strip() else {}; v=d.get("v1",{}); w=v.get("segmentsInWindow",{}); print("tipDaa", int(d.get("tipDaa","0x0"),16), "fresh", v.get("freshRuleActive"), "paidShards", d.get("paidShards"), "pending", w.get("pending"), "proven", w.get("proven"), "unproven", w.get("unproven"), "paidSeg", v.get("paidSegments"))' 2>/dev/null
/opt/igneum/pkg/bin/igneum-miner watch 1 grpc://127.0.0.1:26610 2>/dev/null | grep -o 'daa=[0-9]*' | tail -1
pgrep -c -f '[p]ython3 -u /root/fleet/in/box-prover.py'
for f in /root/fleet/out/prover-state.json /root/fleet/card*/out/prover-state.json; do [ -f $f ] && python3 -c 'import json,sys; s=json.load(open(sys.argv[1])); print("ps", s.get("claimed",0), s.get("submitted",0), s.get("paid",0), s.get("shards_accepted",0), s.get("shards_refused",0), s.get("segment_refused",0), s.get("held",0), s.get("miner_mhs",0))' $f; done; true"""
for f in /root/fleet/out/prover-state.json /root/fleet/card*/out/prover-state.json; do [ -f $f ] && python3 -c 'import json,sys; s=json.load(open(sys.argv[1])); print("ps", s.get("claimed",0), s.get("submitted",0), s.get("paid",0), s.get("shards_accepted",0), s.get("shards_refused",0), s.get("segment_refused",0), s.get("held",0), s.get("miner_mhs",0)); o=s.get("outcomes",{}); print("po", o.get("paid",0), o.get("active",0), o.get("expired",0), o.get("cancelled",0), s.get("deadline_misses",0), round(sum(s.get("wasted_s",{}).values()),1))' $f; done; true"""
def probe(b):
r, out, err = fleet.ssh(b, PROBE, timeout=45)
if r != 0 or not out.strip(): return None
l = out.strip().split("\n"); d = {"swap": "", "exec": None, "blocked": "", "from": "", "tipDaa": None, "fresh": "", "paidShards": None, "pending": None, "proven": None, "unproven": None, "paidSeg": None, "daa": None, "provers": 0, "ps": []}
l = out.strip().split("\n"); d = {"swap": "", "exec": None, "blocked": "", "from": "", "tipDaa": None, "fresh": "", "paidShards": None, "pending": None, "proven": None, "unproven": None, "paidSeg": None, "daa": None, "provers": 0, "ps": [], "po": []}
for x in l:
if x.startswith("RESULT swap"): d["swap"] = x
elif x.startswith("exec "): p = x.split(); d["exec"] = int(p[1]); d["blocked"] = p[3]; d["from"] = " ".join(p[5:])
@ -28,6 +28,7 @@ def probe(b):
elif x.startswith("daa="): d["daa"] = int(x[4:])
elif x.isdigit(): d["provers"] = int(x)
elif x.startswith("ps "): d["ps"].append([float(v) for v in x.split()[1:]])
elif x.startswith("po "): d["po"].append([float(v) for v in x.split()[1:]]) # the outcome ledger (review B F08): paid active expired cancelled deadline_misses wasted_s
return d
zero_since = {}; launched = set(); last_row = 0; last_hour = None
hub = next(b for b in fleet.load().values() if b.get("hub") and b.get("state") != "destroyed" and b.get("hub_peer"))
@ -35,7 +36,8 @@ while True:
reg = fleet.load()
live = [(iid, b) for iid, b in reg.items() if b.get("state") != "destroyed" and b.get("ssh_host") and (b.get("phase") in ("1", "2") or b.get("hub"))]
with ThreadPoolExecutor(14) as ex: res = dict(zip([i for i, _ in live], ex.map(lambda x: probe(x[1]), live)))
tot = {"provers": 0, "claimed": 0, "submitted": 0, "paid": 0, "shards": 0, "refused": 0, "segref": 0, "held": 0, "exec_ok": 0, "boxes": 0}
tot = {"provers": 0, "claimed": 0, "submitted": 0, "paid": 0, "shards": 0, "refused": 0, "segref": 0, "held": 0, "exec_ok": 0, "boxes": 0,
"outcomes": {"paid": 0, "active": 0, "expired": 0, "cancelled": 0}, "deadline_misses": 0, "wasted_s": 0.0}
hubd = None
for iid, b in live:
d = res.get(iid)
@ -55,11 +57,15 @@ while True:
launched.add(iid); fleet.patch(iid, stage="prover", state="running", doing=f"phase 2 prover on the executed tip ({d['exec']})"); log(f"{b['label']}: replay at the tip (exec {d['exec']}), prover started")
tot["provers"] += d["provers"]
for p in d["ps"]: tot["claimed"] += p[0]; tot["submitted"] += p[1]; tot["paid"] += p[2]; tot["shards"] += p[3]; tot["refused"] += p[4]; tot["segref"] += p[5]; tot["held"] += p[6]
for p in d["po"]:
for k, v in zip(("paid", "active", "expired", "cancelled"), p[:4]): tot["outcomes"][k] += int(v)
tot["deadline_misses"] += int(p[4]); tot["wasted_s"] = round(tot["wasted_s"] + p[5], 1)
fleet.patch(iid, last_line=f"exec {d['exec']} tip {d['tipDaa']} provers {d['provers']} " + (" ".join(f"{int(p[0])}/{int(p[1])}/{int(p[2])}" for p in d["ps"])))
if time.time() - last_row > 900:
row = {"hour": now()[:13], "at": now(), "provers": tot["provers"], "boxes_executing": tot["exec_ok"], "boxes": tot["boxes"], "claimed": tot["claimed"], "segments_submitted": tot["submitted"], "segments_done": tot["paid"], "shards_paid": tot["shards"], "shards_refused": tot["refused"], "segment_records_refused": tot["segref"], "held": tot["held"],
"chain": ({"tipDaa": hubd["tipDaa"], "fresh": hubd["fresh"], "pending": hubd["pending"], "proven": hubd["proven"], "unproven": hubd["unproven"], "paidSeg": hubd["paidSeg"], "paidShards": hubd["paidShards"]} if hubd else {}),
"coverage_pct": (round(100 * int(hubd["proven"]) / max(1, int(hubd["proven"]) + int(hubd["pending"]) + int(hubd["unproven"])), 1) if hubd and hubd["proven"] not in (None, "None") else None),
"outcomes": tot["outcomes"], "deadline_misses": tot["deadline_misses"], "wasted_s": tot["wasted_s"], # the fleet's outcome ledger totals (review B F08)
"note": f"{tot['exec_ok']} of {tot['boxes']} boxes executing"}
rows = json.load(open(NIGHT)) if os.path.exists(NIGHT) else []; rows.append(row); json.dump(rows, open(NIGHT, "w"), indent=1)
log("row " + json.dumps(row)); last_row = time.time()

231
tools/fleet/prover-outcomes.py Executable file
View file

@ -0,0 +1,231 @@
#!/usr/bin/env python3
"""The proving pipeline's outcome ledger (review B F08, 8 October 2026): every eligible job a prover claimed and the one
outcome it ended in, read from the fleet's prover state files (tools/fleet/box-prover.py point 6: segments[].outcome) and,
for a prover from before the ledger, reconstructed from its RESULT lines in prover.log. The report answers the finding's
three questions: paid completions, missed deadlines and wasted work by cause, plus the accepted-proof throughput.
tools/fleet/prover-outcomes.py --state out/prover-state.json [--state ...] [--log out/prover.log ...] [--json]
tools/fleet/prover-outcomes.py --self-test
Outcomes: paid (a carrying block paid the segment record), active (claimed and not yet closed: in work, submitted and
waiting, or held for a retry), expired (the deadline passed with no paid record: cause unpaid after a submit, held_expired
for a held record, or never_submitted when the log ends past the deadline with no submit), cancelled (the prover gave the
job up: cause disk, export, cut, chain, timeout, shards, statement, sign, refused). Wasted work is the export, cut and
chain seconds of every job that was not paid, by cause. A job row from a state file wins over the same segment in a log.
"""
import sys, os, json, re, datetime, statistics
CAUSES_CANCELLED = ("disk", "export", "cut", "chain", "timeout", "shards", "statement", "sign", "refused")
CAUSES_EXPIRED = ("unpaid", "held_expired", "never_submitted")
def ts(s):
try: return datetime.datetime.strptime(s, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=datetime.timezone.utc).timestamp()
except Exception: return None
def jobs_from_state(state, label=None):
"""One job per segment row; an old row (no outcome field) is classified from what it carries."""
out = []
for x in state.get("segments") or []:
j = {"label": label or state.get("label"), "first": x.get("first"), "last": x.get("last"), "claimed_at": x.get("claimed_at"), "deadline": x.get("deadline"),
"margin_daa": x.get("margin_daa"), "shards": x.get("shards"), "fresh": x.get("fresh"),
"spent_s": x.get("spent_s", round(float(x.get("export_s", 0)) + float(x.get("cut_s", 0)) + float(x.get("chain_s", 0)), 1)),
"chain_s": x.get("chain_s"), "end_to_end_s": x.get("end_to_end_s"), "submitted_at": x.get("submitted_at"), "paid_at": x.get("paid_at"),
"paid_wei": x.get("paid_wei"), "closed_at": x.get("closed_at"), "miss_daa": x.get("miss_daa"), "source": "state"}
o = x.get("outcome")
if o in ("paid", "expired", "cancelled", "active"): j["outcome"] = o; j["cause"] = x.get("cause")
elif x.get("paid_wei") is not None: j["outcome"] = "paid"; j["cause"] = None
elif x.get("failed"): j["outcome"] = "cancelled"; j["cause"] = x["failed"]
elif x.get("refused") and not x.get("submitted_at"): j["outcome"] = "cancelled"; j["cause"] = "refused"
else: j["outcome"] = "active"; j["cause"] = None
out.append(j)
return out
def miss(tip, deadline):
"""The deadline miss in DAA from the last tip the log named; None when that tip is stale (before the deadline), never negative."""
return tip - deadline if tip is not None and deadline is not None and tip > deadline else None
LINE = re.compile(r"^RESULT (\S+) (\S+)(?: (.*))?$")
def jobs_from_log(text, label=None):
"""Reconstruct the ledger from a prover.log: claim opens a job; chain / shards / statement / sign / segment_refused /
held / submitted / paid / unpaid / held_expired / outcome lines move it. The last tip seen dates a never_submitted expiry."""
jobs = {}; order = []; last_tip = None; last_at = None; label = label
for raw in text.split("\n"):
m = LINE.match(raw.strip())
if not m: continue
kind, a, rest = m.group(1), m.group(2), m.group(3) or ""
at = a if ts(a) else None
if at: last_at = at
if kind == "start":
mm = re.search(r"label=(\S+)", rest); label = label or (mm.group(1) if mm else None); continue
mt = re.search(r"\btip[= ](\d+)", rest)
if mt: last_tip = int(mt.group(1))
if kind == "claim":
mm = re.match(r"segment (\d+)\.\.(\d+) \((\d+) shards, (\w+)\) margin=(\d+) tip=(\d+)", rest)
if not mm: continue
first = int(mm.group(1)); j = {"label": label, "first": first, "last": int(mm.group(2)), "claimed_at": at, "shards": int(mm.group(3)), "fresh": mm.group(4) == "fresh",
"margin_daa": int(mm.group(5)), "deadline": int(mm.group(6)) + 1 + int(mm.group(5)), "spent_s": 0.0, "chain_s": None, "end_to_end_s": None,
"submitted_at": None, "paid_at": None, "paid_wei": None, "closed_at": None, "miss_daa": None, "outcome": "active", "cause": None, "source": "log"}
jobs[first] = j; order.append(first); continue
if kind == "outcome":
mm = re.match(r"segment (\d+)\.\.(\d+) (\w+)(?: cause=(\w+))? spent=([\d.]+) s(?: miss=(\d+) DAA)?", rest)
if not mm or int(mm.group(1)) not in jobs: continue
j = jobs[int(mm.group(1))]; j.update(outcome=mm.group(3), cause=mm.group(4), spent_s=float(mm.group(5)), closed_at=at, miss_daa=int(mm.group(6)) if mm.group(6) else None); continue
if kind == "seg":
first = int(a) if a.isdigit() else None
if first not in jobs: continue
j = jobs[first]
if j["outcome"] != "active": continue
if rest.startswith("chain FAILED"):
mw = re.search(r"wall=([\d.]+) s", rest); w = float(mw.group(1)) if mw else 0.0
j["chain_s"] = w; j["spent_s"] = round(j["spent_s"] + w, 1); j.update(outcome="cancelled", cause="timeout" if "rc=timeout" in rest else "chain", closed_at=at)
elif rest.startswith("chain "):
ms_ = re.search(r"shards ([\d.]+) s, aggregation ([\d.]+)", rest)
if ms_: j["chain_s"] = round(float(ms_.group(1)) + float(ms_.group(2)), 1); j["spent_s"] = round(j["spent_s"] + j["chain_s"], 1)
elif rest.startswith("export FAILED"): j.update(outcome="cancelled", cause="export", closed_at=at)
elif " cut " in (" " + rest) and "FAILED" in rest: j.update(outcome="cancelled", cause="cut", closed_at=at)
elif rest.startswith("shards "):
mm = re.search(r"accepted (\d+) of (\d+)", rest)
if mm and mm.group(1) != mm.group(2): j.update(outcome="cancelled", cause="shards", closed_at=at)
elif rest.startswith("FAILED: statement"): j.update(outcome="cancelled", cause="statement", closed_at=at)
elif "segment sign FAILED" in rest: j.update(outcome="cancelled", cause="sign", closed_at=at)
continue
mm = re.match(r"segment (\d+)", rest)
if not mm or int(mm.group(1)) not in jobs: continue
j = jobs[int(mm.group(1))]
if kind == "submitted":
j["submitted_at"] = at; me = re.search(r"end to end ([\d.]+) s", rest)
if me: j["end_to_end_s"] = float(me.group(1))
elif kind == "segment_refused":
me = re.search(r"end to end ([\d.]+) s", rest)
if me: j["end_to_end_s"] = float(me.group(1))
j["_refused_at"] = at
elif kind == "held": j.pop("_refused_at", None)
elif kind == "paid" and j["outcome"] == "active":
mw = re.search(r"wei=(\d+)", rest); j.update(outcome="paid", paid_at=at, closed_at=at, paid_wei=int(mw.group(1)) if mw else None)
elif kind == "unpaid" and j["outcome"] == "active": j.update(outcome="expired", cause="unpaid", closed_at=at, miss_daa=miss(last_tip, j.get("deadline")))
elif kind == "held_expired" and j["outcome"] == "active": j.update(outcome="expired", cause="held_expired", closed_at=at, miss_daa=miss(last_tip, j.get("deadline")))
for first in order:
j = jobs[first]
if j["outcome"] == "active" and j.pop("_refused_at", None): j.update(outcome="cancelled", cause="refused", closed_at=j.get("closed_at") or last_at)
# a job never submitted whose deadline the chain passed while the log went on: expired, never_submitted
if j["outcome"] == "active" and j["submitted_at"] is None and last_tip is not None and j.get("deadline") and last_tip > j["deadline"] + 50:
j.update(outcome="expired", cause="never_submitted", closed_at=last_at, miss_daa=last_tip - j["deadline"])
j.pop("_refused_at", None)
return [jobs[f] for f in order]
def median(xs):
xs = [x for x in xs if x is not None]
return round(statistics.median(xs), 1) if xs else None
def report(jobs):
tot = {k: sum(1 for j in jobs if j["outcome"] == k) for k in ("paid", "active", "expired", "cancelled")}
paid = [j for j in jobs if j["outcome"] == "paid"]; exp = [j for j in jobs if j["outcome"] == "expired"]; can = [j for j in jobs if j["outcome"] == "cancelled"]
to_pay = [ts(j["paid_at"]) - ts(j["submitted_at"]) for j in paid if j.get("paid_at") and j.get("submitted_at") and ts(j["paid_at"]) and ts(j["submitted_at"])]
wasted = {}
for j in exp + can:
k = j.get("cause") or j["outcome"]; w = wasted.setdefault(k, {"jobs": 0, "seconds": 0.0}); w["jobs"] += 1; w["seconds"] = round(w["seconds"] + float(j.get("spent_s") or 0), 1)
t0 = min((ts(j["claimed_at"]) for j in jobs if j.get("claimed_at") and ts(j["claimed_at"])), default=None)
t1 = max((ts(j[k]) for j in jobs for k in ("closed_at", "paid_at", "submitted_at", "claimed_at") if j.get(k) and ts(j[k])), default=None)
span_h = round((t1 - t0) / 3600, 2) if t0 is not None and t1 is not None and t1 > t0 else None
shards_paid = sum(int(j.get("shards") or 0) for j in paid)
return {
"jobs": len(jobs), "outcomes": tot,
"paid": {"segments": tot["paid"], "shards": shards_paid, "ign": round(sum(int(j.get("paid_wei") or 0) for j in paid) / 1e18, 4),
"median_end_to_end_s": median([j.get("end_to_end_s") for j in paid]), "median_time_to_pay_s": median(to_pay), "median_margin_daa": median([j.get("margin_daa") for j in paid])},
"missed_deadlines": {"jobs": tot["expired"], "by_cause": {c: sum(1 for j in exp if j.get("cause") == c) for c in CAUSES_EXPIRED if any(j.get("cause") == c for j in exp)},
"median_miss_daa": median([j.get("miss_daa") for j in exp]), "median_margin_daa": median([j.get("margin_daa") for j in exp]), "wasted_s": round(sum(float(j.get("spent_s") or 0) for j in exp), 1)},
"wasted_by_cause": dict(sorted(wasted.items(), key=lambda kv: -kv[1]["seconds"])), "wasted_s": round(sum(w["seconds"] for w in wasted.values()), 1),
"spent_s": round(sum(float(j.get("spent_s") or 0) for j in jobs), 1),
"throughput": {"span_h": span_h, "segments_paid_per_h": round(tot["paid"] / span_h, 2) if span_h else None, "shards_paid_per_h": round(shards_paid / span_h, 1) if span_h else None,
"paid_share_of_spent": round(sum(float(j.get("spent_s") or 0) for j in paid) / max(1e-9, sum(float(j.get("spent_s") or 0) for j in jobs)), 3) if jobs else None},
"active": [{"label": j.get("label"), "first": j["first"], "last": j["last"], "claimed_at": j.get("claimed_at"), "submitted_at": j.get("submitted_at"), "deadline": j.get("deadline")} for j in jobs if j["outcome"] == "active"],
}
def text(r):
o = r["outcomes"]; p = r["paid"]; m = r["missed_deadlines"]; t = r["throughput"]
L = [f"Outcome ledger: {r['jobs']} jobs: paid {o['paid']}, active {o['active']}, expired {o['expired']}, cancelled {o['cancelled']}",
f"Paid completions: {p['segments']} segments ({p['shards']} shards, {p['ign']} IGN); median end to end {p['median_end_to_end_s']} s, median time to pay {p['median_time_to_pay_s']} s, median margin at claim {p['median_margin_daa']} DAA",
f"Missed deadlines: {m['jobs']} (" + ", ".join(f"{k} {v}" for k, v in m["by_cause"].items()) + f"); median miss {m['median_miss_daa']} DAA past the deadline, median margin at claim {m['median_margin_daa']} DAA, {m['wasted_s']} s of work",
f"Wasted work by cause ({r['wasted_s']} s of {r['spent_s']} s spent):"]
for k, w in r["wasted_by_cause"].items(): L.append(f" {k}: {w['jobs']} jobs, {w['seconds']} s")
if not r["wasted_by_cause"]: L.append(" none")
L.append(f"Throughput over {t['span_h']} h: {t['segments_paid_per_h']} segments paid per hour, {t['shards_paid_per_h']} shards paid per hour; {t['paid_share_of_spent']} of the seconds spent were paid")
if r["active"]: L.append("Active: " + ", ".join(f"{a['label'] or '?'} {a['first']}..{a['last']}" + (" submitted" if a["submitted_at"] else "") for a in r["active"]))
return "\n".join(L)
SELF_LOG = """RESULT start 2026-10-08T10:00:00Z label=t1 key=0xabc wallet=0x19 threshold=default miner=keep host=True
RESULT claim 2026-10-08T10:00:10Z segment 100..109 (10 shards, fresh) margin=400 tip=1000 candidates=3 rank_by=fnv
RESULT seg 100 chain 2026-10-08T10:05:00Z 10 shard records, chain_len 1, proof 1000 bytes, shards 200.0 s, aggregation 80.0 s
RESULT seg 100 shards 2026-10-08T10:05:10Z accepted 10 of 10
RESULT submitted 2026-10-08T10:05:20Z segment 100..109 record accepted (new=True, chain_len 1), aggregator share 0.1 IGN, end to end 310.0 s, mhs 1
RESULT paid 2026-10-08T10:08:20Z segment 100..109 wei=2000000000000000000 (2.0000 IGN) carrier=1500 after 180 s
RESULT claim 2026-10-08T10:10:00Z segment 110..119 (10 shards, continuing) margin=300 tip=1600 candidates=2 rank_by=fnv
RESULT seg 110 chain FAILED 2026-10-08T10:40:00Z rc=timeout wall=1800.0 s:
RESULT claim 2026-10-08T10:41:00Z segment 120..129 (10 shards, fresh) margin=250 tip=2400 candidates=2 rank_by=fnv
RESULT seg 120 chain 2026-10-08T10:46:00Z 10 shard records, chain_len 1, proof 1000 bytes, shards 210.0 s, aggregation 70.0 s
RESULT seg 120 shards 2026-10-08T10:46:10Z accepted 10 of 10
RESULT submitted 2026-10-08T10:46:20Z segment 120..129 record accepted (new=True, chain_len 1), aggregator share 0.1 IGN, end to end 320.0 s, mhs 1
RESULT pass 5 2026-10-08T10:50:00Z no whole segment inside the margin (worklist 20 entries, tip 2700, mhs 1); waiting
RESULT unpaid 2026-10-08T11:00:00Z segment 120 past its deadline unpaid; carried=0 pool=1
RESULT claim 2026-10-08T11:01:00Z segment 130..139 (10 shards, fresh) margin=240 tip=3000 candidates=1 rank_by=fnv
RESULT seg 130 chain 2026-10-08T11:06:00Z 10 shard records, chain_len 1, proof 1000 bytes, shards 190.0 s, aggregation 60.0 s
RESULT seg 130 shards 2026-10-08T11:06:10Z accepted 9 of 10
RESULT claim 2026-10-08T11:07:00Z segment 140..149 (10 shards, fresh) margin=240 tip=3200 candidates=1 rank_by=fnv
RESULT seg 140 chain 2026-10-08T11:12:00Z 10 shard records, chain_len 1, proof 1000 bytes, shards 190.0 s, aggregation 60.0 s
RESULT seg 140 shards 2026-10-08T11:12:10Z accepted 10 of 10
RESULT segment_refused 2026-10-08T11:12:20Z segment 140..149: pending until the previous segment pays; end to end 300.0 s
RESULT held 2026-10-08T11:12:20Z segment 140 held for retry until DAA 3441
RESULT held_expired 2026-10-08T11:30:00Z segment 140 deadline passed
RESULT claim 2026-10-08T11:31:00Z segment 150..159 (10 shards, fresh) margin=240 tip=3800 candidates=1 rank_by=fnv
RESULT pass 9 2026-10-08T11:40:00Z no whole segment inside the margin (worklist 20 entries, tip 3900, mhs 1); waiting
"""
def self_test():
# known-failed first: a log with no claim has no jobs and an empty report; a paid row never counts as waste
assert jobs_from_log("RESULT start 2026-10-08T10:00:00Z label=x\nRESULT paid 2026-10-08T10:00:01Z segment 5..9 wei=1 (0 IGN) carrier=1 after 1 s\n") == []
r0 = report([]); assert r0["jobs"] == 0 and r0["wasted_s"] == 0 and r0["throughput"]["span_h"] is None
assert report(jobs_from_state({"segments": [{"first": 1, "last": 2, "paid_wei": 5, "chain_s": 100.0, "outcome": "paid"}]}))["wasted_s"] == 0
# an old state row (no outcome field) classifies from what it carries
old = jobs_from_state({"label": "o", "segments": [{"first": 1, "last": 2, "failed": "chain", "chain_s": 50.0}, {"first": 3, "last": 4, "paid_wei": 7}, {"first": 5, "last": 6, "refused": "no"}, {"first": 7, "last": 8, "submitted_at": "2026-10-08T10:00:00Z"}]})
assert [(j["outcome"], j["cause"]) for j in old] == [("cancelled", "chain"), ("paid", None), ("cancelled", "refused"), ("active", None)], old
jobs = jobs_from_log(SELF_LOG)
got = [(j["first"], j["outcome"], j["cause"]) for j in jobs]
assert got == [(100, "paid", None), (110, "cancelled", "timeout"), (120, "expired", "unpaid"), (130, "cancelled", "shards"), (140, "expired", "held_expired"), (150, "active", None)], got
assert jobs[0]["deadline"] == 1401 and jobs[0]["spent_s"] == 280.0 and jobs[0]["paid_wei"] == 2 * 10**18 and jobs[0]["end_to_end_s"] == 310.0
assert jobs[2]["miss_daa"] == 2700 - (2401 + 250) and jobs[4]["miss_daa"] is None, (jobs[2], jobs[4]) # the held_expired line's last tip is stale: no miss figure, never a negative one
r = report(jobs)
assert r["outcomes"] == {"paid": 1, "active": 1, "expired": 2, "cancelled": 2}, r["outcomes"]
assert r["paid"]["segments"] == 1 and r["paid"]["shards"] == 10 and r["paid"]["ign"] == 2.0 and r["paid"]["median_time_to_pay_s"] == 180.0
assert r["missed_deadlines"]["jobs"] == 2 and r["missed_deadlines"]["by_cause"] == {"unpaid": 1, "held_expired": 1}
assert r["wasted_by_cause"]["timeout"] == {"jobs": 1, "seconds": 1800.0} and r["wasted_by_cause"]["unpaid"]["seconds"] == 280.0 and r["wasted_by_cause"]["shards"]["seconds"] == 250.0
assert r["wasted_s"] == 1800.0 + 280.0 + 250.0 + 250.0 and r["spent_s"] == r["wasted_s"] + 280.0
assert r["throughput"]["span_h"] == 1.51 and r["throughput"]["segments_paid_per_h"] == 0.66 and r["throughput"]["paid_share_of_spent"] == 0.098, r["throughput"]
assert len(r["active"]) == 1 and r["active"][0]["first"] == 150
# a state row wins over the log's row for the same segment
merged = merge(jobs_from_state({"label": "t1", "segments": [{"first": 150, "last": 159, "outcome": "paid", "paid_wei": 10**18, "claimed_at": "2026-10-08T11:31:00Z"}]}), jobs)
assert sum(1 for j in merged if j["first"] == 150) == 1 and next(j for j in merged if j["first"] == 150)["outcome"] == "paid"
out = text(r); assert "paid 1, active 1, expired 2, cancelled 2" in out and "timeout: 1 jobs, 1800.0 s" in out
print("RESULT prover-outcomes self-test PASS: 6 log jobs, 4 state rows, merge, report and text as expected")
def merge(state_jobs, log_jobs):
seen = {(j.get("label"), j["first"]) for j in state_jobs}
return state_jobs + [j for j in log_jobs if (j.get("label"), j["first"]) not in seen]
def main(argv):
if "--self-test" in argv: self_test(); return 0
states, logs, as_json, i = [], [], "--json" in argv, 0
while i < len(argv):
if argv[i] == "--state": states.append(argv[i + 1]); i += 2
elif argv[i] == "--log": logs.append(argv[i + 1]); i += 2
else: i += 1
if not states and not logs: print(__doc__); return 2
sj = []; lj = []
for f in states:
s = json.load(open(f)); sj += jobs_from_state(s, s.get("label") or os.path.basename(os.path.dirname(f)))
for f in logs: lj += jobs_from_log(open(f, errors="replace").read())
jobs = merge(sj, lj); r = report(jobs)
if as_json: print(json.dumps({"report": r, "jobs": jobs}, indent=1))
else: print(text(r))
return 0
if __name__ == "__main__": sys.exit(main(sys.argv[1:]))