Merge remote-tracking branch 'build/master' into lab-signing-20
This commit is contained in:
commit
4e17b34de6
69 changed files with 32692 additions and 1164 deletions
|
|
@ -144,6 +144,8 @@ var View = (function () {
|
|||
// section 9 of docs/spec/finality-guarantees.md: a block under a locked checkpoint is finalised even while the network's
|
||||
// finality is paused; the pause is the network's word, never a transaction's
|
||||
case 'finality not active': return { word: 'finalised', tone: 'ink', why: 'under a locked checkpoint, not yet verified here; finality is paused on the network above it' };
|
||||
// review B F04: a recovery lock is labelled "recovery", never plain "final" (the node lane's lockKind word)
|
||||
case 'finalised by a recovery lock': return { word: 'finalised (recovery lock)', tone: 'ink', why: 'under a recovery lock: the surviving majority’s lock after a long pause, not a certified checkpoint; not yet verified here' };
|
||||
case 'unknown': return { word: 'pending', tone: '', why: 'not in any block the node holds' };
|
||||
}
|
||||
if (e.finality === 'in_block') return { word: 'included', tone: '', why: 'in ' + blk };
|
||||
|
|
|
|||
|
|
@ -107,6 +107,10 @@ test('one state word per row: the wallet\'s verified final wins, failed from the
|
|||
// the network's finality is paused; the pause is the network's state word, never a transaction's. Known-failed first: the
|
||||
// node's "finality not active" word was shown on the row.
|
||||
assert.deepEqual(V.stateWord(e, 'finality not active'), { word: 'finalised', tone: 'ink', why: 'under a locked checkpoint, not yet verified here; finality is paused on the network above it' });
|
||||
// review B F04 (the founder, 8 October 2026): a recovery lock is labelled "recovery", never plain "final": the node's word
|
||||
// for a transaction under one (the node lane's lockKind) reads on the row as finalised by a recovery lock. Known-failed
|
||||
// first: the word fell through to the label.
|
||||
assert.deepEqual(V.stateWord(e, 'finalised by a recovery lock'), { word: 'finalised (recovery lock)', tone: 'ink', why: 'under a recovery lock: the surviving majority’s lock after a long pause, not a certified checkpoint; not yet verified here' });
|
||||
assert.equal(V.stateWord(e, 'unknown').word, 'pending');
|
||||
// no node word yet (the public RPC, or not asked): the wallet's label
|
||||
assert.deepEqual(V.stateWord(e, ''), { word: 'included', tone: '', why: 'in block 140,262' });
|
||||
|
|
@ -117,8 +121,8 @@ test('one state word per row: the wallet\'s verified final wins, failed from the
|
|||
assert.deepEqual(V.stateWord(r, ''), { word: 'executed', tone: 'ink', why: 'in block 140,201' });
|
||||
assert.equal(V.stateWord(entry({ hash: 'proving-1-0', kind: 'proving', finality: 'final', checkpoint: 9 }), '').word, 'finalised');
|
||||
// never a stronger word than the chain's: the words the row can show are exactly the node's plus failed
|
||||
const words = new Set(['pending', 'included', 'executed', 'proven', 'finalised', 'failed']);
|
||||
for (const w of ['pending', 'included', 'executed', 'proven', 'finalised', 'finality not active', 'unknown', '']) assert.ok(words.has(V.stateWord(e, w).word), w);
|
||||
const words = new Set(['pending', 'included', 'executed', 'proven', 'finalised', 'finalised (recovery lock)', 'failed']);
|
||||
for (const w of ['pending', 'included', 'executed', 'proven', 'finalised', 'finality not active', 'finalised by a recovery lock', 'unknown', '']) assert.ok(words.has(V.stateWord(e, w).word), w);
|
||||
});
|
||||
|
||||
test('the row model: kind word, who, sign, amount, the rows that need the node\'s word', () => {
|
||||
|
|
|
|||
280
docs/analysis/class-v6/amd-intel-energy.md
Normal file
280
docs/analysis/class-v6/amd-intel-energy.md
Normal 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.
|
||||
|
|
@ -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))
|
||||
|
|
@ -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
|
||||
|
|
@ -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 })
|
||||
|
|
@ -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
|
||||
|
|
@ -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 })
|
||||
98
docs/analysis/class-v6/amd-intel-energy/ze-power.c
Normal file
98
docs/analysis/class-v6/amd-intel-energy/ze-power.c
Normal 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;
|
||||
}
|
||||
40
docs/analysis/proving-outcome-ledger.md
Normal file
40
docs/analysis/proving-outcome-ledger.md
Normal 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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -359,4 +359,7 @@ Recorded as the founder's call, read through the shipper; they override section
|
|||
| F4 | One positioning line | the hero keeps it; `#s-positioning` under Updates is gone | cards-201 |
|
||||
| F5 | The watchdog's words while the node serves no epoch state stream | "waiting for the node's execution state for this epoch", never "the worker did not load its program" | `app/igneum-app/src/watchdog.rs` on cards-201 |
|
||||
| F6 | A node with no peers holds the worker, never a worker fault (19:25 BST on the mini: the worker idled for lack of templates and the watchdog blamed it, "did not come back within 180 s of the miner restarting it") | whenever the node reports zero peers or no template for the window, every watchdog clock holds (the restart grace included) and the card reads "no block templates: the node has no peers" (or "…answers none for the window"); the node line reads "Node: no peers, dialling the seeds" | `app/igneum-app/src/watchdog.rs` (`tick(now, node_ready, no_peers)`, `held_by_node`), `engine.rs`, `ui/app.js` on branch peers-202 for v2.0.2 |
|
||||
| F7 | Review B F14, the founder's ruling at 19:57 BST: two builds from one tree | the window names the build on the About line ("Igneum Miner" or "Igneum Miner Lab"), shows the remote-jobs switch on the lab build only, and asks "Update automatically" on the welcome screen (on by default; the Windows installer's task writes the same choice); with it off, nothing installs until Install now, an urgent manifest included, and an unsupported version pauses mining and says so | `app/igneum-app/src/edition.rs`, `ui/index.html`, `ui/app.js`, `packaging/windows/Igneum-Miner.iss` on branch reviewb-202 for v2.0.2 |
|
||||
| F8 | Review B F07: the per-device proving mode is explicit | simultaneous only on a tested configuration (24 GB and up), time-share where the compressed shard proof fits alone (the miner steps off the card, the memory is confirmed free by nvidia-smi before the proof runs, the dataset is rebuilt after), mining-only where no complete paid proof fits; the Proving section's per-card line names the mode from the engine (`state.proving.modes`) | `src/device.rs`, `src/provedefault.rs`, `src/prover.rs`, `ui/app.js` on reviewb-202 |
|
||||
| F9 | Review B F04: a recovery lock is labelled "recovery", never "final" | the block inspector reads "finalised by a recovery lock", the status strip "recovery lock" with its why, from the node's lockKind (`igneum_getProvingStatus`) once the node lane's field lands; until then the pause word stands; the wallet's History row follows on master | `src/engine.rs`, `ui/app.js` on reviewb-202; the wallet on master |
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -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 |
|
||||
|
|
|
|||
9
docs/plans/igneum-2.0-master/README.md
Normal file
9
docs/plans/igneum-2.0-master/README.md
Normal 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.
|
||||
File diff suppressed because it is too large
Load diff
Binary file not shown.
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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 | }
|
||||
```
|
||||
|
||||
181
docs/plans/igneum-2.0-master/evidence/MANIFEST.json
Normal file
181
docs/plans/igneum-2.0-master/evidence/MANIFEST.json
Normal 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"
|
||||
}
|
||||
48
docs/plans/igneum-2.0-master/evidence/README.txt
Normal file
48
docs/plans/igneum-2.0-master/evidence/README.txt
Normal 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.
|
||||
BIN
docs/plans/igneum-2.0-master/igneum-2.0-complete-master.pdf
Normal file
BIN
docs/plans/igneum-2.0-master/igneum-2.0-complete-master.pdf
Normal file
Binary file not shown.
19001
docs/plans/igneum-2.0-master/igneum-2.0-complete-master.txt
Normal file
19001
docs/plans/igneum-2.0-master/igneum-2.0-complete-master.txt
Normal file
File diff suppressed because it is too large
Load diff
5341
docs/plans/igneum-2.0-master/traceability.json
Normal file
5341
docs/plans/igneum-2.0-master/traceability.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -21,7 +21,7 @@ Built 8 October 2026, 18:3x BST (the founder's order: no feature left out, no st
|
|||
| 15 | D1. A frozen, reproducible baseline (p. 8 to 9) | Reference GPU population published: several vendors, memory sizes, generations, used cards (5090, 5080, 4090, 3090, 9070 XT, RX 7600 8 GB, Arc, Apple). | research lane (ad6a2bd47d4a46105) | the 21:00 landing | landed: reference-population.md (2db4fe34), every cell labelled; the 7600 grid, the Arc watts and the Ada and Ampere knees owed | docs/analysis/class-v6/reference-population.md | GPU-01, ECO-01 |
|
||||
| 16 | D1. A frozen, reproducible baseline (p. 8 to 9) | Two tests per class: existing owner (power, wear, fees, alternative use) and new entrant (purchase, operating, resale). | research lane (ad6a2bd47d4a46105) | the 21:00 landing | landed: in reference-population.md at 0.06 / 0.12 / 0.25 per kWh | docs/analysis/class-v6/reference-population.md | ECO-02 |
|
||||
| 17 | D1. A frozen, reproducible baseline (p. 8 to 9) | Central measure served: cost per accepted unit of work = (annualised hardware + power + hosting, failures, fees) / annual accepted work. | research lane (ad6a2bd47d4a46105) | 12:00 tomorrow | in flight: in reference-population.md; the site serves by 12:00 tomorrow | docs/analysis/class-v6/reference-population.md | ECO-01 |
|
||||
| 18 | D2. Two architectural experiments, not twenty knobs (p. 10) | (a) Reorganise existing work for unavoidable live state and resource coupling, counts held constant. RESULT 8 Oct 17:25: KILL as a class. Only the window width reaches the chip (+1.2 pJ per lane-op at N5); rearranging the dependency graph of the same ops moves neither side. Document: docs/analysis/class-v6/connected-state.md. The generator variant and liveness tool stay behind a flag. | adversary lane (a1a9876a88f5a72fc) | landed | PASSED (as a KILL): connected-state.md on master 3919d430; the class fails its own gate | docs/analysis/class-v6/connected-state.md | POW-03, POW-04 |
|
||||
| 18 | D2. Two architectural experiments, not twenty knobs (p. 10) | (a) Reorganise existing work for unavoidable live state and resource coupling, counts held constant. RESULT 8 Oct 17:25: KILL as a class. Only the window width reaches the chip (+1.2 pJ per lane-op at N5); rearranging the dependency graph of the same ops moves neither side. Document: docs/analysis/class-v6/connected-state.md. The generator variant and liveness tool stay behind a flag. | adversary lane (a1a9876a88f5a72fc) | landed | PASSED (as a KILL): connected-state.md on master 3919d430; the class fails its own gate; layer 8 off (the D2 row's third experiment): main's wording (20:1x UK): layer 8 off at stock PASS (the honest side within one run's noise, the chip side minus 4 to 11 percent, the acceptance 6.10 PASS); the knee rows owed (PC 1 held until the withdrawn job's cap); the layer re-entered by a new class if they fail, left out by a new class if they pass; the frozen object keeps layer 8 on, the flag CLASS_V6_FLAG_NOWIN admissible and off | docs/analysis/class-v6/connected-state.md | POW-03, POW-04 |
|
||||
| 19 | D2. Two architectural experiments, not twenty knobs (p. 10) | (b) Attack memory sharing, recomputation and data-local execution against v6 (ProgPoW review threat: dataset split across processors, compute moved to the data). Price the cheapest combination of moving state, moving data, recomputing and local resources, not the expected architecture. | adversary lane (a1a9876a88f5a72fc) | 21:30 tonight | in flight: the first D2(b) row at 17:3x (the stored-half hybrid), the data-local and dataset-comparison additions in the 21:30 delta | docs/analysis/class-v6/multi-family-adversary.md | ADV-02, ADV-03 |
|
||||
| 20 | D2. Two architectural experiments, not twenty knobs (p. 10) | Cumulative memory complexity and bandwidth hardness mapped onto the actual evaluation across many hashes (shared datasets, partial caches, recomputation, multiple engines amortising setup). Which trade-offs are bounded, which rest on physical-design experiments. | adversary lane (a1a9876a88f5a72fc) | 12:00 tomorrow | in flight: section 16 of multi-family-adversary.md (3a8874fef) | docs/analysis/class-v6/multi-family-adversary.md | ADV-02 |
|
||||
| 21 | D2. Two architectural experiments, not twenty knobs (p. 10) | Selective participation: distribution of the specialist's advantage across programs and epochs, not the mean; downtime, difficulty adjustment, re-entry included. | adversary lane (a1a9876a88f5a72fc) | landed | landed: 9 percent spread across 2,000 era draws, the best-half specialist 2 percent for half its revenue (the D2(b) row) | docs/analysis/class-v6/multi-family-adversary.md | ADV-04 |
|
||||
|
|
@ -119,6 +119,20 @@ Built 8 October 2026, 18:3x BST (the founder's order: no feature left out, no st
|
|||
| 112 | External source review of the mining algorithm (docs/analysis/review-2026-10-08/, on master at 0ae450bd; it examined the Mac checkout's older V2 to V4 crate, not class v6, and says so) | Finding A08 "An operation described as bijective is not always bijective" (on master at 0ae450bd, the harness run on build-2 to its end with every anchor it asserts agreeing with the tree, the v5 and v6 vectors outside its model); the review's harness is the cross-check evidence; the verdict reads fixed by construction, not applicable, or known open with a clock, and nothing reads PASS without the native test | hash lane (a690540514aa453d7); the node lane carries the verdict into the 23:30 freeze | 22:30 | RUNNING tonight (main's dispatch, 18:5x BST) | docs/analysis/review-2026-10-08/ | POW-02 (the review's harness as cross-check; the native test decides) |
|
||||
| 113 | External source review of the mining algorithm (docs/analysis/review-2026-10-08/, on master at 0ae450bd; it examined the Mac checkout's older V2 to V4 crate, not class v6, and says so) | Finding A09 "A naive stronger rejection test can exhaust all candidates" (on master at 0ae450bd, the harness run on build-2 to its end with every anchor it asserts agreeing with the tree, the v5 and v6 vectors outside its model); the review's harness is the cross-check evidence; the verdict reads fixed by construction, not applicable, or known open with a clock, and nothing reads PASS without the native test | hash lane (a690540514aa453d7); the node lane carries the verdict into the 23:30 freeze | 22:30 | RUNNING tonight (main's dispatch, 18:5x BST) | docs/analysis/review-2026-10-08/ | POW-04 (the review's harness as cross-check; the native test decides) |
|
||||
|
||||
## Coverage at 19:0x BST: 113 pins (104 of the plan, 9 of the review), 113 owned, open 30, in flight 44, landed 34, passed 5. Cases: 128 in 16 suites, every case owned; run under the standard: EVM-02 to 06 and VER-03, 04, 05, 06, 08 PASS (10), ECO-05 FAIL, VER-01 and VER-02 FAIL by design and disclosed, 62 RUNNING, 52 NOT RUN, 1 DEFERRED (P13).
|
||||
| 114 | External review B (docs/analysis/review-2026-10-08-b/dispatch.md, 8 October 19:44 UK; fourteen findings F01 to F14) | F04 recovery lock weaker than final: THE FOUNDER'S RULING (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) | node lane (a283f5f0d364ceef0) with the reference-apps lane and the site lane | the node field on successor-2.0.1 e9ab052f (pair about 20:45); the reference apps by 23:00; the explorer with them | RULED; in flight | docs/analysis/review-2026-10-08-b/dispatch.md; successor-2.0.1 e9ab052f | FIN-01 to FIN-08, VER-03, VER-05, VER-06, VER-08 |
|
||||
| 115 | External review B (the same) | F14 lab fleet control is not the public trust model: THE FOUNDER'S RULING (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 | shipper (ae892a8b0f78fe31c) with the relay lane (a22d765a2e0355a9f) | 2.0.2's cut (the app and relay lanes, 23:00 for the split's plan) | RULED; in flight | docs/analysis/review-2026-10-08-b/dispatch.md | UX-01, UX-07, OPS-02, OPS-04, OPS-06 |
|
||||
| 116 | External review B (the same) | F01 the proof-verdict cache omits the statement (P0): the cache holds immutable verified facts per proof hash and every lookup compares the carried statement and the accepted identities; no zero accepted id; a context refusal never cached | enforced-proving lane (a6e8f84588b809d62) with the node lane (the two-node cache-history test) | verdict-cache-fix off 5eab958f, suites green by 21:30; rides 2.0.2 | RUNNING, dispatched | docs/analysis/review-2026-10-08-b/dispatch.md | ZKP-01, ZKP-05, ZKP-06 |
|
||||
| 117 | External review B (the same) | F02 the proof rule fails open: the test bypass behind a build feature off in every release build (a CI check); a missing oracle or keys after activation is not-ready and never passes | enforced-proving lane with the steward | with F01 by 21:30 | RUNNING, dispatched | the same | ZKP-01, ZKP-03, GOV-05 |
|
||||
| 118 | External review B (the same) | F03 one release manifest pinning node, generator, dataset policy, acceptance, host ABI, miner app, pool, guests, keys and activation; the hash lane's ProgramClass::V6 on the freeze | steward (a2ecfa95d3206016c), hash lane, pool seat | F0 at 8bdd1ff0 is the base; ProgramClass::V6 by 20:45 in the object's commit; the pool's build against the manifest | RUNNING, dispatched | docs/plans/igneum-2.0-f0-manifest.md | GOV-01, GOV-02, POW-07 |
|
||||
| 119 | External review B (the same) | F05 the reg64 address mixer has an exact incremental form: the prefix-tree form natively with an equivalence test across every source register; the kernel-level measurement on a 5090 and a 4090; the chip side priced (floor lane 3: 2.6 percent of the chip's energy, the connected-state KILL unchanged) | hash lane, adversary lane, floor lane 3, the fleet lane (the two cards) | the native test by 21:05; the cards by 23:00; the row 23:30 | RUNNING, dispatched | class-v6-floor-sram 3a16190a section 4a | POW-02, ADV-02, ADV-05 |
|
||||
| 120 | External review B (the same) | F06 acceptance and census do not cover the final execution: the freeze object's acceptance evaluates the actual 64-register schedule; the live census on the final packs; main's default: "frozen pending D1 census" if the live census is not green by 00:30 | hash lane, lane D (a07a99a3788566af2), the census hand (afb2fb655385dc259) | the final packs 21:20; the census and 6.11 reads by 22:00 on build-6 | RUNNING, dispatched | class-v6 04442d9ca (is_class_v4_shape), e72d4f202 (the liveness rule) | POW-02, POW-03, POW-04 |
|
||||
| 121 | External review B (the same) | F07 one per-device scheduler with explicit modes (simultaneous on measured headroom, time-share with the dataset evicted and memory confirmed free, mining-only); the memory and crash rows on 8, 12 and 16 GB cells | app lane (the shipper), fleet lane | the modes in 2.0.2 (23:00); the fleet's rows from 01:00 | RUNNING, dispatched | the dispatch | GPU-05, UX-02, CAP-02 |
|
||||
| 122 | External review B (the same) | F08 the proving pipeline hides expired work: an outcome ledger for every eligible job (completed, active, expired, cancelled), deadline misses and accepted-proof throughput published, failure by cause | enforced-proving lane, fleet lane, site (the ops page) | the ledger by 23:00; the two-hour declared-load hold after 2.0.2's cut | RUNNING, dispatched | the dispatch | CAP-03, CAP-04, CAP-05, OPS-01 |
|
||||
| 123 | External review B (the same) | F09 the economic table and the failed specialist: every pass or fail cell generated from the declared inequality and its inputs | research lane, floor lane 3, coordinator | done 19:56 (3a16190a: the hand-written (c) cell was wrong, the criterion stands; no chip meets (c) at any price in the window; the board six of seven) | landed on the branch; in the 21:00 landing | class-v6-floor-sram 3a16190a (market.py M3, section 12) | ECO-03, ECO-05, ECO-06 |
|
||||
| 124 | External review B (the same) | F10 the CUDA readback: a separate selection pass with sentinels, exact comparison and overflow fallback, then asynchronous overlap; kernel throughput and energy published | worker lane (new; the hash lane spawns it) | the 5090 and 3090 rented 23:15 for 23:30 to 01:00 | NOT RUN, dispatched | the dispatch | GPU-02, POW-08 |
|
||||
| 125 | External review B (the same) | F11 Ember search and identity: objective-aware bounded two-sided search, rebase on goal change, a workload fingerprint invalidating stale results | Ember lane (new; the shipper spawns it) | 2.0.2 or later; the plan by 23:00 | NOT RUN, dispatched | the dispatch | UX-02, GPU-03 |
|
||||
| 126 | External review B (the same) | F12 pool payout durability (durable idempotent intent, the exact signed transaction before broadcast, balance reserved atomically, the state machine) and F13 pool admission bounds (frame size enforced while reading, bounded write queues, one membership per session, nonce dedup after validation) | pool seat (a3832b1c3b274b310) | the plan by 23:00; the code on pool-2.0 after UX-05's runs | NOT RUN, dispatched | the dispatch; pool-2.0 72cc306e | UX-04, UX-05, INC-06 |
|
||||
|
||||
## Coverage at 20:0x UK: 126 pins (104 of the plan, 9 of review A, 13 of review B), 126 owned, 2 RULED by the founder (F04, F14), open 30, in flight 56, landed 35, passed 5. Cases: 128 in 16 suites plus suite REV (44 regressions, the steward adds it by 20:45), every case owned; the registry at 19:45: 73 RUNNING, 53 NOT RUN with reasons, 1 DEFERRED (P13); team-run PASS 10 (EVM-02 to 06; VER-03, 04, 05, 06, 08), FAIL 3 (ECO-05 under P12; VER-01, VER-02 by design); nothing PASS under the procedure until the steward's test-record writes it.
|
||||
|
||||
The pass criteria are the Test and Acceptance Standard's (docs/plans/igneum-2.0-test-standard.pdf, 128 cases, approved as proposed by the founder on 8 October 2026 with P13 deferred; the live registry docs/plans/igneum-2.0-test-registry.json): a row reads passed only when its cases pass under the standard. The plan's own conditions, per section: D1 an independent operator reproduces the baseline from the served kit alone; D2 the candidate improves against re-optimised adversaries within the preset limits, else published as a failure; D3 the best supported advantage inside the chosen envelope with uncertainty published; D4 the model names credible conditions for sustained commodity participation and where it fails; D5 specified behaviour with no emergency change and no privileged intervention; the launch requirements live; the claim ladder earned rung by rung.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
@ -226,6 +226,48 @@ Rule: a case maps to a cell only where the cell's tests visibly answer it; cover
|
|||
- ZKP-08 Preserve authority and audit all acceptance paths: partial: the body rule and the payment rule read one floor, a producer with its body rule off still pays nothing on honest nodes (the fast-time crossing); the replacement-prover and authority steps are not run; the proof side of the derivation is P22 stage 3, PENDING
|
||||
- EVM-08 Verify controlled execution and verifier upgrades: partial: the controlled verifier upgrade as a key succession (both pairs embedded, the window, the start refusal) and its fast-time crossing; the mixed-client crossing on the live network and the failed-distribution step are not run
|
||||
|
||||
### harness:reference-apps-compat
|
||||
|
||||
- Command: `node tools/reference-apps/compat/run.mjs (from the main repo against the devnet read RPC with the funded tester key; rows land in tools/reference-apps/compat/results.json and render to /compatibility)`
|
||||
- Box class: harness (network run on the 2.0 devnet, read RPC + one funded test account)
|
||||
- Fixtures: none
|
||||
- Cases:
|
||||
- EVM-02 Preserve transaction binding and replay protection: partial: a transaction signed for the previous devnet's chain id is refused and a signed transaction sent twice is refused on the live devnet; the F0 replacement rule and the mempool-independent evidence are the exec suite's
|
||||
- EVM-03 Test two-dimensional fees and proving limits: partial: fee quotes, eth_estimateGas against gasUsed for a transfer and a call, the documented 21,000 local-limit difference and the pgas receipt fields on the live devnet; determinism across nodes and proofs and the P09 bounds are the proving-limit harness (not run)
|
||||
- EVM-04 Exercise block context and randomness assumptions: partial: block.number, timestamp, chainid, basefee, blockhash(number-1), CREATE2 address, block.coinbase and block.prevrandao read against the chain and the documented differences; conformance vectors for F0 are EVM-01's
|
||||
- EVM-05 Run representative contract integration journeys: partial: ERC-20 deploy, transfer, approve/transferFrom, a probe contract, a persisting counter, a revert with reason and an out-of-gas call on the live devnet; application-level invariants of the DEX integration set are the DEX lane's
|
||||
- EVM-06 Validate wallets, RPC and indexers: partial: receipt fields and logsBloom recompute, the block's receipts trie root rebuilds, eth_getLogs by address/topic/range, eth_getBlockByNumber with full transactions and eth_getBlockReceipts; wallet UI states (included, executed, proven, finalised) and the indexer's canonical-record check are the wallet and explorer lanes' rows
|
||||
|
||||
### harness:reference-apps-ver
|
||||
|
||||
- Command: `node tools/reference-apps/ver/run.mjs (from the main repo; the reference apps' verification rows against the devnet read RPC and the light service; evidence per case in tools/reference-apps/ver/evidence/<case>.json; the browser negative set is /lc/test and the Node negative set is tools/reference-apps/light-service/verify.test.mjs)`
|
||||
- Box class: harness (network run on the 2.0 devnet plus Sepolia reads)
|
||||
- Fixtures: none
|
||||
- Cases:
|
||||
- VER-03 Prove successful payment rather than inclusion: partial: a payment receipt is labelled payment only when the receipt is proven against the segment statement's receipts commitment under the finality certificate, inclusion-only receipts read inclusion, a tampered receipt and a flipped status fail; run on the Devnet 3 fixtures, the 2.0 devnet re-run waits for its first paid segment whose last block carries a transaction
|
||||
- VER-04 Bound cross-chain oracle trust and replay: partial: the oracle's trust() names the deployer-installed table and the unchecked aggregator signature, a second hash at a stored certificate index is refused, a root claim at an unknown index is refused, another chain id is refused (tools/reference-apps/oracle/test.mjs on Sepolia); Review B F04: the Sepolia verifiers apply the two-thirds rule only, carry no lock-kind field, and an under-threshold (recovery-rule) certificate fails closed in submitCertificate, so no stored root is a recovery lock (the DEX lane's docs/bridge/light-client-bridge.md paragraph); the claimed-option review of the trust model is the reviewer's
|
||||
- VER-05 Reconstruct required state without founder storage: partial: the public node and the reference reader on build-1 executed the 2.0 devnet from genesis with no snapshot (unit ExecStart without --igneum-exec-snapshot), and the pages fetch headers, certificates, coinbase bodies and account proofs from the public read RPC only; the archive and availability model beyond the 2,048-block state ring is the OPS no-founder exercise
|
||||
- VER-06 Detect withholding, corruption and stale data: partial: the read service never serves a certificate from another network, a withheld header in the path and a corrupted proof node are detected, stale data prints its lock age beside every balance, and a client told no certificate never falls back to a trusted RPC balance; detection on the fault network F4 is not run
|
||||
- VER-08 Keep every user-facing state truthful: partial: every served state on /light, /receipt and /oracle uses one of included, executed, proven, finalised from the shared definition block, the lock line serves the live DAA before the first certificate, and the trust assumptions are listed on each page; the wallet and explorer surfaces are their lanes' rows
|
||||
|
||||
### harness:reference-apps-ver-trust-anchor
|
||||
|
||||
- Command: `node tools/reference-apps/ver/run.mjs (the same run; VER-01 and VER-02 read from tools/reference-apps/ver/evidence/VER-01.json and VER-02.json and are FAIL by design until the voter table is committed in a header or shipped as a trust anchor with authority-change tracking)`
|
||||
- Box class: harness (network run on the 2.0 devnet)
|
||||
- Fixtures: none
|
||||
- Cases:
|
||||
- VER-01 Authenticate light-client bootstrap: fails today, disclosed: the light client authenticates the certificate's signatures and the header path but the voter table is a node-supplied trust anchor named on /light; a forged table signature and a wrong-network certificate are refused
|
||||
- VER-02 Verify evolving authority and execution statements: fails today, disclosed: the execution statement's proof is verified by the nodes and the browser verifies the aggregator signature and the post root under it; an in-browser verifier for the permitted proof and a table commitment close it
|
||||
|
||||
### suite:pool
|
||||
|
||||
- Command: `tools/build-remote.sh --box 2 -- test --release -p igneum-pool (from pool/, the pool-2.0 branch against the fork's successor-2.0.1)`
|
||||
- Box class: suite (bounded; box 2)
|
||||
- Fixtures: F0, F1
|
||||
- Cases:
|
||||
- UX-05 Keep voting keys with the miner through pooling: partial: the vote-key commitment on the wire (verify::tests::a_share_under_another_key_is_refused_before_the_hash: known-pass the member's key, known-fail another key on the share and a job naming another key, refused with code vote_key before the hash), the open pool's sidechain::check_key (the claimed key, the header's key and the reveal are one key), the same-nonce-other-template wrong_hash test, the TLS binding test; the malicious-pool run and the leave-and-retain run on the devnet-4 pair are the UX-05 batch's other evidence
|
||||
- UX-04 Pay small operators without hidden custody: partial: PPLNS distribution tests (fee first, never overpays, late joiner), the payout key file round trip, the signed transfer decode; the live payout path on the devnet-4 pair is the UX-04 batch's evidence
|
||||
|
||||
## Automated cases with no harness in the matrix (NOT RUN, the reason)
|
||||
|
||||
- GOV-02 Approve thresholds before results: the approval is recorded in the registry's approval field; the automated half (thresholds frozen before any run_status) is the gate rule landing by 21:00
|
||||
|
|
@ -248,10 +290,6 @@ Rule: a case maps to a cell only where the cell's tests visibly answer it; cover
|
|||
- ECO-06 Fund security and proving as issuance falls: F7
|
||||
- ECO-07 Price memory growth and honest-card displacement: F7
|
||||
- EVM-01 Match the selected EVM semantics: no EVM conformance-vector harness is mapped tonight; the exec suite does not run the reference test vectors
|
||||
- EVM-03 Test two-dimensional fees and proving limits: the two-dimensional fee tests need the proving-limit harness
|
||||
- EVM-04 Exercise block context and randomness assumptions: block context and randomness vectors are an EVM harness still to map
|
||||
- EVM-05 Run representative contract integration journeys: contract journeys need the DEX lane's integration set on Devnet 4
|
||||
- EVM-06 Validate wallets, RPC and indexers: wallet, RPC and indexer validation is the explorer and wallet lanes' rows
|
||||
- EVM-07 Handle execution denial-of-service workloads: execution DoS workloads need the workload catalogue F3
|
||||
- CAP-01 Reproduce the historical consumer-shard result: the consumer-shard reproduction is the fleet lane's pods
|
||||
- CAP-02 Prove on the actual mining configuration: proving on the mining configuration is the fleet lane's
|
||||
|
|
@ -269,11 +307,6 @@ Rule: a case maps to a cell only where the cell's tests visibly answer it; cover
|
|||
- INC-07 Resist self-dealing fees and fake proving demand: self-dealing fees need the economic model and a live window
|
||||
- INC-08 Quantify provider and supplier failure concentration: failure concentration is a live-window measurement
|
||||
- FIN-06 Analyse old-key compromise and long-range histories: old-key compromise analysis is the finality lane's long-range scenario, not yet in the sim
|
||||
- VER-01 Authenticate light-client bootstrap: light-client bootstrap is the reference apps lane (/light); no automated harness mapped tonight
|
||||
- VER-02 Verify evolving authority and execution statements: the evolving-authority statements are the reference apps lane's
|
||||
- VER-03 Prove successful payment rather than inclusion: payment proof (/receipt) is the reference apps lane's
|
||||
- VER-05 Reconstruct required state without founder storage: state reconstruction without founder storage is the OPS no-founder exercise
|
||||
- VER-06 Detect withholding, corruption and stale data: withholding and corruption detection needs the fault network F4
|
||||
- VER-07 Protect wallet keys, signing and recovery: wallet key protection is the wallet lane's security row
|
||||
- OPS-01 Run the complete no-founder exercise: the no-founder exercise is an operations run, not a suite
|
||||
- OPS-02 Diversify bootstrap and resist peer isolation: bootstrap diversity needs the fault network F4
|
||||
|
|
@ -295,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: 45 mapped to a cell, 67 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
|
|
@ -20,3 +20,8 @@ Registry: docs/plans/igneum-2.0-test-registry.json, suite UX. Owner lane: the sh
|
|||
- Version visibility and the unsigned build: the Mac download page and /miner carry the first-open two-step (site commit 6a81aa70, at the edge 18:24); the DMG README step 2 and the release note say the build is unsigned tonight (aa354ed5).
|
||||
- Actionable failures: the watchdog's message when the node serves no epoch state stream was the wrong cause ("the worker did not load its program") on 2.0.0 (the founder's mini, 18:0x); 2.0.1 names it ("waiting for the node's execution state for this epoch"). The root cause (every node on 4cdcc488 dropped the genesis stream at block 1) is the node lane's 5713d547.
|
||||
- Still to run: a failed or corrupt update (a wrong sha in the manifest against a running 2.0.1 app), explicit update approval (v2.0.2), the diagnostic export, on PC 2 tonight.
|
||||
|
||||
## Incident incident-u64-coinbase-2026-10-08 (UX-06 FAIL, 19:41 to 20:0x BST)
|
||||
- The shipped igneum-miner (the 4cdcc488 and ef0f2ed8 pairs) refused every block template whose coinbase exceeded a u64: on the founder's Mac mini at 19:41 "! template error: amount high word 2 (low word 2140158083214006468) on a network at 8 decimals, where every amount fits a u64", then "NODE SLOW: no template from the node in 29 pass(es)", 0 MH/s; rpc/core/src/error.rs:131. Cause: igneum/miner/src/main.rs installed the network's base unit (install_unit_from) only for `inspect`; `mine`, `watch`, `export-pack` and the prepare path read amounts at the 8-decimal width, so a merged coinbase above 18.4 IGN was refused.
|
||||
- The fleet at 19:44: the refusal on 40 of 43 mining boxes (hub-1 323 refusals, 120 NODE SLOW); hub-1's selected chain down to 2 to 4 miner keys over its last 300 EVM blocks (27 at 19:40) and 22 blocks in 14 minutes; 0 blocks per minute at 19:54: the cause of the solo branch and the vote split after the 19:15 outage.
|
||||
- The fix: release-2.0.0-node 7cfa422a (every miner command installs the base unit before any amount is read; the known-failed test with the mini's exact refusal; 777214af's first form did not compile). The clearing evidence is the fixed miner's live canary on build-1's seed (templates parsing, a MINER SUMMARY with a rate, a template whose coinbase exceeds a u64 joined), recorded as its own batch when the node lane prints it.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
83
proto-cuda/nvrtc/emu/select-check.sh
Executable file
83
proto-cuda/nvrtc/emu/select-check.sh
Executable 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 //')"
|
||||
|
|
@ -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
90
proto-cuda/nvrtc/serve-bench.sh
Executable 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"
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -449,7 +449,7 @@
|
|||
</div>
|
||||
<div class="grid c2">
|
||||
<div class="dl-list" style="margin:0">
|
||||
<a data-dl="miner-windows" href="https://dl.igneum.network/public/igneum-miner-windows.exe" class="primary">Windows <span data-dl-meta="miner-windows">v0.3.26 · 63.8 MB</span></a>
|
||||
<a data-dl="miner-windows" href="https://dl.igneum.network/public/igneum-miner-windows.exe" class="primary">Windows <span data-dl-meta="miner-windows">v2.0.0 · 63.8 MB</span></a>
|
||||
<a data-dl="miner-mac" href="https://dl.igneum.network/public/igneum-miner-mac.dmg">macOS <span data-dl-meta="miner-mac">v2.0.0 · 45.9 MB</span></a>
|
||||
<a href="/miner#hive">Linux · HiveOS <span>the tarball and the flight sheet, on the miner page</span></a>
|
||||
</div>
|
||||
|
|
@ -472,7 +472,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<a class="dl-bar" href="/miner#get"><b>Download the miner</b><span data-dl-meta="miner-windows">v0.3.26 · 63.8 MB</span></a>
|
||||
<a class="dl-bar" href="/miner#get"><b>Download the miner</b><span data-dl-meta="miner-windows">v2.0.0 · 63.8 MB</span></a>
|
||||
|
||||
<!-- footer:start -->
|
||||
<footer class="site-footer">
|
||||
|
|
|
|||
|
|
@ -244,8 +244,8 @@
|
|||
<div><h3>Ember for Windows</h3><div class="sub">the installer, from the signed manifest</div></div>
|
||||
<div class="body">
|
||||
<p>Run the installer, press Start. NVIDIA and AMD cards both mine. Setting an NVIDIA power cap asks for administrator rights once.</p>
|
||||
<a data-dl="miner-windows" href="https://dl.igneum.network/public/igneum-miner-windows.exe" class="btn primary"><span class="osmark bare" data-os="windows" title="Windows"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M3 5.6l7.3-1v7.1H3zM11.4 4.4L21 3v8.7h-9.6zM3 12.3h7.3v7.1L3 18.4zM11.4 12.3H21V21l-9.6-1.4z"/></svg></span>Download for Windows <span class="meta" data-dl-meta="miner-windows">v0.3.26 · 63.8 MB</span></a>
|
||||
<div class="sha"><b>sha256</b> <span data-dl-sha="miner-windows">8e674bd5896b0d61b74c6e6783085804dcae6d614859592506d84e2c93943b48</span></div>
|
||||
<a data-dl="miner-windows" href="https://dl.igneum.network/public/igneum-miner-windows.exe" class="btn primary"><span class="osmark bare" data-os="windows" title="Windows"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M3 5.6l7.3-1v7.1H3zM11.4 4.4L21 3v8.7h-9.6zM3 12.3h7.3v7.1L3 18.4zM11.4 12.3H21V21l-9.6-1.4z"/></svg></span>Download for Windows <span class="meta" data-dl-meta="miner-windows">v2.0.0 · 63.8 MB</span></a>
|
||||
<div class="sha"><b>sha256</b> <span data-dl-sha="miner-windows">793a631dc2084e6d6f778f13602d674a0c134af746b6355c25d3998c2be18b79</span></div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="dl-card" id="mac">
|
||||
|
|
@ -263,16 +263,16 @@
|
|||
<div><h3>Ember for Linux</h3><div class="sub">one tarball, the miner and the node inside</div></div>
|
||||
<div class="body">
|
||||
<p>Unpack it, run <code>igneum</code> with your payout address. The same signed manifest as the desktop apps. NVIDIA through CUDA, AMD through OpenCL.</p>
|
||||
<a data-dl="miner-hive" href="https://dl.igneum.network/public/igneum-miner-hive.tar.gz" class="btn primary"><span class="osmark bare" data-os="linux" title="Linux"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M12 2c-2.4 0-4 1.9-4 4.6 0 1.2.2 2 0 2.8-.6 1.4-2.1 2.9-2.6 4.9-.3 1.1-.1 2 .3 2.6-.6.4-1.3 1-1.1 1.7.3 1 2.1 1.2 3.2 1.8.7.4 1.5.6 2.1.1.6.2 1.3.3 2.1.3s1.5-.1 2.1-.3c.6.5 1.4.3 2.1-.1 1.1-.6 2.9-.8 3.2-1.8.2-.7-.5-1.3-1.1-1.7.4-.6.6-1.5.3-2.6-.5-2-2-3.5-2.6-4.9-.2-.8 0-1.6 0-2.8C16 3.9 14.4 2 12 2zm-1.4 4.2c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zm2.8 0c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zM12 9.3c.9 0 1.9.5 1.9 1s-1 1.2-1.9 1.2-1.9-.7-1.9-1.2 1-1 1.9-1zm0 3.4c2.2 0 3.6 2.6 3.6 4.4 0 1.5-1.6 2.3-3.6 2.3s-3.6-.8-3.6-2.3c0-1.8 1.4-4.4 3.6-4.4z"/></svg></span>Download the tarball <span class="meta" data-dl-meta="miner-hive">v0.3.26 · 27.7 MB</span></a>
|
||||
<div class="sha"><b>sha256</b> <span data-dl-sha="miner-hive">e3e4482c2b423c62b62e3de91f4eb06627723dd4a84770aba6e81a69111746c5</span></div>
|
||||
<a data-dl="miner-hive" href="https://dl.igneum.network/public/igneum-miner-hive.tar.gz" class="btn primary"><span class="osmark bare" data-os="linux" title="Linux"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M12 2c-2.4 0-4 1.9-4 4.6 0 1.2.2 2 0 2.8-.6 1.4-2.1 2.9-2.6 4.9-.3 1.1-.1 2 .3 2.6-.6.4-1.3 1-1.1 1.7.3 1 2.1 1.2 3.2 1.8.7.4 1.5.6 2.1.1.6.2 1.3.3 2.1.3s1.5-.1 2.1-.3c.6.5 1.4.3 2.1-.1 1.1-.6 2.9-.8 3.2-1.8.2-.7-.5-1.3-1.1-1.7.4-.6.6-1.5.3-2.6-.5-2-2-3.5-2.6-4.9-.2-.8 0-1.6 0-2.8C16 3.9 14.4 2 12 2zm-1.4 4.2c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zm2.8 0c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zM12 9.3c.9 0 1.9.5 1.9 1s-1 1.2-1.9 1.2-1.9-.7-1.9-1.2 1-1 1.9-1zm0 3.4c2.2 0 3.6 2.6 3.6 4.4 0 1.5-1.6 2.3-3.6 2.3s-3.6-.8-3.6-2.3c0-1.8 1.4-4.4 3.6-4.4z"/></svg></span>Download the tarball <span class="meta" data-dl-meta="miner-hive">v2.0.0 · 53.3 MB</span></a>
|
||||
<div class="sha"><b>sha256</b> <span data-dl-sha="miner-hive">5c1369fc5559e8c85534da08cb6eafff9f688336f8993b44e0512fe86d6284d1</span></div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="dl-card" id="hive">
|
||||
<span class="osmark" data-os="hive" title="HiveOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round" stroke-linecap="round"><path d="M12 2.6 20.2 7.3v9.4L12 21.4 3.8 16.7V7.3z"/><path d="M12 7.4 16 9.7v4.6L12 16.6 8 14.3V9.7z"/><path d="M12 7.4V2.6M16 9.7l4.2-2.4M16 14.3l4.2 2.4M12 16.6v4.8M8 14.3l-4.2 2.4M8 9.7 3.8 7.3"/></svg></span>
|
||||
<div><h3>Ember on HiveOS</h3><div class="sub">for the rig people, the same tarball</div></div>
|
||||
<div class="body">
|
||||
<div class="hive-sheet"><b>Flight Sheet.</b> Miner: <b>Custom</b>. Installation URL: <code data-dl-url="miner-hive">https://dl.igneum.network/dl/public/igneum-hive-0.3.26.tar.gz</code>. Miner name <code>igneum</code>, wallet and worker <code>0x<your 40-hex payout address>.%WORKER_NAME%</code>. Hive itself is untested on our side: tell us what breaks.</div>
|
||||
<a data-dl="miner-hive" href="https://dl.igneum.network/public/igneum-miner-hive.tar.gz" class="btn"><span class="osmark bare" data-os="hive" title="HiveOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round" stroke-linecap="round"><path d="M12 2.6 20.2 7.3v9.4L12 21.4 3.8 16.7V7.3z"/><path d="M12 7.4 16 9.7v4.6L12 16.6 8 14.3V9.7z"/><path d="M12 7.4V2.6M16 9.7l4.2-2.4M16 14.3l4.2 2.4M12 16.6v4.8M8 14.3l-4.2 2.4M8 9.7 3.8 7.3"/></svg></span>Download the tarball <span class="meta" data-dl-meta="miner-hive">v0.3.26 · 27.7 MB</span></a>
|
||||
<div class="hive-sheet"><b>Flight Sheet.</b> Miner: <b>Custom</b>. Installation URL: <code data-dl-url="miner-hive">https://dl.igneum.network/dl/public/igneum-hive-2.0.0.tar.gz</code>. Miner name <code>igneum</code>, wallet and worker <code>0x<your 40-hex payout address>.%WORKER_NAME%</code>. Hive itself is untested on our side: tell us what breaks.</div>
|
||||
<a data-dl="miner-hive" href="https://dl.igneum.network/public/igneum-miner-hive.tar.gz" class="btn"><span class="osmark bare" data-os="hive" title="HiveOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round" stroke-linecap="round"><path d="M12 2.6 20.2 7.3v9.4L12 21.4 3.8 16.7V7.3z"/><path d="M12 7.4 16 9.7v4.6L12 16.6 8 14.3V9.7z"/><path d="M12 7.4V2.6M16 9.7l4.2-2.4M16 14.3l4.2 2.4M12 16.6v4.8M8 14.3l-4.2 2.4M8 9.7 3.8 7.3"/></svg></span>Download the tarball <span class="meta" data-dl-meta="miner-hive">v2.0.0 · 53.3 MB</span></a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@
|
|||
<h1>The card you own has <span class="accent">a new chain</span> to mine.</h1>
|
||||
<p class="lead">A GPU-secured network for Ethereum-compatible applications and verifiable computation.</p>
|
||||
<div class="fold-actions" id="fold-actions">
|
||||
<a data-dl="miner-windows" href="https://dl.igneum.network/public/igneum-miner-windows.exe" data-os-button="windows" class="btn primary"><span class="osmark bare" data-os="windows" title="Windows"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M3 5.6l7.3-1v7.1H3zM11.4 4.4L21 3v8.7h-9.6zM3 12.3h7.3v7.1L3 18.4zM11.4 12.3H21V21l-9.6-1.4z"/></svg></span>Download for Windows<span class="meta" data-dl-meta="miner-windows">v0.3.26 · 63.8 MB</span></a>
|
||||
<a data-dl="miner-windows" href="https://dl.igneum.network/public/igneum-miner-windows.exe" data-os-button="windows" class="btn primary"><span class="osmark bare" data-os="windows" title="Windows"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M3 5.6l7.3-1v7.1H3zM11.4 4.4L21 3v8.7h-9.6zM3 12.3h7.3v7.1L3 18.4zM11.4 12.3H21V21l-9.6-1.4z"/></svg></span>Download for Windows<span class="meta" data-dl-meta="miner-windows">v2.0.0 · 63.8 MB</span></a>
|
||||
<a data-dl="miner-mac" href="https://dl.igneum.network/public/igneum-miner-mac.dmg" data-os-button="mac" class="btn"><span class="osmark bare" data-os="mac" title="macOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M16.4 12.6c0-2.5 2-3.6 2.1-3.7-1.2-1.7-3-1.9-3.6-2-1.5-.2-3 .9-3.8.9-.8 0-2-.9-3.3-.8-1.7 0-3.2 1-4.1 2.5-1.8 3-.5 7.6 1.3 10.1.9 1.2 1.9 2.6 3.2 2.5 1.3 0 1.8-.8 3.3-.8 1.6 0 2 .8 3.3.8 1.4 0 2.3-1.2 3.1-2.5 1-1.4 1.4-2.8 1.4-2.9 0 0-2.7-1-2.9-4.1zM13.9 5.3c.7-.8 1.2-2 1-3.2-1 0-2.2.7-2.9 1.5-.6.7-1.2 1.9-1 3 1.1.1 2.2-.5 2.9-1.3z"/></svg></span>Download for macOS<span class="meta" data-dl-meta="miner-mac">v2.0.0 · 45.9 MB</span></a>
|
||||
</div>
|
||||
<p class="fold-more fold-one">One click: node, miner, prover. The card starts.</p>
|
||||
|
|
|
|||
|
|
@ -5081,7 +5081,9 @@ function verifyCheckpoint(data, deps2) {
|
|||
const top = headers[headers.length - 1];
|
||||
if (top.hash !== data.hash) return done(fail("the last header is not the certified checkpoint block", { headers_checked: checked }));
|
||||
if (data.previous && headers[0].hash !== data.previous.hash) return done(fail("the first header is not the previous locked checkpoint", { headers_checked: checked }));
|
||||
if (BigInt(top.blue_score) < 30n * BigInt(index)) return done(fail(`checkpoint ${index} needs blue score at least ${30 * index}, header has ${top.blue_score}`, { headers_checked: checked }));
|
||||
const needBlue = 30n * BigInt(index), needDaa = 30n * (BigInt(index) - 1n);
|
||||
const byBlue = BigInt(top.blue_score) >= needBlue, byDaa = BigInt(top.daa_score) >= needDaa;
|
||||
if (!byBlue && !byDaa) return done(fail(`checkpoint ${index} needs blue score at least ${needBlue} or DAA score at least ${needDaa}, header has blue ${top.blue_score} and DAA ${top.daa_score}`, { headers_checked: checked }));
|
||||
if (voters.length !== Number(cert.voter_count)) return done(fail(`certificate names ${cert.voter_count} voters, ${voters.length} given`, { headers_checked: checked }));
|
||||
for (let i = 0; i < voters.length; i++) {
|
||||
const v = voters[i];
|
||||
|
|
@ -5142,26 +5144,10 @@ function verifyCheckpoint(data, deps2) {
|
|||
}
|
||||
|
||||
// site/lc/core.js
|
||||
var DST_SEGMENT = "IGNEUM_SEGMENT_RECORD_V1_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_";
|
||||
var SEGMENT_RECORD_LEN = 2 + 8 + 8 + 32 + 48 + 20 + 340 + 32 + 96;
|
||||
var ZERO32 = new Uint8Array(32);
|
||||
var te2 = new TextEncoder();
|
||||
var strip = (s) => String(s).replace(/^0x/i, "");
|
||||
var u64le = (v) => {
|
||||
const b = new Uint8Array(8);
|
||||
new DataView(b.buffer).setBigUint64(0, BigInt(v), true);
|
||||
return b;
|
||||
};
|
||||
var u16le = (v) => {
|
||||
const b = new Uint8Array(2);
|
||||
new DataView(b.buffer).setUint16(0, Number(v), true);
|
||||
return b;
|
||||
};
|
||||
var u32le = (v) => {
|
||||
const b = new Uint8Array(4);
|
||||
new DataView(b.buffer).setUint32(0, Number(v), true);
|
||||
return b;
|
||||
};
|
||||
function concat2(parts) {
|
||||
const n = parts.reduce((a, p) => a + p.length, 0);
|
||||
const m = new Uint8Array(n);
|
||||
|
|
@ -5187,114 +5173,6 @@ function merkleRootFromPath(leaf, index, siblings, blake2b2) {
|
|||
}
|
||||
return h;
|
||||
}
|
||||
function coinbaseTxHash(tx, blake2b2, amountWireLen = 8) {
|
||||
const version = Number(tx.version || 0);
|
||||
const parts = [u16le(version), u64le((tx.inputs || []).length)];
|
||||
for (const inp of tx.inputs || []) {
|
||||
parts.push(hexToBytes3(strip(inp.previousOutpoint.transactionId)), u32le(inp.previousOutpoint.index));
|
||||
const sig = hexToBytes3(strip(inp.signatureScript || ""));
|
||||
parts.push(u64le(sig.length), sig);
|
||||
if (version < 1) parts.push(new Uint8Array([Number(inp.sigOpCount || 0)]));
|
||||
parts.push(u64le(inp.sequence));
|
||||
if (version >= 1) parts.push(u16le(inp.computeBudget || 0));
|
||||
}
|
||||
parts.push(u64le((tx.outputs || []).length));
|
||||
for (const out of tx.outputs || []) {
|
||||
const v = new Uint8Array(amountWireLen);
|
||||
let x = BigInt(out.value);
|
||||
for (let i = 0; i < amountWireLen; i++) {
|
||||
v[i] = Number(x & 0xffn);
|
||||
x >>= 8n;
|
||||
}
|
||||
const spk = hexToBytes3(strip(out.scriptPublicKey));
|
||||
parts.push(v, spk.subarray(0, 2), u64le(spk.length - 2), spk.subarray(2));
|
||||
if (version >= 1) parts.push(new Uint8Array([out.covenant ? 1 : 0]));
|
||||
}
|
||||
const payload = hexToBytes3(strip(tx.payload || ""));
|
||||
parts.push(u64le(tx.lockTime || 0), hexToBytes3(strip(tx.subnetworkId)), u64le(tx.gas || 0), u64le(payload.length), payload);
|
||||
const mass = BigInt(tx.mass || 0);
|
||||
if (version < 1) {
|
||||
if (mass > 0n) parts.push(u64le(mass));
|
||||
} else parts.push(u64le(mass));
|
||||
return keyed(blake2b2, "TransactionHash")(concat2(parts));
|
||||
}
|
||||
function coinbaseExtraData(payload) {
|
||||
if (payload.length < 19) return new Uint8Array(0);
|
||||
const scriptLen = payload[18];
|
||||
return payload.subarray(19 + scriptLen);
|
||||
}
|
||||
function takeSection(extra, tag) {
|
||||
const n = extra.length;
|
||||
if (n < 8) return { items: null, before: extra };
|
||||
const t2 = String.fromCharCode(...extra.subarray(n - 4));
|
||||
if (t2 !== tag) return { items: null, before: extra };
|
||||
const len = new DataView(extra.buffer, extra.byteOffset + n - 8, 4).getUint32(0, true);
|
||||
if (len + 8 > n) return { items: null, before: extra };
|
||||
return { items: extra.subarray(n - 8 - len, n - 8), before: extra.subarray(0, n - 8 - len) };
|
||||
}
|
||||
function segmentRecordsOf(payload) {
|
||||
const extra = coinbaseExtraData(payload);
|
||||
const f = takeSection(extra, "IGNF");
|
||||
const p = takeSection(f.before, "IGNP");
|
||||
const s = takeSection(p.before, "IGNS");
|
||||
const out = [];
|
||||
if (!s.items) return out;
|
||||
for (let o = 0; o + SEGMENT_RECORD_LEN <= s.items.length; o += SEGMENT_RECORD_LEN) out.push(parseSegmentRecord(s.items.subarray(o, o + SEGMENT_RECORD_LEN)));
|
||||
return out;
|
||||
}
|
||||
function parseSegmentRecord(b) {
|
||||
if (b.length !== SEGMENT_RECORD_LEN) throw new Error(`segment record is ${b.length} bytes, not ${SEGMENT_RECORD_LEN}`);
|
||||
const dv = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
||||
let o = 0;
|
||||
const version = dv.getUint16(o, true);
|
||||
o += 2;
|
||||
const first = dv.getBigUint64(o, true);
|
||||
o += 8;
|
||||
const last = dv.getBigUint64(o, true);
|
||||
o += 8;
|
||||
const block = b.subarray(o, o + 32);
|
||||
o += 32;
|
||||
const pubkey = b.subarray(o, o + 48);
|
||||
o += 48;
|
||||
const payout = b.subarray(o, o + 20);
|
||||
o += 20;
|
||||
const publicValues = b.subarray(o, o + 340);
|
||||
o += 340;
|
||||
const proofHash = b.subarray(o, o + 32);
|
||||
o += 32;
|
||||
const signature = b.subarray(o, o + 96);
|
||||
return { bytes: b, version, first, last, block, pubkey, payout, publicValues, proofHash, signature, statement: parseBlockStatement(publicValues) };
|
||||
}
|
||||
function parseBlockStatement(b) {
|
||||
const dv = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
||||
const h = (o) => bytesToHex3(b.subarray(o, o + 32));
|
||||
return {
|
||||
chain_id: dv.getBigUint64(0),
|
||||
number: dv.getBigUint64(8),
|
||||
block_hash: h(16),
|
||||
parent_hash: h(48),
|
||||
shard_count: dv.getUint32(80),
|
||||
tx_commitment: h(84),
|
||||
pre_root: h(116),
|
||||
post_root: h(148),
|
||||
receipts: h(180),
|
||||
gas_used: dv.getBigUint64(212),
|
||||
pgas_used: dv.getBigUint64(220),
|
||||
executed: dv.getUint32(228),
|
||||
skipped: dv.getUint32(232),
|
||||
provers: h(236),
|
||||
shard_vk: h(268),
|
||||
agg_vk: h(300),
|
||||
chain_len: dv.getBigUint64(332)
|
||||
};
|
||||
}
|
||||
function segmentRecordMessage(chainId, r2) {
|
||||
return concat2([te2.encode("igneum-segment-record-v1/" + chainId), new Uint8Array([0]), u64le(r2.first), u64le(r2.last), r2.block, r2.payout, r2.publicValues, r2.proofHash]);
|
||||
}
|
||||
function verifySegmentRecordSignature(chainId, r2, bls2) {
|
||||
const L = bls2.longSignatures;
|
||||
return L.verify(r2.signature, L.hash(segmentRecordMessage(chainId, r2), DST_SEGMENT), r2.pubkey);
|
||||
}
|
||||
function rlpDecode(b) {
|
||||
const [item, rest] = rlpItem(b, 0);
|
||||
if (rest !== b.length) throw new Error("rlp: trailing bytes");
|
||||
|
|
@ -5333,31 +5211,6 @@ function rlpItem(b, o) {
|
|||
}
|
||||
return [items, end];
|
||||
}
|
||||
function rlpEncode(item) {
|
||||
if (item instanceof Uint8Array) {
|
||||
if (item.length === 1 && item[0] < 128) return item;
|
||||
return concat2([rlpLen(item.length, 128), item]);
|
||||
}
|
||||
const body = concat2(item.map(rlpEncode));
|
||||
return concat2([rlpLen(body.length, 192), body]);
|
||||
}
|
||||
function rlpLen(n, base) {
|
||||
if (n < 56) return new Uint8Array([base + n]);
|
||||
const bytes = [];
|
||||
let x = n;
|
||||
while (x > 0) {
|
||||
bytes.unshift(x & 255);
|
||||
x = Math.floor(x / 256);
|
||||
}
|
||||
return new Uint8Array([base + 55 + bytes.length, ...bytes]);
|
||||
}
|
||||
var trimBig = (v) => {
|
||||
v = BigInt(v);
|
||||
if (v === 0n) return new Uint8Array(0);
|
||||
let h = v.toString(16);
|
||||
if (h.length % 2) h = "0" + h;
|
||||
return hexToBytes3(h);
|
||||
};
|
||||
function verifyHeaderPath(headers, blake2b2, fromHash, toHash) {
|
||||
if (!headers.length) throw new Error("no headers");
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
|
|
@ -5425,140 +5278,6 @@ function parseTx(raw) {
|
|||
if (type === 1) return { type, chain_id: bigOf(body[0]).toString(), nonce: bigOf(body[1]).toString(), to: body[4].length ? hex(body[4]) : null, value: bigOf(body[5]).toString(), data: hex(body[6]), gas: bigOf(body[3]).toString() };
|
||||
return { type: 0, nonce: bigOf(body[0]).toString(), to: body[3].length ? hex(body[3]) : null, value: bigOf(body[4]).toString(), data: hex(body[5]), gas: bigOf(body[2]).toString() };
|
||||
}
|
||||
function hpEncode(nibs, leaf) {
|
||||
const odd = nibs.length % 2;
|
||||
const flag = (leaf ? 2 : 0) + odd;
|
||||
const out = [];
|
||||
if (odd) out.push(flag << 4 | nibs[0]);
|
||||
else out.push(flag << 4);
|
||||
for (let i = odd; i < nibs.length; i += 2) out.push(nibs[i] << 4 | nibs[i + 1]);
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
function orderedTrieRoot(values, keccak) {
|
||||
const items = values.map((v, i) => ({ nibs: nibblesOf(rlpEncode(trimBig(BigInt(i)))), value: v }));
|
||||
items.sort((a, b) => {
|
||||
const n = Math.min(a.nibs.length, b.nibs.length);
|
||||
for (let k = 0; k < n; k++) if (a.nibs[k] !== b.nibs[k]) return a.nibs[k] - b.nibs[k];
|
||||
return a.nibs.length - b.nibs.length;
|
||||
});
|
||||
if (!items.length) return keccak(rlpEncode(new Uint8Array(0)));
|
||||
const root = trieNodeTop(items, keccak);
|
||||
return root;
|
||||
}
|
||||
function nibblesOf(b) {
|
||||
const out = [];
|
||||
for (const x of b) out.push(x >> 4, x & 15);
|
||||
return out;
|
||||
}
|
||||
var Raw = class {
|
||||
constructor(bytes) {
|
||||
this.bytes = bytes;
|
||||
}
|
||||
};
|
||||
function rlpList(parts) {
|
||||
const body = concat2(parts.map((p) => p instanceof Raw ? p.bytes : rlpEncode(p)));
|
||||
const len = body.length;
|
||||
const head = len < 56 ? new Uint8Array([192 + len]) : (() => {
|
||||
const b = [];
|
||||
let x = len;
|
||||
while (x > 0) {
|
||||
b.unshift(x & 255);
|
||||
x = Math.floor(x / 256);
|
||||
}
|
||||
return new Uint8Array([247 + b.length, ...b]);
|
||||
})();
|
||||
return concat2([head, body]);
|
||||
}
|
||||
function build(items, keccak) {
|
||||
if (items.length === 1) return rlpList([hpEncode(items[0].nibs, true), items[0].value]);
|
||||
let prefix = 0;
|
||||
while (items.every((it) => it.nibs.length > prefix && it.nibs[prefix] === items[0].nibs[prefix])) prefix++;
|
||||
if (prefix > 0) {
|
||||
const childEnc = build(items.map((it) => ({ nibs: it.nibs.slice(prefix), value: it.value })), keccak);
|
||||
return rlpList([hpEncode(items[0].nibs.slice(0, prefix), false), childEnc.length < 32 ? new Raw(childEnc) : keccak(childEnc)]);
|
||||
}
|
||||
const parts = [];
|
||||
for (let b = 0; b < 16; b++) {
|
||||
const sub = items.filter((it) => it.nibs.length > 0 && it.nibs[0] === b).map((it) => ({ nibs: it.nibs.slice(1), value: it.value }));
|
||||
if (!sub.length) {
|
||||
parts.push(new Uint8Array(0));
|
||||
continue;
|
||||
}
|
||||
const enc = build(sub, keccak);
|
||||
parts.push(enc.length < 32 ? new Raw(enc) : keccak(enc));
|
||||
}
|
||||
const here = items.find((it) => it.nibs.length === 0);
|
||||
parts.push(here ? here.value : new Uint8Array(0));
|
||||
return rlpList(parts);
|
||||
}
|
||||
function trieNodeTop(items, keccak) {
|
||||
return keccak(build(items, keccak));
|
||||
}
|
||||
function receiptEnvelope(r2) {
|
||||
const h = (x) => hexToBytes3(strip(x));
|
||||
const status = Number(r2.status) === 1 ? new Uint8Array([1]) : new Uint8Array(0);
|
||||
const logs = (r2.logs || []).map((l) => [h(l.address), (l.topics || []).map(h), h(l.data || "0x")]);
|
||||
const body = rlpEncode([status, trimBig(BigInt(r2.cumulativeGasUsed)), h(r2.logsBloom), logs]);
|
||||
const type = Number(r2.type || 0);
|
||||
return type === 1 || type === 2 ? concat2([new Uint8Array([type]), body]) : body;
|
||||
}
|
||||
function verifyPaymentReceipt(pr, deps2) {
|
||||
const { blake2b: blake2b2, bls: bls2, keccak } = deps2;
|
||||
const base = verifyReceipt(pr, deps2);
|
||||
if (!base.verified) return { ...base, payment: false };
|
||||
const steps = base.steps.slice();
|
||||
const t0 = now();
|
||||
const step = (name, fn) => {
|
||||
try {
|
||||
const d = fn();
|
||||
steps.push({ name, ok: true, detail: d });
|
||||
return d;
|
||||
} catch (e) {
|
||||
steps.push({ name, ok: false, detail: String(e.message || e) });
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
const done = (extra) => ({ ...base, ...extra, steps, ms: Math.round((base.ms + now() - t0) * 10) / 10 });
|
||||
try {
|
||||
const seg = pr.segment;
|
||||
if (!seg) throw Object.assign(new Error(pr.payment_unavailable || "no proven segment record covers the chain block that executed this transaction yet"), { soft: true });
|
||||
const cp = pr.checkpoint.certificate;
|
||||
step("header chain from the record's carrier block up to the checkpoint", () => `${verifyHeaderPath(seg.headers, blake2b2, seg.headers[0].hash, cp.hash)} headers`);
|
||||
const record = step("the segment record is in the carrier's coinbase (hash recomputed, merkle path) and signed by its aggregator", () => {
|
||||
const cb = coinbaseTxHash(seg.carrier.coinbase, blake2b2, seg.carrier.amount_wire_len || 8);
|
||||
const root = merkleRootFromPath(cb, Number(seg.carrier.leaf_index), seg.carrier.merkle_siblings.map((x) => hexToBytes3(strip(x))), blake2b2);
|
||||
if (bytesToHex3(root) !== strip(seg.headers[0].hash_merkle_root)) throw new Error("the merkle path does not reach the carrier's hash_merkle_root");
|
||||
const rec = segmentRecordsOf(hexToBytes3(strip(seg.carrier.coinbase.payload))).find((r2) => bytesToHex3(r2.bytes) === strip(seg.record_hex));
|
||||
if (!rec) throw new Error("the named segment record is not in the carrier's coinbase");
|
||||
if (!verifySegmentRecordSignature(cp.chain_id, rec, bls2)) throw new Error("the aggregator's signature does not verify");
|
||||
return `record for blocks ${rec.first} to ${rec.last}, aggregator ${voteKeyHash(rec.pubkey, blake2b2).slice(0, 12)}`;
|
||||
}) && segmentRecordsOf(hexToBytes3(strip(seg.carrier.coinbase.payload))).find((r2) => bytesToHex3(r2.bytes) === strip(seg.record_hex));
|
||||
const st = record.statement;
|
||||
step("the statement is for the chain block that executed the transaction", () => {
|
||||
if (Number(st.number) !== Number(pr.execution.chain_block) || st.block_hash !== strip(pr.execution.chain_block_hash)) throw new Error(`the statement is for chain block ${st.number}, the transaction executed at ${pr.execution.chain_block}`);
|
||||
return `chain block ${st.number}, ${st.shard_count} shard(s), receipts commitment ${st.receipts.slice(0, 12)}`;
|
||||
});
|
||||
step("the shard receipts roots hash to the statement's receipts commitment", () => {
|
||||
const roots = seg.shard_receipts_roots.map((x) => hexToBytes3(strip(x)));
|
||||
if (roots.length !== Number(st.shard_count)) throw new Error(`${roots.length} roots given, the statement names ${st.shard_count} shards`);
|
||||
if (bytesToHex3(keccak(concat2(roots))) !== st.receipts) throw new Error("keccak over the shard receipts roots is not the statement's commitment");
|
||||
return `${roots.length} root(s), shard ${seg.shard_index} is ${strip(seg.shard_receipts_roots[seg.shard_index]).slice(0, 12)}`;
|
||||
});
|
||||
const mine = step("the shard's receipts rebuild its receipts trie root, and the transaction's receipt sits in it", () => {
|
||||
const root = bytesToHex3(orderedTrieRoot(seg.receipts.map(receiptEnvelope), keccak));
|
||||
if (root !== strip(seg.shard_receipts_roots[seg.shard_index])) throw new Error(`the ${seg.receipts.length} receipts rebuild root ${root.slice(0, 12)}, the committed root is ${strip(seg.shard_receipts_roots[seg.shard_index]).slice(0, 12)}`);
|
||||
const r2 = seg.receipts[Number(seg.receipt_position)];
|
||||
if (!r2 || strip(r2.transactionHash) !== strip(pr.tx_hash)) throw new Error("the receipt at the named position is not this transaction's");
|
||||
return `${seg.receipts.length} receipt(s), this one at position ${seg.receipt_position}, status ${Number(r2.status) === 1 ? "success" : "failed"}, gas ${parseInt(r2.gasUsed, 16)}, ${(r2.logs || []).length} log(s)`;
|
||||
}) && seg.receipts[Number(seg.receipt_position)];
|
||||
const ok = Number(mine.status) === 1;
|
||||
const outcome = ok ? { asset: "IGN (the native coin)", recipient: base.tx.to, amount_wei: base.tx.value, logs: (mine.logs || []).map((l) => ({ address: l.address, topics: l.topics, data: l.data })) } : null;
|
||||
return done({ verified: true, payment: ok, outcome, receipt_status: ok ? "success" : "failed", note: ok ? "the transfer outcome is authenticated through the proven segment's receipts commitment" : "the transaction executed and FAILED: no transfer took place; authenticated the same way" });
|
||||
} catch (e) {
|
||||
if (e.soft) return done({ verified: true, payment: false, payment_unavailable: String(e.message || e) });
|
||||
return done({ verified: false, payment: false, reason: String(e.message || e) });
|
||||
}
|
||||
}
|
||||
function formatIgn(wei, decimals = 18) {
|
||||
const v = BigInt(wei);
|
||||
const base = 10n ** BigInt(decimals);
|
||||
|
|
@ -5595,16 +5314,14 @@ if (args.includes("--tamper")) {
|
|||
console.log(`tampered copy (one nibble of the raw transaction): ${t2.verified ? "NOT REFUSED, this verifier is broken" : "REFUSED"}${t2.reason ? " :: " + t2.reason : ""}`);
|
||||
if (t2.verified) process.exit(1);
|
||||
}
|
||||
var r = receipt.segment ? verifyPaymentReceipt(receipt, deps) : verifyReceipt(receipt, deps);
|
||||
var payment = !!(r.verified && r.payment);
|
||||
console.log(`${payment ? "PAYMENT RECEIPT" : "TRANSACTION INCLUSION RECEIPT"} 0x${receipt.tx_hash} on ${receipt.chain_id} (Devnet 3, no value)`);
|
||||
console.log(payment ? "What this file authenticates: that the signed transaction is included in a finalised block and executed with status success, its receipt sitting in the shard receipts trie whose root the proven segment statement commits to. Trusted: the voter table from the node; the SP1 proof behind the statement is verified by nodes, not here." : "What this file authenticates: that the signed transaction is included in a block that is finalised. What it does not: the execution outcome (status, gas), which is carried as the node reported it." + (receipt.payment_unavailable ? " Why no payment receipt: " + receipt.payment_unavailable : ""));
|
||||
var r = verifyReceipt(receipt, deps);
|
||||
console.log(`TRANSACTION INCLUSION RECEIPT 0x${receipt.tx_hash} on ${receipt.chain_id} (devnet, no value)`);
|
||||
console.log("What this file authenticates: that the signed transaction is included in a block that is finalised. What it does not: the execution outcome (status, gas), which is carried as the node reported it. A payment receipt, which authenticates the transfer outcome, is a different file.");
|
||||
print(r);
|
||||
if (!r.verified) {
|
||||
console.log(`REFUSED: ${r.reason}`);
|
||||
process.exit(1);
|
||||
}
|
||||
var t = r.tx;
|
||||
if (payment) console.log(`PAYMENT VERIFIED in ${r.ms} ms: ${formatIgn(t.value)} IGN (${t.value} wei of the native coin) to ${t.to || "contract creation"}, executed with status success (${r.outcome.logs.length} log(s)) at chain block ${receipt.execution.chain_block}, in block ${r.block.slice(0, 16)} at DAA ${r.block_daa}, finalised under checkpoint ${r.checkpoint}; ${r.certificate}.`);
|
||||
else console.log(`INCLUSION VERIFIED in ${r.ms} ms: a signed transaction of ${formatIgn(t.value)} IGN to ${t.to || "contract creation"} (${t.value} wei) is included in block ${r.block.slice(0, 16)} at DAA ${r.block_daa} (${new Date(Number(r.block_time)).toISOString()}), finalised under checkpoint ${r.checkpoint}; ${r.headers} headers checked; ${r.certificate}.`);
|
||||
console.log("Reported by the node, not authenticated by this file: from " + (receipt.tx_as_reported && receipt.tx_as_reported.from) + (payment ? "" : receipt.execution ? `, executed with status ${receipt.execution.status === "0x1" ? "success" : "failed"}` : "") + ". The voter table with weights came from the node (spec 10.1). In the four words: " + (payment ? "included, executed, proven and finalised are authenticated under the stated trust." : "included and finalised are authenticated, executed is reported, proven is not claimed."));
|
||||
console.log(`INCLUSION VERIFIED in ${r.ms} ms: a signed transaction of ${formatIgn(t.value)} IGN to ${t.to || "contract creation"} (${t.value} wei) is included in block ${r.block.slice(0, 16)} at DAA ${r.block_daa} (${new Date(Number(r.block_time)).toISOString()}), finalised under checkpoint ${r.checkpoint}; ${r.headers} headers checked; ${r.certificate}.`);
|
||||
console.log("Reported by the node, not authenticated by this file: from " + (receipt.tx_as_reported && receipt.tx_as_reported.from) + (receipt.execution ? `, executed with status ${receipt.execution.status === "0x1" ? "success" : "failed"}` : "") + ". The voter table with weights came from the node (spec 10.1). In the four words: included and finalised are authenticated, executed is reported, proven is not claimed.");
|
||||
|
|
|
|||
|
|
@ -283,8 +283,8 @@ pre b{color:var(--molten-text);font-weight:500}
|
|||
<div class="platform-body" id="panel-windows" role="tabpanel" aria-labelledby="tab-windows" data-platform-panel="windows">
|
||||
<div class="download-platform"><span class="osmark" data-os="windows" title="Windows"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M3 5.6l7.3-1v7.1H3zM11.4 4.4L21 3v8.7h-9.6zM3 12.3h7.3v7.1L3 18.4zM11.4 12.3H21V21l-9.6-1.4z"/></svg></span><div><h3>Ember for Windows</h3><p>the installer, from the signed manifest</p></div></div>
|
||||
<p class="download-note">Run the installer, press Start. NVIDIA and AMD cards both mine. Setting an NVIDIA power cap asks for administrator rights once.</p>
|
||||
<a data-dl="miner-windows" href="https://dl.igneum.network/public/igneum-miner-windows.exe" class="btn primary"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v12m-5-5 5 5 5-5M5 16v4h14v-4"/></svg>Download for Windows <span data-dl-meta="miner-windows" style="font-weight:400;opacity:.85">v0.3.26 · 63.8 MB</span></a>
|
||||
<div class="download-detail"><span>from dl.igneum.network</span><span>sha256 <span data-dl-sha="miner-windows">8e674bd5896b0d61b74c6e6783085804dcae6d614859592506d84e2c93943b48</span></span></div>
|
||||
<a data-dl="miner-windows" href="https://dl.igneum.network/public/igneum-miner-windows.exe" class="btn primary"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v12m-5-5 5 5 5-5M5 16v4h14v-4"/></svg>Download for Windows <span data-dl-meta="miner-windows" style="font-weight:400;opacity:.85">v2.0.0 · 63.8 MB</span></a>
|
||||
<div class="download-detail"><span>from dl.igneum.network</span><span>sha256 <span data-dl-sha="miner-windows">793a631dc2084e6d6f778f13602d674a0c134af746b6355c25d3998c2be18b79</span></span></div>
|
||||
</div>
|
||||
<div class="platform-body" id="panel-mac" role="tabpanel" aria-labelledby="tab-mac" data-platform-panel="mac" hidden>
|
||||
<div class="download-platform"><span class="osmark" data-os="mac" title="macOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M16.4 12.6c0-2.5 2-3.6 2.1-3.7-1.2-1.7-3-1.9-3.6-2-1.5-.2-3 .9-3.8.9-.8 0-2-.9-3.3-.8-1.7 0-3.2 1-4.1 2.5-1.8 3-.5 7.6 1.3 10.1.9 1.2 1.9 2.6 3.2 2.5 1.3 0 1.8-.8 3.3-.8 1.6 0 2 .8 3.3.8 1.4 0 2.3-1.2 3.1-2.5 1-1.4 1.4-2.8 1.4-2.9 0 0-2.7-1-2.9-4.1zM13.9 5.3c.7-.8 1.2-2 1-3.2-1 0-2.2.7-2.9 1.5-.6.7-1.2 1.9-1 3 1.1.1 2.2-.5 2.9-1.3z"/></svg></span><div><h3>Ember for macOS</h3><p>a disk image, Apple silicon, Metal</p></div></div>
|
||||
|
|
@ -296,10 +296,10 @@ pre b{color:var(--molten-text);font-weight:500}
|
|||
<div class="platform-body" id="panel-hive" role="tabpanel" aria-labelledby="tab-hive" data-platform-panel="hive" hidden>
|
||||
<div class="download-platform"><span class="osmark" data-os="hive" title="HiveOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round" stroke-linecap="round"><path d="M12 2.6 20.2 7.3v9.4L12 21.4 3.8 16.7V7.3z"/><path d="M12 7.4 16 9.7v4.6L12 16.6 8 14.3V9.7z"/><path d="M12 7.4V2.6M16 9.7l4.2-2.4M16 14.3l4.2 2.4M12 16.6v4.8M8 14.3l-4.2 2.4M8 9.7 3.8 7.3"/></svg></span><div><h3>Ember for Linux and HiveOS</h3><p>a tarball for rigs and Hive flight sheets</p></div></div>
|
||||
<p class="download-note">For the rig people. One tarball, the miner and the node inside, the same signed manifest as the desktop apps.</p>
|
||||
<a data-dl="miner-hive" href="https://dl.igneum.network/public/igneum-miner-hive.tar.gz" class="btn primary"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v12m-5-5 5 5 5-5M5 16v4h14v-4"/></svg>Download the tarball <span data-dl-meta="miner-hive" style="font-weight:400;opacity:.85">v0.3.26 · 27.7 MB</span></a>
|
||||
<a data-dl="miner-hive" href="https://dl.igneum.network/public/igneum-miner-hive.tar.gz" class="btn primary"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v12m-5-5 5 5 5-5M5 16v4h14v-4"/></svg>Download the tarball <span data-dl-meta="miner-hive" style="font-weight:400;opacity:.85">v2.0.0 · 53.3 MB</span></a>
|
||||
<p class="fair" id="versions">Current build: Windows <span data-rm="versions.miner-windows.version">0.3.26</span>, macOS <span data-rm="versions.miner-mac.version">2.0.0</span>, HiveOS <span data-rm="versions.miner-hive.version">0.3.26</span>, the wallet <span data-rm="versions.wallet-mac.version">0.1.6</span>, <span data-rm="read_at_short">read 8 October 2026, 17:2x UK</span>. The machine-readable list, with every file’s SHA-256, is <a href="/release.json">/release.json</a>.</p>
|
||||
<div class="hive-sheet" id="hive"><b>HiveOS Flight Sheet.</b> Miner: <b>Custom</b>. Installation URL: <code data-dl-url="miner-hive">https://dl.igneum.network/dl/public/igneum-hive-0.3.26.tar.gz</code>. Miner name <code>igneum</code>, wallet and worker <code>0x<your 40-hex payout address>.%WORKER_NAME%</code>. Hive itself is untested on our side: tell us what breaks.</div>
|
||||
<p class="sha">sha256 <span data-dl-sha="miner-hive">e3e4482c2b423c62b62e3de91f4eb06627723dd4a84770aba6e81a69111746c5</span></p>
|
||||
<div class="hive-sheet" id="hive"><b>HiveOS Flight Sheet.</b> Miner: <b>Custom</b>. Installation URL: <code data-dl-url="miner-hive">https://dl.igneum.network/dl/public/igneum-hive-2.0.0.tar.gz</code>. Miner name <code>igneum</code>, wallet and worker <code>0x<your 40-hex payout address>.%WORKER_NAME%</code>. Hive itself is untested on our side: tell us what breaks.</div>
|
||||
<p class="sha">sha256 <span data-dl-sha="miner-hive">5c1369fc5559e8c85534da08cb6eafff9f688336f8993b44e0512fe86d6284d1</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -459,7 +459,7 @@ pre b{color:var(--molten-text);font-weight:500}
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<a class="dl-bar" href="#get"><b>Download the miner</b><span data-dl-meta="miner-windows">v0.3.26 · 63.8 MB</span></a>
|
||||
<a class="dl-bar" href="#get"><b>Download the miner</b><span data-dl-meta="miner-windows">v2.0.0 · 63.8 MB</span></a>
|
||||
|
||||
<!-- footer:start -->
|
||||
<footer class="site-footer">
|
||||
|
|
|
|||
|
|
@ -260,10 +260,10 @@
|
|||
<section class="card" aria-labelledby="deployed">
|
||||
<h2 id="deployed">On Sepolia</h2>
|
||||
<div class="kv" data-oracle-kv>
|
||||
<div class="k">Oracle</div><div class="mono" data-oracle-address>0xbb3450049926da3572e6b67cb94df312fe349e34 <small>IgneumStateOracle for the 2.0 devnet (igneum-devnet-4, statements with chain id 4465), deployed 8 October 2026 on the shared verifier (tx 0x5eb08f3a…a5a9, block 11871485). The earlier devnet's oracles stay as deployed with their records: 0x3ad71d46…3ab6 (chain ids 4463 and 4464, on the shared verifier) and 0xefe9879d…a1b2 (the stand-in verifier at first).</small></div>
|
||||
<div class="k">Oracle</div><div class="mono" data-oracle-address>0x06c12c0c44c952ae10dcbbe3d5450a13168baccb <small>IgneumStateOracle for the Igneum 2.0 devnet (igneum-devnet-4: statements with chain id 4465, coinbase amounts 16 bytes wide at 18 decimals), deployed 8 October 2026 on the devnet-4 verifier (block 11871931). Earlier oracles stay as deployed with their records: 0xbb345004…9e34 (devnet-4 chain id, the 8-byte width), 0x3ad71d46…3ab6 (Devnet 3 on its verifier), 0xefe9879d…a1b2 (the stand-in verifier at first).</small></div>
|
||||
<div class="k">Verifier</div><div class="mono" data-verifier-address>0x874D8Be5385474414aeb69EE4AB8374DA34b8c1F <small>the shared IgneumCertificateVerifier for igneum-devnet-4 (the DEX lane's, a real BLS12-381 check on chain through the EIP-2537 precompiles), set on this oracle by setVerifier on 8 October 2026 (tx 0x7ccffe23…3df1); its voter table installs at the first lock (the chain's first finality certificate, when the weight window fills at DAA 7,200), so no devnet-4 certificate is recorded before that: "table installs at the first lock". The earlier devnet's verifier 0xAf74f3F5…D744 holds that devnet's table at checkpoint 2127.</small></div>
|
||||
<div class="k">Chain</div><div>Sepolia, chain id 11155111; the Igneum 2.0 devnet (igneum-devnet-4), no value</div>
|
||||
<div class="k">Proven roots</div><div class="mono">Certificate 2232 (0xf056c26e…, 29 voters, signed weight 4,988 of 7,164) recorded on the shared verifier with a real BLS check, tx 0x1794b785…1e8a, 792,677 gas. Devnet chain block 28439, post_root 0x6e6d2fc8… stored on this oracle with a 6-header path, tx 0xcdb24dbc…ee5fc, 1,624,984 gas; provenBalance read 721.451608 IGN for 0xcaed79d8…c087 on Sepolia, equal to the devnet node's eth_getProof.</div>
|
||||
<div class="k">Proven roots</div><div class="mono">On the 2.0 devnet: certificate 249 (0x181c5dfd…, 38 voters, signed weight 4,968 of 7,151) verified by the devnet-4 verifier against the table it installed at the chain's first lock (checkpoint 241); chain block 3815's post_root stored on this oracle with a 3-header path, tx 0x490deea5…d869, 1,545,112 gas; provenBalance read 5,731.050719 IGN for 0x53fe9802…144b on Sepolia, equal to the devnet node's eth_getProof. On Devnet 3 (its own verifier and oracles): certificate 2232 recorded with a real BLS check (tx 0x1794b785…, 792,677 gas), block 28439's root stored (tx 0x3629525e…, 1,624,984 gas), 721.451608 IGN read for 0xcaed79d8…c087.</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="note">How to read a balance from another contract: <code>IIgneumStateOracle(oracle).provenBalance(number, account, accountProof)</code>, where <code>number</code> is a devnet chain block whose state root the oracle holds and <code>accountProof</code> is the <code>eth_getProof</code> account proof at that block. A storage slot: <code>provenStorage(number, account, slot, accountProof, storageProof)</code>. Both revert on any mismatch.</p>
|
||||
|
|
|
|||
|
|
@ -99,7 +99,13 @@ export function verifyCheckpoint(data, deps) {
|
|||
const top = headers[headers.length - 1];
|
||||
if (top.hash !== data.hash) return done(fail('the last header is not the certified checkpoint block', { headers_checked: checked }));
|
||||
if (data.previous && headers[0].hash !== data.previous.hash) return done(fail('the first header is not the previous locked checkpoint', { headers_checked: checked }));
|
||||
if (BigInt(top.blue_score) < 30n * BigInt(index)) return done(fail(`checkpoint ${index} needs blue score at least ${30 * index}, header has ${top.blue_score}`, { headers_checked: checked }));
|
||||
// C1, the placement check: checkpoint i is the selected-chain block at blue score 30 i under the launch rule, or at
|
||||
// DAA score 30 (i - 1) under the DAA-seconds rule from genesis (finality.rs daa_target with the anchor at genesis and
|
||||
// I0 = 1; the Igneum 2.0 devnet runs that rule from DAA 0, so its checkpoints sit at DAA >= 30 (i - 1) while their blue
|
||||
// score falls short of 30 i). Either placement satisfies the check; the signatures and the weight rule are the security.
|
||||
const needBlue = 30n * BigInt(index), needDaa = 30n * (BigInt(index) - 1n);
|
||||
const byBlue = BigInt(top.blue_score) >= needBlue, byDaa = BigInt(top.daa_score) >= needDaa;
|
||||
if (!byBlue && !byDaa) return done(fail(`checkpoint ${index} needs blue score at least ${needBlue} or DAA score at least ${needDaa}, header has blue ${top.blue_score} and DAA ${top.daa_score}`, { headers_checked: checked }));
|
||||
|
||||
// 3: the voter list
|
||||
if (voters.length !== Number(cert.voter_count)) return done(fail(`certificate names ${cert.voter_count} voters, ${voters.length} given`, { headers_checked: checked }));
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
52
tools/ci/batches/201-7cfa422a-aa0e0f45.json
Normal file
52
tools/ci/batches/201-7cfa422a-aa0e0f45.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
16
tools/ci/batches/p02-fleet-pods-20261008-01.json
Normal file
16
tools/ci/batches/p02-fleet-pods-20261008-01.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"run_id": "p02-fleet-pods-20261008-01",
|
||||
"manifest_sha": "417c4a57",
|
||||
"evidence_dir": "build-1:/srv/artefacts/tas/p02-rows",
|
||||
"boxes": [
|
||||
"tas-p02-2080ti", "tas-p02-3060-12g", "tas-p02-3070-8g", "tas-p02-4060-8g", "tas-p02-4070-12g", "tas-p02-4070ti-12g"
|
||||
],
|
||||
"cells": [
|
||||
{
|
||||
"cell": "bench:fleet-pods",
|
||||
"status": "RUNNING",
|
||||
"evidence": "build-1:/srv/artefacts/tas/p02-rows/p02-rows.txt",
|
||||
"note": "six CUDA retail cells measured (2080 Ti, 3060 12 GB, 3070 8 GB, 4060 8 GB, 4070 12 GB, 4070 Ti): hl-v5-win, hl-v6-all, hl-v6-foldrw rate and board power, every check PASS on the kit fingerprints; RUNNING not PASS because the AMD cells are PC 1 only (no rented AMD listed), the 1080 Ti cell is open, the 1300 MHz lock did not take inside RunPod containers, the 4060 host reports no power, and the v6 rows predate the 04442d9c pack re-export (program id only)"
|
||||
}
|
||||
]
|
||||
}
|
||||
12
tools/ci/batches/pool-2.0-20261008-01.json
Normal file
12
tools/ci/batches/pool-2.0-20261008-01.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"run_id": "pool-2.0-20261008-01",
|
||||
"manifest_sha": "72cc306e8",
|
||||
"evidence_dir": "build-2:/srv/builds/igneum-wt-pool-2.0/pool (the suite log; 30 of 30 ok at 20:1x UK) and docs/analysis/binding-review-2026-10.md",
|
||||
"cells": [
|
||||
{
|
||||
"cell": "suite:pool",
|
||||
"status": "RUNNING",
|
||||
"evidence": "build-2 suite 30 of 30 ok on 72cc306e8 (the vote-key known-pass and known-fail and the sidechain key check among them); the devnet-4 pair runs pending"
|
||||
}
|
||||
]
|
||||
}
|
||||
16
tools/ci/batches/ra-20261008T1744-evm.json
Normal file
16
tools/ci/batches/ra-20261008T1744-evm.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"run_id": "ra-20261008T1744-evm",
|
||||
"manifest_sha": "4cdcc488",
|
||||
"cut_tip": "igneumd 2.0.0 on the 2.0 devnet (igneum-devnet-4, chain id 4465), the read RPC rpc.devnet.igneum.network",
|
||||
"evidence_dir": "tools/reference-apps/compat/results.json (on master; 22 rows, each tagged register_case; rendered at /compatibility)",
|
||||
"boxes": [
|
||||
"mac-reference-apps-lane (network client only; no build)"
|
||||
],
|
||||
"cells": [
|
||||
{
|
||||
"cell": "harness:reference-apps-compat",
|
||||
"status": "RUNNING",
|
||||
"evidence": "tools/reference-apps/compat/results.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
21
tools/ci/batches/ra-20261008T1746-ver.json
Normal file
21
tools/ci/batches/ra-20261008T1746-ver.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"run_id": "ra-20261008T1746-ver",
|
||||
"manifest_sha": "4cdcc488",
|
||||
"cut_tip": "igneumd 2.0.0 on the 2.0 devnet (igneum-devnet-4, chain id 4465); Devnet 3 fixtures for the payment receipt; Sepolia oracle 0xbb345004 on the devnet-4 verifier",
|
||||
"evidence_dir": "tools/reference-apps/ver/evidence (on master; summary.json plus one file per case)",
|
||||
"boxes": [
|
||||
"mac-reference-apps-lane (network client only; no build)"
|
||||
],
|
||||
"cells": [
|
||||
{
|
||||
"cell": "harness:reference-apps-ver",
|
||||
"status": "RUNNING",
|
||||
"evidence": "tools/reference-apps/ver/evidence/summary.json"
|
||||
},
|
||||
{
|
||||
"cell": "harness:reference-apps-ver-trust-anchor",
|
||||
"status": "FAIL",
|
||||
"evidence": "tools/reference-apps/ver/evidence/VER-01.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
4
tools/ci/notes/f04-fin-20261008.json
Normal file
4
tools/ci/notes/f04-fin-20261008.json
Normal 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)"
|
||||
}
|
||||
4
tools/ci/notes/f14-ops-20261008.json
Normal file
4
tools/ci/notes/f14-ops-20261008.json
Normal 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"
|
||||
}
|
||||
4
tools/ci/notes/f14-ux-20261008.json
Normal file
4
tools/ci/notes/f14-ux-20261008.json
Normal 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"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
65
tools/ci/proof-rule-bypass-check.sh
Executable file
65
tools/ci/proof-rule-bypass-check.sh
Executable 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
5
tools/ci/review-suite-check.sh
Executable 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
75
tools/ci/review-suite.mjs
Normal 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
44
tools/ci/test-map-merge.py
Executable 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")
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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); }
|
||||
|
|
|
|||
|
|
@ -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()}")
|
||||
|
|
|
|||
|
|
@ -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
231
tools/fleet/prover-outcomes.py
Executable 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:]))
|
||||
File diff suppressed because one or more lines are too long
1
tools/reference-apps/fixtures/dn4/balance.json
Normal file
1
tools/reference-apps/fixtures/dn4/balance.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -29,6 +29,7 @@ contract IgneumStateOracle {
|
|||
address public owner;
|
||||
uint64 public immutable evmChainId; // the statement's chain id before the network's class v5 floor (Devnet 3: 4463), 0 to skip the check
|
||||
uint64 public immutable evmChainIdAfterFloor; // the id after the floor (Devnet 3: 4464); a record names the id in force at its block
|
||||
uint256 public immutable amountWireLen; // an output value's width in the transaction hashing: 8 bytes at 8 decimals (Devnet 3), 16 at 18 decimals (the 2.0 devnet)
|
||||
mapping(uint64 => Root) private _roots;
|
||||
|
||||
event StateRoot(uint64 indexed number, bytes32 postRoot, bytes32 blockHash, uint64 certIndex, bytes32 carrier);
|
||||
|
|
@ -36,11 +37,12 @@ contract IgneumStateOracle {
|
|||
|
||||
uint256 private constant RECORD_LEN = 586;
|
||||
|
||||
constructor(address verifier_, uint64 evmChainId_, uint64 evmChainIdAfterFloor_) {
|
||||
constructor(address verifier_, uint64 evmChainId_, uint64 evmChainIdAfterFloor_, uint256 amountWireLen_) {
|
||||
verifier = IIgneumCertificateVerifier(verifier_);
|
||||
owner = msg.sender;
|
||||
evmChainId = evmChainId_;
|
||||
evmChainIdAfterFloor = evmChainIdAfterFloor_;
|
||||
amountWireLen = amountWireLen_ == 0 ? 8 : amountWireLen_;
|
||||
}
|
||||
|
||||
function setVerifier(address v) external {
|
||||
|
|
@ -131,8 +133,8 @@ contract IgneumStateOracle {
|
|||
|
||||
// ---- the coinbase transaction and its segment record ---------------------------------------------
|
||||
|
||||
// Walks the serialized transaction to the payload. Amounts are 8 bytes on the wire.
|
||||
function payloadOf(bytes memory t) public pure returns (uint256 off, uint256 len) {
|
||||
// Walks the serialized transaction to the payload. Amounts are amountWireLen bytes on the wire.
|
||||
function payloadOf(bytes memory t) public view returns (uint256 off, uint256 len) {
|
||||
uint16 version = uint16(le64(t, 0) & 0xffff);
|
||||
uint256 p = 2;
|
||||
uint256 nIn = le64(t, p);
|
||||
|
|
@ -148,7 +150,7 @@ contract IgneumStateOracle {
|
|||
uint256 nOut = le64(t, p);
|
||||
p += 8;
|
||||
for (uint256 i = 0; i < nOut; i++) {
|
||||
p += 8 + 2;
|
||||
p += amountWireLen + 2;
|
||||
uint256 spkLen = le64(t, p);
|
||||
p += 8 + spkLen;
|
||||
if (version >= 1) {
|
||||
|
|
@ -199,7 +201,7 @@ contract IgneumStateOracle {
|
|||
// Reads record `recordIndex` of the IGNS section: (last block number, block hash, post state root, chain id).
|
||||
function readRecord(bytes memory t, uint256 recordIndex)
|
||||
public
|
||||
pure
|
||||
view
|
||||
returns (uint64 number, bytes32 blockHash, bytes32 postRoot, uint64 chainId)
|
||||
{
|
||||
(uint256 pOff, uint256 pLen) = payloadOf(t);
|
||||
|
|
|
|||
|
|
@ -31,13 +31,15 @@ const oracleOnly = process.argv.includes('--oracle-only');
|
|||
const vi = process.argv.indexOf('--verifier'); const verifierArg = vi > 0 ? process.argv[vi + 1] : null;
|
||||
// --chain-ids A,B: the statement chain ids the oracle accepts (Devnet 3: 4463,4464; the 2.0 devnet igneum-devnet-4: 4465,4465)
|
||||
const ci = process.argv.indexOf('--chain-ids'); const chainIds = ci > 0 ? process.argv[ci + 1].split(',').map(Number) : [EVM_CHAIN_ID, EVM_CHAIN_ID_AFTER_FLOOR];
|
||||
// --amount-wire-len N: 8 on Devnet 3 (8 decimals), 16 on the 2.0 devnet (18 decimals)
|
||||
const ai = process.argv.indexOf('--amount-wire-len'); const amountWireLen = ai > 0 ? Number(process.argv[ai + 1]) : 8;
|
||||
const previous = existsSync(out) ? deployment() : null;
|
||||
const stub = oracleOnly && previous ? previous.stub : await deploy('StubCertificateVerifier', []);
|
||||
const oracle = await deploy('IgneumStateOracle', [verifierArg || stub.address, chainIds[0], chainIds[1] ?? chainIds[0]]);
|
||||
const oracle = await deploy('IgneumStateOracle', [verifierArg || stub.address, chainIds[0], chainIds[1] ?? chainIds[0], amountWireLen]);
|
||||
const after = await pub.getBalance({ address: account.address });
|
||||
const d = {
|
||||
network: 'sepolia', chainId: 11155111, rpc: 'https://ethereum-sepolia-rpc.publicnode.com', deployer: account.address,
|
||||
evmChainId: chainIds[0], evmChainIdAfterFloor: chainIds[1] ?? chainIds[0], verifier: verifierArg || 'stub', verifierAddress: verifierArg || stub.address, stub, oracle, deployedAt: new Date().toISOString(), spentWei: (bal - after).toString(),
|
||||
evmChainId: chainIds[0], evmChainIdAfterFloor: chainIds[1] ?? chainIds[0], verifier: verifierArg || 'stub', verifierAddress: verifierArg || stub.address, amountWireLen, stub, oracle, deployedAt: new Date().toISOString(), spentWei: (bal - after).toString(),
|
||||
previous: previous ? [...(previous.previous || []), { stub: previous.stub, oracle: previous.oracle, deployedAt: previous.deployedAt, writes: previous.writes || [] }] : [],
|
||||
};
|
||||
writeFileSync(out, JSON.stringify(d, null, 1) + '\n');
|
||||
|
|
|
|||
|
|
@ -5,8 +5,9 @@
|
|||
"deployer": "0xe1D08384ef4c4c1511c8f05F94C914C4DeDc2787",
|
||||
"evmChainId": 4465,
|
||||
"evmChainIdAfterFloor": 4465,
|
||||
"verifier": "0xAf74f3F512081291D663Bb1d6b6d37E99e37D744",
|
||||
"verifierAddress": "0xAf74f3F512081291D663Bb1d6b6d37E99e37D744",
|
||||
"verifier": "0x874D8Be5385474414aeb69EE4AB8374DA34b8c1F",
|
||||
"verifierAddress": "0x874D8Be5385474414aeb69EE4AB8374DA34b8c1F",
|
||||
"amountWireLen": 16,
|
||||
"stub": {
|
||||
"address": "0xa197ef31d5d5613125779179668e2482ac69a6f6",
|
||||
"tx": "0xa01091b62dfb4a89b25afb57a3d384677a9ad62370748504d2b3f288c0d3ad38",
|
||||
|
|
@ -14,13 +15,13 @@
|
|||
"gasUsed": 8434842
|
||||
},
|
||||
"oracle": {
|
||||
"address": "0xbb3450049926da3572e6b67cb94df312fe349e34",
|
||||
"tx": "0x5eb08f3aadfbcec8e6eeccf5349278e1c5eeba27c08d6150aa2fdbd0395ca5a9",
|
||||
"block": 11871485,
|
||||
"gasUsed": 20561543
|
||||
"address": "0x06c12c0c44c952ae10dcbbe3d5450a13168baccb",
|
||||
"tx": "0xc519742488633bcbce3252d89c9203aea1408ef32686a5332771c1ee4ac41783",
|
||||
"block": 11871931,
|
||||
"gasUsed": 20734771
|
||||
},
|
||||
"deployedAt": "2026-10-08T16:53:26.556Z",
|
||||
"spentWei": "20561871984688",
|
||||
"deployedAt": "2026-10-08T18:23:03.332Z",
|
||||
"spentWei": "20736491985993",
|
||||
"previous": [
|
||||
{
|
||||
"stub": {
|
||||
|
|
@ -195,24 +196,64 @@
|
|||
"headers": 6
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"stub": {
|
||||
"address": "0xa197ef31d5d5613125779179668e2482ac69a6f6",
|
||||
"tx": "0xa01091b62dfb4a89b25afb57a3d384677a9ad62370748504d2b3f288c0d3ad38",
|
||||
"block": 11869583,
|
||||
"gasUsed": 8434842
|
||||
},
|
||||
"oracle": {
|
||||
"address": "0xbb3450049926da3572e6b67cb94df312fe349e34",
|
||||
"tx": "0x5eb08f3aadfbcec8e6eeccf5349278e1c5eeba27c08d6150aa2fdbd0395ca5a9",
|
||||
"block": 11871485,
|
||||
"gasUsed": 20561543
|
||||
},
|
||||
"deployedAt": "2026-10-08T16:53:26.556Z",
|
||||
"writes": [
|
||||
{
|
||||
"at": "2026-10-08T16:58:16.749Z",
|
||||
"what": "sepolia transfer 0.01 ETH to the DEX lane build-4 key (no value)",
|
||||
"to": "0x70e692A9f1fa1067a8c2d64fDAf1636b4EAEC7e2",
|
||||
"tx": "0xc938895c2bf96d3237b48aa01f32be81932c73b47000384e164108a0fd62dde4",
|
||||
"block": 11871509,
|
||||
"gasUsed": 204600
|
||||
},
|
||||
{
|
||||
"at": "2026-10-08T17:00:39.551Z",
|
||||
"what": "setVerifier (igneum-devnet-4 verifier)",
|
||||
"verifier": "0x874D8Be5385474414aeb69EE4AB8374DA34b8c1F",
|
||||
"tx": "0x7ccffe23aed7ef4876c37f8f595853362d8989f79e6d47a8c943ec378c163df1",
|
||||
"block": 11871521,
|
||||
"gasUsed": 31170
|
||||
},
|
||||
{
|
||||
"at": "2026-10-08T18:21:25.683Z",
|
||||
"what": "submitCertificate (shared verifier)",
|
||||
"verifier": "0x874D8Be5385474414aeb69EE4AB8374DA34b8c1F",
|
||||
"index": "249",
|
||||
"checkpoint": "0x181c5dfd477671d70b040d38051da6eb6ac4412fb40bdb07fcabff6b9512af85",
|
||||
"tx": "0x779ff9a374d706a804600af2b2c9c4f95aa064c3de8c73b85567b772c7084776",
|
||||
"block": 11871923,
|
||||
"gasUsed": 828823
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"writes": [
|
||||
{
|
||||
"at": "2026-10-08T16:58:16.749Z",
|
||||
"what": "sepolia transfer 0.01 ETH to the DEX lane build-4 key (no value)",
|
||||
"to": "0x70e692A9f1fa1067a8c2d64fDAf1636b4EAEC7e2",
|
||||
"tx": "0xc938895c2bf96d3237b48aa01f32be81932c73b47000384e164108a0fd62dde4",
|
||||
"block": 11871509,
|
||||
"gasUsed": 204600
|
||||
},
|
||||
{
|
||||
"at": "2026-10-08T17:00:39.551Z",
|
||||
"what": "setVerifier (igneum-devnet-4 verifier)",
|
||||
"verifier": "0x874D8Be5385474414aeb69EE4AB8374DA34b8c1F",
|
||||
"tx": "0x7ccffe23aed7ef4876c37f8f595853362d8989f79e6d47a8c943ec378c163df1",
|
||||
"block": 11871521,
|
||||
"gasUsed": 31170
|
||||
"at": "2026-10-08T18:23:14.145Z",
|
||||
"what": "submitStateRoot",
|
||||
"vector": "devnet-3",
|
||||
"number": "3815",
|
||||
"postRoot": "0xaf9c80517d84f917a09099359d498fb09c6000cc85ae7a66df168674367757af",
|
||||
"certIndex": "249",
|
||||
"tx": "0x490deea5d1d244504aaa562ad7facad22f44d87d49a004dbd6716f7b2f1dd869",
|
||||
"block": 11871932,
|
||||
"gasUsed": 1545112,
|
||||
"calldataBytes": 23400,
|
||||
"headers": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue