Merge master 36816182 into workers-page-nine under the master-landing lock
This commit is contained in:
commit
a433836eb3
62 changed files with 3293 additions and 368 deletions
|
|
@ -27,11 +27,15 @@ population, Apple reported and not headlined; every defence is scored after the
|
|||
2. The rows (ASAP7, placed and routed, SPEF, gate-level VCD; the SRAM term modelled, bands shown; node factors
|
||||
claimed): the full core 9.36 pJ per lane-op at ASAP7 on the class v4 draw (8.6 to 11.1), 6.55 at N5, 4.72 at N3;
|
||||
k 0.64 node-for-node and 0.46 a node ahead at the 5090's lock; the reserve families k 0.33 to 0.86 placed; the
|
||||
genesis-only core 7.78 (k 0.53 / 0.38); the 32-lane genesis core 10 percent under (synthesis).
|
||||
genesis-only core 7.78 (k 0.53 / 0.38); the 32-lane cores 10 to 13 percent under their 8-lane forms (synthesis:
|
||||
the full core 5.73, k 0.39 / 0.28), so the adversary's cheapest row is the 32-lane placed core at about 5.7 pJ
|
||||
per lane-op at N5 (k 0.55 same-node), a sixth under the headline row.
|
||||
3. The complete machine (memory, controller, host share, power train, cooling) on the card's own GDDR7: 1.5x the
|
||||
5090 at its lock per joule node-for-node (1.3x to 1.7x), 1.8x a node ahead (1.5x to 2.0x); 3.3x per dollar; HBM3
|
||||
buys it nothing; the N2 SRAM die 2.3x and 3.1x; the stored-half hybrid (section 13) 1.9x and 2.4x at USD 2.80 per
|
||||
MH/s. Capex per MH/s is the larger half of the chip's edge; the project cost is its hold.
|
||||
5090 at its lock per joule node-for-node on the placed 8-lane full core (1.3x to 1.7x), 2.1x on the placed
|
||||
32-lane core's floor (1.7x to 2.4x); 1.8x and 2.4x a node ahead; 3.3x per dollar; HBM3 buys it nothing; the N2
|
||||
SRAM die 2.3x to 4.2x same-node; the stored-half hybrid (section 13) 1.9x to 2.6x at USD 2.80 per MH/s. The
|
||||
same-node honest bracket across the adversary's choices is 1.5x to 2.1x on the DRAM board. Capex per MH/s is the
|
||||
larger half of the chip's edge; the project cost is its hold.
|
||||
4. Data-local execution cannot pay (the live state is 26x the read, section 11); memory sharing is the baseline;
|
||||
recomputation loses; selective participation gains 2 to 5 percent for 50 to 90 percent of revenue (section 13).
|
||||
5. Class v7 should take the epoch-defined bounded dataset with a published support horizon and keep the floor
|
||||
|
|
@ -348,10 +352,28 @@ under the class v4 draw (shfla and mm8 live -1.2 percent, all eight live -9.2 pe
|
|||
hash and pays +29 percent of shadow energy, and the card's premium rises by the same +29 percent (its tile at 70 pJ
|
||||
per lane against the draw's 10.3): the ratio does not move, the lane count does (section 14, row 1).
|
||||
|
||||
The 32-lane genesis core (synthesis only; four window macros, one imem pair shared by 32 lanes): 261,440 cells,
|
||||
5.29 pJ per lane-op at ASAP7 (4.65 to 6.77) on the class v4 draw, 3.70 at N5, k 0.36 at the lock: 10 percent
|
||||
under the 8-lane core, the imem and the sequencer amortised over four times the lanes. The 32-lane full core
|
||||
(synthesis) and its placed form are in the flow (adv-a and adv-f at 16:4x BST) and land as a delta.
|
||||
The 32-lane cores: the genesis comparator PLACED AND ROUTED (adv-g, 20:1x BST; SPEF, gate-level VCD at the
|
||||
30 ns constraint, four window macros and one imem macro, 717,268 cells with fill): 4.80 pJ per lane-op at ASAP7 on
|
||||
the class v4 draw (4.17 to 6.28), 3.36 at N5, 2.42 at N3, k 0.33 node-for-node and 0.24 a node ahead at the
|
||||
5090's lock. That is 9 percent UNDER its own synthesis row (5.29), where the 8-lane cores read 32 to 42 percent
|
||||
over theirs: the 32-lane core is routed under a 30 ns constraint (its unpipelined crossbar path is 27 ns) and the
|
||||
flow's resizer downsizes every cell to it, so the placed figure carries smaller cells and lower capacitance than a
|
||||
chip clocked at 1.5 ns with its pipeline registers would. The honest 32-lane row is therefore a band, not a point:
|
||||
3.36 pJ at N5 (the relaxed-clock placement, the floor) to 5.7 (the synthesised 32-lane full core 5.73 at ASAP7
|
||||
times the 8-lane cores' measured placement factor 1.42, the ceiling), k 0.33 to 0.55 same-node and 0.24 to 0.40 a
|
||||
node ahead. The synthesis rows beside it: the genesis core 261,440 cells, 5.29 (4.65 to 6.77), 3.70 at N5; the
|
||||
full core 393,036 cells, 5.73 (5.09 to 7.20), 4.01 at N5, 2.89 at N3, k 0.39 / 0.28; the bank's cost at 32 lanes
|
||||
8 percent of the energy on the draw and 50 percent of the cells. The placed 32-lane FULL core runs on adv-g (from
|
||||
synthesis at 19:58 BST, the same 30 ns constraint) and lands as a delta; its placed figure will sit inside the
|
||||
same band for the same reason.
|
||||
|
||||
The complete machine at the 32-lane band (the genesis comparator's placed 3.36 pJ at N5 as the floor; the ceiling
|
||||
is section 6's placed 8-lane full core less a sixth): the GDDR7 board 2.1x the 5090 at its lock per joule
|
||||
node-for-node at the floor (1.7x to 2.4x) and 2.4x a node ahead (2.0x to 2.7x), against 1.5x and 1.8x on the
|
||||
placed 8-lane full core; the N2 SRAM die 4.2x and 5.7x at the floor. So the same-node honest bracket across the
|
||||
adversary's lane count, core and sizing choices is 1.5x to 2.1x per joule on the DRAM board (1.3x to 2.4x on the
|
||||
bands), 1.9x to 2.6x with the stored-half hybrid, USD per MH/s unchanged (the core is a tenth of the board's capex
|
||||
at 32 lanes).
|
||||
|
||||
The board rows of section 6 restated at the placed energy (the full core at 6.55 pJ per lane-op at N5, band 6.01
|
||||
to 7.77; 4.72 at N3): the complete GDDR7 machine 1.53 microjoules per hash, **1.5x the 5090 at its lock per joule
|
||||
|
|
@ -726,7 +748,14 @@ percent (5th to 95th percentile 1.45x to 1.58x on the placed rows); a specialist
|
|||
gains 2, 3 or 5 percent of ratio and loses 50, 75 or 90 percent of revenue, because difficulty follows the fleet it
|
||||
joins and it earns in proportion to the time it mines; downtime and re-entry cost it the DAA window's lag each
|
||||
way and buy it nothing. The GPU-cost budget of 10 percent at the lock (set before these results) is untouched:
|
||||
no honest energy is spent in this section. Served sentence for the bracket: against a chip on the same node the
|
||||
no honest energy is spent in this section. (5) "Layer 8 off" (the hash lane's D2 experiment of 18:12 BST: the era layout's window-layer draw removed, every
|
||||
window the whole dataset): the hottest half then serves exactly 50 percent of the reads at every program, so the
|
||||
uniform-store row becomes the hot-set row: the hybrid reads 1.85x (1.58 to 2.03) same-node and 2.27x (1.95 to
|
||||
2.50) a node ahead at USD 3.34 per MH/s and 273 MH/s per board, against 1.93x / 2.40x at USD 2.80 on today's mean
|
||||
program (hit 0.581) and 2.09x / 2.65x at USD 1.88 on Devnet 3's p98 program (0.72): the window layer's removal
|
||||
takes 4 percent off the hybrid's per-joule edge at the mean program and 11 percent at the tail, and its capex edge
|
||||
from 5.7x to 4.8x of the card's, for one and-or per load on the honest side. The rows are in
|
||||
`tools/chip-model/mf/results/d2b-n5.md` and `d2b-n3.md`. Served sentence for the bracket: against a chip on the same node the
|
||||
honest floor is the stored-half hybrid at 1.9x to 2.1x per joule and 6x to 9x per dollar (1.65x to 2.3x on the
|
||||
band), not the DRAM board's 1.5x and 3.3x; the lever on it is the dataset floor as a capex ticket.
|
||||
|
||||
|
|
@ -751,7 +780,131 @@ Reading: no row loses competitiveness; the cheapest adaptation is firmware for e
|
|||
sequence for an entry outside it (row 2, 1.3x to 1.45x same-node while live), and a respin only if the chain adds a
|
||||
family the sequence cannot carry, which it has 360 days' notice of. The stress life of three years holds in full.
|
||||
|
||||
PENDING with clocks: the 32-lane full core's synthesis row (adv-a, in ABC at 17:0x BST; by 19:30) and its placed
|
||||
row (adv-f, at floorplan; by 21:00); the k lane's crossbar, scratch and tile rows (its pod); a real memory
|
||||
PENDING with clocks: the 32-lane full core's placed row (adv-g from 19:58 BST on a 64-core host, adv-f since 18:12
|
||||
on a slower one, whichever lands first, by 22:00; the 32-lane genesis comparator's placed row landed at 20:1x,
|
||||
section 5, and brackets it); the k lane's crossbar, scratch and tile rows (its pod); a real memory
|
||||
compiler's figure for the two macros (owed, no clock: FakeRAM gives area and pins only); the per-family rows on
|
||||
the 32-lane placed core (by 21:30 if adv-f lands, else the next pass).
|
||||
|
||||
## 15. The SRAM die's ticket reconciled (the research lane's and the coordinator's ask, 17:2x BST; floor lane 3's USD 0.6 against this file's USD 2.94 per MH/s)
|
||||
|
||||
The two figures price different machines. Lane B's USD 0.25 to 0.4 of silicon per MH/s (chip-model-v3 5.12,
|
||||
hardware-future) is the die at ZERO SHADOW: 2,100 MH/s per 2 GiB reticle at 300 W, no shadow core at all; with a
|
||||
board it reads about USD 0.6. This file's USD 2.94 is the same die carrying the class v4 shadow on the placed mf core
|
||||
(6.55 pJ per lane-op at N5), with the machine held at lane B's 270 to 300 W: the shadow is 13x the die's own energy
|
||||
per hash, so at that budget the machine makes 383 MH/s and the fixed tickets (die, package, board, host) divide by
|
||||
383 instead of 2,100. Neither is the adversary's choice. The designer sets the power budget to minimise dollars per
|
||||
MH/s; the die's read ceiling (floor lane 3: about 1,060 G reads per second, 8.3 GH/s) is never reached because the
|
||||
core's silicon and the power train bind first. Line by line, every term with its source and label:
|
||||
|
||||
| Component | This file at 270 W (section 6) | The optimised machine (this section) | Source and label |
|
||||
|---|---|---|---|
|
||||
| The 2 GiB SRAM die: 452 mm^2 of macro on a 550 to 650 mm^2 N2 die, a USD 30,000 wafer, lane B's yield model | USD 500 (400 to 600) | the same | lane B 4.9 (claimed density and wafer price; the yield approximate) |
|
||||
| The shadow core's silicon: this core's placed floorplan 0.0036 mm^2 per lane at ASAP7, x0.55 to N5 (0.002 mm^2), one lane-op per 7.5 ns (the 5-phase slot), 770 lanes per MH/s; USD 0.36 per mm^2 of N5 (sram-mirror's yield model) | USD 0.55 per MH/s (590 mm^2 at 383 MH/s) | USD 0.11 to 0.55 per MH/s: a core pipelined to one op per cycle per lane is five times denser (0.0004 mm^2 per lane; about +0.1 to 0.2 pJ per op for the pipeline registers, inside the band); the record's USD 25 to 40 per 166 MH/s (0.15 to 0.24) sits inside | this file's placed floorplan (measured), the pipelining factor approximate |
|
||||
| The package: two reticle-class dies (the SRAM die and the core die) with a wide link between them | USD 200 (an interposer, approximate) | USD 60 to 200: an organic flip-chip substrate carries a UCIe-class link at the hash's 80 bits per read (floor lane 3's hop, 0.5 pJ per bit); the interposer only if the link must be wider | approximate; the interposer price chip-model-v3 5.1 (claimed) |
|
||||
| The board, assembly, PSU and cooling | USD 200 flat (board 150, assembly 50; PSU and cooling inside) | USD 150 plus USD 0.15 per watt of PSU and cooling (approximate): USD 240 at 600 W, 375 at 1.5 kW | approximate |
|
||||
| The host (one full node per 100 machines) | USD 15 | USD 15 | section 6 |
|
||||
| The sustained rate | 383 MH/s at 270 W (power-bound on the core) | 852 MH/s at 600 W, 1,420 at 1 kW, 2,130 at 1.5 kW, 2,840 at 2 kW (the die's own ceiling 8.3 GH/s, never reached) | board.py on the placed core (modelled) |
|
||||
| **USD per MH/s** | **2.94** | **1.63 / 1.20 / 0.98 / 0.88 at 600 W to 2 kW on this core; 1.07 / 0.73 / 0.56 / 0.47 with the pipelined core and the organic package** | modelled |
|
||||
|
||||
The reconciled figure: **about USD 1.0 per MH/s for the SRAM-die machine carrying the class v4 shadow (0.5 to 1.6),
|
||||
at 1 to 1.5 kW per machine**, set by the power train and the core's silicon and not by the die; lane B's USD 0.6
|
||||
holds only at zero shadow, this file's USD 2.94 only at a 300 W budget, and both stand in the record with those
|
||||
labels. Per joule the optimised machine is unchanged (the energy per hash does not depend on the budget: 2.3x
|
||||
same-node, 3.1x a node ahead at the placed core).
|
||||
|
||||
What a maker's first batch of 1,000 dies would actually pay: not the unit ticket. A thousand 2 GiB dies at 1 to 2
|
||||
GH/s each are 1 to 2 TH/s, several times the chain's whole hashrate at the rental equilibrium (floor lane 3's
|
||||
section 4: a third of the network is under two dies at every price in its table), so the batch's cost is the
|
||||
project (USD 100 M to 500 M at N2, claimed) spread over a hashrate the chain cannot absorb: USD 50 to 250 per MH/s
|
||||
of NRE against USD 1 of unit cost, and the first dies' yield (a 600 mm^2 die with 452 mm^2 of macro, redundancy
|
||||
untested) adds USD 100 to 400 per die, approximate. The die's economics are project economics: the coexistence
|
||||
verdict should take USD 1.0 per MH/s (0.5 to 1.6) as the unit ticket of a fleet that exists and the project cost
|
||||
as the gate on whether it ever does.
|
||||
|
||||
## 16. The formal memory model: the class v6 evaluation in capacity, bandwidth, energy and amortisation terms (the 2.0 register's row, drafted 18:1x BST for the 12:00 delta)
|
||||
|
||||
One evaluation (a hash) under class v6: 128 dependent loads of a 4-byte word (W = 1; 16 bytes at W = 4) from a
|
||||
dataset of D bytes (2 GiB at genesis, the floor schedule 5.5 / 8.5 / 11.5 GiB), each address the fold of the lane's
|
||||
live state (64 registers, 61 necessary), each load returning into that state; between loads the shadow block (6,912
|
||||
instructions per iteration, 102,100 per hash) and the base program (512); the dataset rebuilt once per window from
|
||||
the chain's state (class v5 and v6) or from the epoch's seed (the class v7 default), 157 G ops per GiB. The terms,
|
||||
each with its bound and whether the bound is closed-form or rests on physical design:
|
||||
|
||||
| Term | Per evaluation | Across N evaluations on one machine | The bound | Closed-form or physical |
|
||||
|---|---|---|---|---|
|
||||
| Capacity, the dataset | D bytes must be addressable at the hash's latency: every item is reachable from every lane with uniform probability (the fold is keyed by the destination register; no item is colder than 1/D by construction, the hot set is per program and per window) | shared by every lane and every engine on the board: D once per machine, never per engine | a machine holds D or recomputes (section 13: recomputation loses at every point; a partial store of fraction f serves f of the reads uniformly, or up to 0.58 to 0.72 at f = 0.5 on the window layer, 0.50 with the layer off) | closed-form (the item distribution is the census's; the SRAM price per GiB is claimed) |
|
||||
| Capacity, the live state | 2,112 bits per hash in flight; the chain forbids reducing it (61 of 64 registers necessary; the connected-state lane: free for a chip, only the width counts) | lanes in flight x 2,112 bits: 1,172 lanes on the GDDR7 board (310 KB), 21,000 on the SRAM die (5.5 MB) | an SRAM macro per 8 lanes, 3.5 pJ per access (2.0 to 7.0): the one term this file's placed rows measure | physical (the macro energy band; the rest of the core measured placed) |
|
||||
| Bandwidth, random reads | 128 activates per hash: the device's activate rate, not its pin rate, binds (21.3 G per second on 16 GDDR7 devices, 82 percent reached by the 5090; 10.7 G on an HBM3 stack, unmeasured, the JEDEC floor 2.3 G; the SRAM die's 1,060 G never reached before power binds) | N x 128 activates: the sustained rate is activates per second over 128, shared across every engine on the board | the memory's activate ceiling per dollar of devices: a chip cannot buy more activates per device than the card has (section 11) | the GDDR7 ceiling measured on the card (82 percent); the HBM3 ceiling physical (the AWS F2 hour); the SRAM die's a model |
|
||||
| Bandwidth, bytes | 32-byte sectors at W = 1 (4 KB per hash, 38 percent of the GDDR7 pins at the activate ceiling); 2 sectors at W = 16 (dead on the cards) | linear in N | never the bound at W = 1 to 8 | closed-form |
|
||||
| Bandwidth, the state to the data | 2,112 bits per read if the computation moves to the item; 80 bits per read if the item moves to the lane | 26x on every medium (section 11) | data-local execution cannot pay | closed-form (the bit counts) with the per-bit energies claimed |
|
||||
| Energy, the memory | 2.0 nJ per GDDR7 read (1.5 to 2.6), 1.2 on HBM3, 0.25 on the SRAM die at W = 1: 0.256 / 0.154 / 0.032 microjoules per hash | linear in N plus the static and controller terms (35 W on the GDDR7 board) | the device's activate energy (chip-model-v3 5.3, modelled from HBM2's breakdown and the vendors' per-bit figures) | physical (claimed breakdowns; the card's marginal measured at 8.7 nJ per read) |
|
||||
| Energy, the shadow | 102,612 lane-ops x 6.55 pJ (placed, N5) = 0.67 microjoules; 0.48 at N3; the 32-lane core about 0.58 | linear in N; the clock, the window and the units measured per op; the leakage per lane | the placed rows (sections 3 to 5); the SRAM term's band | physical (measured placed; the SRAM term modelled) |
|
||||
| Energy, the machine | the power train (PSU 92 percent, VRM 90), cooling (3 percent), the host (0.85 W per machine) | fixed per machine, amortised over its rate | section 6's rows | approximate |
|
||||
| Amortisation, the set-up | the dataset build 0.7 J per GiB per window per machine; the host USD 15 and 0.85 W per machine; the era's registers | shared by every engine of the machine and every hash of the window: 1.4e-12 J per hash, under 1 percent of capex | nothing to amortise further: the set-up is already a window term | closed-form (the build measured on a card, the host approximate) |
|
||||
| Amortisation, the silicon | the core die USD 0.11 to 0.55 per MH/s, the memory USD 320 to 1,000 per board, the package, the board | spread over the life (0.5 to 3 years): 0.085 USD per TH at 3 years on the GDDR7 machine, 0.22 to 0.27 on the cards | capex per sustained MH/s is the larger half of the chip's edge (section 8) | the core measured placed, the memory and board claimed or approximate |
|
||||
| Selective participation | the per-epoch ratio spreads 9 percent (5th to 95th percentile 1.45x to 1.58x same-node) | a specialist mining the best x of epochs earns x of the revenue at +2 to +5 percent of ratio | nothing to gain | closed-form on the band (the draws uniform within it, approximate) |
|
||||
|
||||
The reading: capacity is a ticket (D once per machine, in DRAM at USD 10 per GiB or SRAM at USD 250), bandwidth is
|
||||
the activate ceiling per dollar of devices and is the card's own, energy is the memory's activate energy plus the
|
||||
shadow at the placed core's pJ per op, and amortisation is already complete at one machine (the set-up is a window
|
||||
term, the silicon a life term). The bounds that are closed-form: the capacity and state terms, the bit counts of
|
||||
data-local execution, the bytes, the set-up amortisation, selective participation. The bounds that rest on physical
|
||||
design: the SRAM macro's access energy (the one modelled term in the placed rows), the HBM3 activate ceiling, the
|
||||
SRAM die's wire term and the on-package hop, the device activate energies (claimed breakdowns), and the board's
|
||||
power train and cooling (approximate). Nothing in the model rewards a chip for anything but the memory's own
|
||||
activate energy and the shadow's pJ per op, which is where sections 6 and 13 put the honest bracket.
|
||||
|
||||
## 17. Review B's F05: the reg64 address mixer's incremental form, priced at the placed level (20:0x BST; the clock 23:30)
|
||||
|
||||
The finding: the connected-state candidate's address (the fold over all 63 live registers before every load) has an
|
||||
exact incremental form through a prefix-XOR tree, so a chip never reads and folds 63 registers per load; a design
|
||||
that assumes the literal fold overstates the chip's cost, and a one-register perturbation test says nothing about a
|
||||
minimum circuit. The shipped class (v4, v5, v6) folds ONE source register per load (this core's load op), so the
|
||||
finding touches the connected-state candidate (killed as a class at 17:25 BST on other grounds) and the bound it
|
||||
sets for any future class that couples the address to the whole window. Three designs for that coupling on this
|
||||
core, priced with the placed per-op figures (6.55 pJ per lane-op at N5 on the class v4 draw; the SRAM macro 3.5 pJ
|
||||
per 256-bit access, 2.0 to 7.0, shared by 8 lanes; the 102,612-op hash with 128 loads):
|
||||
|
||||
| Design | Extra state per lane | Extra work | Extra lane-ops per hash | Extra energy per hash at N5 | Share of the chip's shadow energy (0.672 microjoules) |
|
||||
|---|---|---|---|---|---|
|
||||
| The literal fold: read and fold 63 registers at each load | none | 63 reads of the window (SIMD, one macro access per 8 lanes each) and 63 rotate-xor ops per load | 128 x 63 = 8,064 (plus 8,064 macro accesses per 8 lanes: 3.5 pJ x 8,064 / 8 = 3.5 nJ) | 8,064 x 6.55 pJ + 3.5 nJ = 56 nJ (48 to 72) | +8.4 percent |
|
||||
| The prefix-XOR tree (the reviewer's form, floor lane 3's Fenwick reading): 65 words of prefix state, seven read-modify-writes per register write, eleven ops per load | 260 bytes in a second gated macro (one per 8 lanes, the same shape as the window) | 7 x 2 macro accesses and 7 xor-rotate ops per instruction that writes the window; 11 ops per load | on the base program's 512 writes and 128 loads: 4,992 ops and 7,168 accesses per 8 lanes; on every instruction of the hash (the shadow block writes the same window): 718,000 ops and 1.44 M accesses | base program only: 33 nJ of ops + 3.1 nJ of accesses = 36 nJ; every instruction: 4.7 microjoules of ops + 0.63 of accesses (7x the whole shadow) | +5.4 percent if the shadow block's writes are exempt; +800 percent if they are not |
|
||||
| The running total (this lane's form: the fold is linear over GF(2), so a write to register j changes the fold by a fixed rotation of old xor new, and the old value is already read in phase 0 of this core's slot) | one 32-bit register per lane (flops, written every instruction) | two rotates and two xors per instruction, merged into the write phase | 102,612 (one op's worth of datapath per instruction, no extra macro access) | about 1.0 pJ per instruction (an xor-rotate pair on operands already latched; the add row's datapath term is 1.7 pJ, approximate): 0.10 microjoules | +15 percent, paid on every instruction; +0.8 percent if only the base program's writes count |
|
||||
|
||||
Reading. (1) Which form the adversary picks depends on how many instructions write the window between loads: at
|
||||
the shipped shape (one load per 800 instructions once the shadow block is in) the LITERAL fold is the cheapest
|
||||
(+8.4 percent of the shadow, 56 nJ), because any incremental form pays per write and the shadow writes 800 times
|
||||
per load; the running total wins only if the shadow block's registers are exempt from the fold (then +0.8
|
||||
percent), and the Fenwick tree never wins on this core (its seven read-modify-writes per instruction are macro
|
||||
accesses, the one term this core pays dearly for). Floor lane 3's 30 nJ for the tree counts the base program's
|
||||
writes only; the row above shows both counts. (2) The bound the review asks for: the cheapest adversary form on
|
||||
the placed core costs at most 56 nJ per hash (the literal fold; 48 to 72 across the SRAM band) and at least 5 nJ
|
||||
(the running total with the shadow exempt), so the complete-machine bracket for a class carrying the whole-window
|
||||
fold moves from 1.5x (1.3 to 1.7) same-node to 1.42x to 1.49x (the chip's machine energy 1.528 to 1.53 + 0.056 x
|
||||
1.21 = 1.60 microjoules at the literal fold; 1.535 at the running total), and 1.8x a node ahead to 1.7x to 1.8x:
|
||||
the coupling buys the honest side at most 0.08x on this core, which is inside the SRAM term's band and agrees
|
||||
with the connected-state lane's KILL (1.10x against its 1.25x gate) and floor lane 3's 2.6 percent on the GDDR7
|
||||
board. (3) No row here assumes a 63-read cost that the adversary can avoid: the literal fold is priced as 63 SIMD
|
||||
macro reads shared by 8 lanes (3.5 nJ per hash of accesses, not 63 window reads per lane), and the incremental
|
||||
forms are priced per write. The shipped class is untouched: its fold reads one register. (4) The GPU side of the same two forms (the fleet
|
||||
lane, 20:4x BST, the hash lane's hl-v6-all fold form against hl-v6-all-prefix, the same program id and vectors,
|
||||
stock clocks, 250 batches, fingerprints equal; evidence on build-1 under /srv/artefacts/tas/54900078/ and
|
||||
/srv/artefacts/tas/si1xc4yhpakk1v/): the 5090 reads 70.6 MH/s at 437.8 W (6.20 nJ per hash) on the literal fold
|
||||
and 70.2 at 500.0 W (7.13 nJ) on the prefix form, 100 against 248 registers per thread; the 4090 31.3 MH/s at
|
||||
238.1 W (7.60 nJ) against 31.3 at 231.7 W (7.41 nJ), 93 against 154 registers. The rate is unmoved on both cards
|
||||
(both are bound at the dataset reads) and the prefix form costs the 5090 14 percent more board power for the same
|
||||
hashes; so on the card as on this core the cheapest form at the shipped shape is the literal fold, and the
|
||||
review's incremental form is a design the adversary can take and does not want. (5) The bound restated for the
|
||||
connected-state class's OWN shape (the hash lane's closed form, 20:5x BST: `address_source(s) = r[s] ^ ror(P_s, 1)
|
||||
^ (S ^ P_s ^ a[s])`, a[k] = rotl(r[k], (63 - k) mod 32), S the xor of every a[k], P_s the prefix xor below s;
|
||||
32 loads per 64-instruction iteration, 256 loads and 512 register writes per hash in the base program): there the
|
||||
literal fold is 256 x 63 = 16,128 lane-ops and 16,128 SIMD macro reads per hash, 113 nJ at N5 (97 to 145), and the
|
||||
prefix form is one running-total update per write (an xor-rotate pair, about 1 pJ) plus six Fenwick read-modify-
|
||||
writes per write (twelve macro accesses per 8 lanes, 5.3 pJ) and six prefix reads with three xors per load: 512 x
|
||||
12.3 pJ + 256 x 5.6 pJ = 7.7 nJ (5 to 15). So on that class the review is right by 14x: the chip pays about 8 nJ
|
||||
per hash for the whole-window coupling, 1.2 percent of its shadow energy, and the complete-machine bracket moves
|
||||
from 1.5x to 1.49x same-node (not to 1.42x, which was the literal fold's cost); the card meanwhile keeps the fold
|
||||
at 6.20 nJ per hash on the 5090 and would pay 7.13 on the prefix form. The two shapes together: the chip's cost of
|
||||
the coupling is 8 nJ (connected-state shape, prefix form) to 56 nJ (shipped shape, literal form), 0.02x to 0.08x
|
||||
of the bracket, and no design in the record charges the chip a 63-read cost it can avoid.
|
||||
|
|
|
|||
54
docs/analysis/pool/pool-pair-2026-10-08.md
Normal file
54
docs/analysis/pool/pool-pair-2026-10-08.md
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# The devnet-4 pool pair, 8 October 2026 (UX-05, UX-04 evidence; the night's record)
|
||||
|
||||
Pool seat, 8 October 2026, 20:0x to 21:4x UK. Binaries: the node /srv/artefacts/200-12424341/node-lane/igneumd on
|
||||
build-2 (successor-2.0.1's gate build), the pool daemon from pool-2.0 (8106ba27 then e063fdcd; igneum-pow fingerprint
|
||||
cbc5bd0aa10585c8, the freeze 1c420786), two CPU members from the fork at 2b1a247a (the same fingerprint; a test
|
||||
binary, never shipped). Two Vast hosts rented for the pair (i7-5820K, Xeon E5-2609 v3, Haswell) died with Illegal
|
||||
instruction at the class v5 catch-up on every node build and were destroyed; the pair ran on build-2 under lease
|
||||
pool class measure and was brought down by pid file at 21:3x UK (0 leases, 0 processes left).
|
||||
|
||||
## UX-05 Keep voting keys with the miner through pooling: PASS in the crate
|
||||
|
||||
| Test (pool crate, build-2) | Known-pass | Known-fail | Result |
|
||||
|---|---|---|---|
|
||||
| `verify::tests::a_share_under_another_key_is_refused_before_the_hash` | the member's key on job and share: ok | another key on the share, and a job naming another key: `vote_key`, hash 0, under a tenth of an evaluation | ok (51 of 51 at e063fdcd, 52 of 52 at 986252e7) |
|
||||
| `sidechain::tests::a_share_whose_keys_disagree_is_refused_before_its_pow` | make_share | the claim swapped, the header's key swapped, a foreign reveal | ok |
|
||||
| `the same nonce on another template hashes differently` | | a nonce moved to another template: `wrong_hash` | ok |
|
||||
| the TLS binding (replay refused), the admission bounds, the intents (review B) | | | ok |
|
||||
| `an_unsynced_node_hands_the_pool_no_state_and_no_job` (986252e7) | a synced node: leaves served, jobs issued | synced false: no leaves, no job | red against 8106ba27 on build-6, green with the guard |
|
||||
|
||||
The live pair reached: authorise with the members' own keys (the pool log names each key beside its payout address),
|
||||
class v5 seeds issued (epoch 1a87e870..., day 20734, era 7c36b833...), jobs issued with `vote_key_hash` the
|
||||
member's, shares sent. Every share read `wrong_hash`.
|
||||
|
||||
## The wrong_hash cause, from the retained logs (not a generator skew)
|
||||
|
||||
The node, the pool and the member carry the same igneum-pow (fingerprint cbc5bd0aa10585c8, the freeze; pool-2.0's
|
||||
tree 93c36844 byte-identical to 1c420786, read back by the pool lane from the mirror and from the binaries). The pool
|
||||
and the members hashed the identical epoch seed, day, class and era. Build-2's node never reached synced on devnet-4
|
||||
(its log loops on "class v5 execution catch-up" against peers that close the connection; `synced=false` on every
|
||||
read), so the class v5 state leaves the pool and the members fetched by `igneum_getPowStateLeaves` for the epoch's
|
||||
seed block were the still-settling catch-up state, not the node's state at template time; a class v5 lane hash over
|
||||
unsettled leaves does not match the node's. The class kept: a class v5 pool needs its node fully synced before it
|
||||
verifies shares, because the dataset is keyed on settled execution state. The guard by construction is pool-2.0
|
||||
986252e7 (the provider refuses leaves while `igneum_getExecStatus` reads unsynced, blocked or reexecuting; the feed
|
||||
issues no job; the STATUS line says so).
|
||||
|
||||
## Two 2.0 gaps closed by the pair (both on pool-2.0, in the 2.0.2 take)
|
||||
|
||||
1. The pool daemon had no class v5 day-state source and issued no job on a class v5 chain (8ff7a0f4:
|
||||
`state_provider.rs`, `--exec-rpc` defaulting to `--evm-rpc`).
|
||||
2. The daemon read amounts at 8 decimals and refused every 18-decimal template as a high-word amount (8106ba27: the
|
||||
base unit installed from the node's network before any amount is read).
|
||||
|
||||
## UX-04 Pay small operators without hidden custody
|
||||
|
||||
PPLNS distribution, the payout key round trip and the signed transfer decode: PASS in the crate. The live payout path
|
||||
(a member earns, is paid at the minimum, reconnects, a redirection attempt) is NOT RUN: it needs the pair on a
|
||||
fully-synced devnet-4 node with the 2.0.2 kit, tomorrow.
|
||||
|
||||
## Consequences per tier
|
||||
|
||||
A home miner on a pool: the key stays theirs on every job and share and a pool naming another key is refused before
|
||||
the hash; a pool operator: the daemon refuses to issue jobs until its node is synced, so an unsynced start costs idle
|
||||
minutes, never a wrong_hash storm; the network: no change to consensus from any of this.
|
||||
File diff suppressed because one or more lines are too long
21
docs/plans/evidence/UX-01-20261008.md
Normal file
21
docs/plans/evidence/UX-01-20261008.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# UX-01 evidence, run ux-01-20261008-win-2.0.0 (8 October 2026)
|
||||
|
||||
Onboard ordinary owners on native desktop apps, the team-run half: the two Windows PCs take the Igneum Miner 2.0.x Windows
|
||||
entry (2.0.0 live 18:44 UK, Setup exe 793a631d, manifest aa354ed5; 2.0.1 about 20:55 UK with the coinbase-over-u64 fix)
|
||||
through the app's own update path, with no click after the install and mining on every card (the founder's rule of
|
||||
18:0x UK), observed through the apps' own intake uploads and the relay agents. The P10 study (30 unaffiliated
|
||||
participants) is NOT RUN: not yet recruited; never a staff run.
|
||||
|
||||
## Shape
|
||||
- PC 1 (ae432dc7): Igneum Miner 0.3.26 since 17:26 UK, RTX 5090, RTX 5080, RX 7600; mining off under the Devnet 3 off order; the 2.0.0 installer downloaded and verified 18:58 UK, the install queued in the hash lane's order (update-now-20261008-181124).
|
||||
- PC 2 (1ccfe586): RTX 5090, RTX 5060 Ti, Arc B580; mining off since the Devnet 3 off jobs (#1519 api/pause 16:38 UK, #1521 17:15 UK).
|
||||
- The read-back: the engine's "update-return: app <v> up after the update from <from>" line at intake, then the relay run "mining on after the 2.0.1 install" (mining-on-after-200.ps1: wait for 2.0.1 or later, clear a stale install-running.flag, api/resume, start the app if silent) printing `RESULT MINING-ON ok|partial version=... network=... node=... synced=... paused=false cards=<n> rates=...`.
|
||||
|
||||
## Read-back lines (time UK, PC, the line or the fault and its class)
|
||||
- 19:22:01 PC 2: `update-return: app 2.0.0 up after the update from 0.3.26 (node igneumd 2.0.0, machine 1ccfe586)`; node started 19:22:05; `[ok] mining resumed` 19:22:13 (no click: relay run #1742 cleared the pause through api/resume).
|
||||
- 19:28 PC 2 (#1754): `RESULT MINING-ON partial version=2.0.0 network=devnet node=no peers synced=False paused=False cards=3` (every card waiting). Class: the devnet-4 hubs' outage (every dial target down 19:15 to 19:17, back by 19:29), not the install.
|
||||
- 19:41 PC 2 (#1757): `RESULT MINING-ON partial ... node=behind peers=1 synced=False tip_age_s=1289`; IBD completed 19:29:16, the node then "behind". Class: the 2.0.0 miner refuses every template whose coinbase exceeds a u64 (the shipper, 19:5x UK), fixed in node 777214af as the 2.0.1 entry.
|
||||
- PC 1: pending (0.3.26; the install runs when its queue reaches update-now, then 2.0.1 through the same path).
|
||||
|
||||
## Status
|
||||
- 20:36 UK: RUNNING; the 2.0.1 read-backs (relay runs #1762 PC 2, #1763 PC 1) are the next lines.
|
||||
48
docs/plans/finality-native-runs-2026-10-09.md
Normal file
48
docs/plans/finality-native-runs-2026-10-09.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# The native finality runs of 9 October 2026 (the node lane; Review B F04 and I03; V6-09 beside them)
|
||||
|
||||
Status: PLANNED (written 8 October 2026, 21:4x UK, main's order under the night rule). The runs start from 00:30 UK on build-8 and build-9 (build-7 for the two-node cache case), under the lease pool, every process under a pid watcher that restarts it and records the restart; the rows land in `sim/results_v2.md` under "Rule v4" with their result files under `sim/finality-attacks-results/` and the registry batch (FIN-08, FIN-02's native half, ROT-05, VER-08's recovery row) through `tools/ci/test-record.mjs`, by 07:00 UK. An accelerated simulation is evidence of the rule's shape, never operating history: every row below runs on real nodes with the 60x file's windows (W = 120 DAA s at 1 block/s) and says so.
|
||||
|
||||
The harness: `tools/finality-attacks/v3.mjs` (three nodes on the 60x file, six voters, one-way delay 300 ms per proxied link, the pass lines of `finality-guarantees.md` 6.7), extended for the rows that need a third island, an equivocating key, a stopped voter, a succession item and a backfilled checkpoint history; the extensions land with the rows. The node: the 2.0.2 line (successor-2.0.1 at or after 45e7b910: rule v4 with the pause fix 6872db13, the lock kind of F04, the finality-backed take of a9ff0a25) in the gate pair under `/srv/artefacts/200-<sha>/node-lane` (gate evidence; never a fleet binary).
|
||||
|
||||
## 1. The 40/40/20 case past the window with equivocation (F04, the reviewer's hard FAIL line)
|
||||
|
||||
Three islands by weight 40 / 40 / 20 (keys p0,p1 on n0; p2,p3 on n1; q0 on n2, shares 0.2 / 0.2 / 0.2 / 0.2 / 0.2 across five keys, the sixth key e the equivocator at 0.2 placed by the row), WARM 230 s, SPLIT 420 s (longer than the window after the last lock), HEAL 400 s, 1 block/s in all, rule v4 with the recovery on.
|
||||
|
||||
| row | the equivocator e | expected under 6.2 and 6.5 | the FAIL line |
|
||||
|---|---|---|---|
|
||||
| 1a | absent (honest three-way) | no side locks during the split (no island holds more than half of the anchored table), every node pauses, the heal locks within two intervals, 0 conflicts | any lock during the split; any conflicting certificate |
|
||||
| 1b | mines on island A only (reaching 60 of the anchored table on A) | A recovers once a full window has passed (the recovery lock at the first index past the window), B and C pause, the heal brings B and C onto A's chain, 0 conflicts | a lock on B or C; two certificates at one index |
|
||||
| 1c | mines dust-valid on A AND B (the 6.5 bound: both at 60 percent) | BOTH A and B recover after the window: two recovery certificates at one index, the measured conflict the spec names; the heal strips e (3.6) and reports the pair under 3.11.4; the history through the anchored lock untouched | a recovery lock presented as final on any surface (lockKind must read "recovery" on both); the anchored history moving |
|
||||
| 1d | 1c under the pause-only variant (recovery off) | no lock on any side during the split, the pause until the heal, 0 conflicts (the strictly stronger guarantee of 6.5) | any lock during the split |
|
||||
|
||||
Measured per row: new locks per side during the split (index and DAA), the lock kind on `igneum_getFinalityCheckpoints` (final or recovery), conflicting certificates logged, disagreeing locked indices after the heal, the equivocator's strip at the heal (the ban line), every pre-heal lock kept, the first lock after the heal (s).
|
||||
|
||||
## 2. The pause-only alternative with backfill, missing history and the old keys returning (I03)
|
||||
|
||||
Rule v4, recovery off. A chain is run 230 s with six voters, then split 3/3 for 420 s (no lock on either side, the pause), then healed; during the heal window: (a) a fresh node joins from genesis and must backfill every checkpoint and certificate (the historical-checkpoint backfill; it reads the same latest lock as the three), (b) one node restarts from a datadir with its finality state removed (missing historical data: it rebuilds from the chain's carried certificates and must agree), (c) every old key returns and signs (the pause ends on two thirds, the lock within two intervals). Measured: the backfilled node's locked indices equal the others' (0 disagreement), the restarted node's, the first lock after the return, 0 conflicts.
|
||||
|
||||
## 3. Authority succession and strip, restart, certificate arrival order, seed boundaries (I03)
|
||||
|
||||
| row | shape | expected |
|
||||
|---|---|---|
|
||||
| 3a | a voter leaves (W7, the leave item carried) mid-window, the rest sign | the frozen table reduces by the leaver (frozen_floor), the next lock at two thirds of the remaining; the leaver's weight never counts |
|
||||
| 3b | a key is stripped (an equivocation evidence item carried) one interval before a lock | the strip applies before the lock's test; a certificate carrying the stripped key's signature counts it as 0 |
|
||||
| 3c | a voter restarts between determination and lock (kept datadir) | it votes once (no double vote), the lock forms on time, its locks equal the others' after the restart |
|
||||
| 3d | two certificates for consecutive indices arrive in reverse order at a node (the fleet's 263-then-262 read on hub-1) | both lock under the rule, the LOCKED line prints the certificate's fractions (the 2.0.2 log fix), no "signed 0" artefact |
|
||||
| 3e | a seed boundary (the epoch's reference block) inside a pause | the seed is drawn from the chain block below the lead whether or not it is locked (section 8); mining continues; the first lock after the pause names a block past the boundary |
|
||||
|
||||
## 4. The inter-chain verifier and a recovery lock (I03)
|
||||
|
||||
The Sepolia certificate verifier (dex lane's bridge, 0x874D8Be5… on igneum-devnet-4's voter table) given (i) a final certificate, (ii) a recovery certificate from row 1b: it accepts (i) as final; for (ii) it must read lockKind "recovery" and never present it as final (the bridge doc 829b839ec: recovery locks fail closed on the verifier, which reads only weight). The row records what the verifier answers for each and the receipt page's word; a recovery certificate accepted as final is the FAIL line.
|
||||
|
||||
## 5. V6-09, cold compressed verification on the minimum validator (with the enforced-proving lane)
|
||||
|
||||
One validator node pinned to one core (taskset), no warm relay cache (fresh datadir, the pool empty), fed by a relay peer: (a) ten cold valid shard proofs in one block's carried records (the measured 0.668 to 0.710 s a proof on one loaded core); (b) ten expensive-invalid payloads (proofs whose bytes deserialise to the largest accepted shape and fail at the last check) with the relay's pre-deserialise cap (MAX_PROOF_BYTES in consensus-core, V6-08's constant) and the in-flight bound of 8; (c) a forged record under each pair across the key and fee transitions, before, at and after activation, on an unmodified validator. Measured per row: verify seconds per proof, block-validation time against the deadline, the NotReady retries, memory before and after deserialising, the carrier paid exactly once (igneum_getProofRecords on the validator against the relay's), the forged record refused with its reason. The rows go to the test map cell `harness:v6-09-cold-verify` with the F0 fixture, by 12:00 UK 9 October.
|
||||
|
||||
## 6. The two-node cache-history test (F01, with the proving lane's fix 3f672661)
|
||||
|
||||
Two nodes, one with a warm verdict cache built on carriers 1..k, one cold, both fed the same blocks in a different order (the cold one sees the later carrier first): identical block-validity verdicts and identical payouts (igneum_getProofRecords and the paid map equal on both), with the known-failed shape first (the pre-fix node's cached context refusal replayed under a later carrier, the 19:49 UK reproduction). On build-7 by 03:00 UK.
|
||||
|
||||
## Boxes and clocks
|
||||
|
||||
build-8: sections 1 and 2 (the 40/40/20 rows 1a to 1d, then the backfill case), from 00:30, rows by 05:00. build-9: sections 3 and 4 (after the 2.0.2 line's gate ends), from 01:00, rows by 06:00; section 5 with the enforced-proving lane's fixtures, rows by 12:00. build-7: section 6, by 03:00. Every run under `lease pool N --class release` with a pid file and a watcher; a stall is recorded for the 08:00 read-back, never a hand-made lock. The rows land in sim/results_v2.md with their result files and the registry batch by 07:00 UK, the plan's status moved to MEASURED per section as each lands.
|
||||
|
|
@ -6,7 +6,7 @@ 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) | 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) |
|
||||
| Miner cut tip (release-2.0.1) | aa0e0f45 is the shipped tip (the entries stand on its binaries); release-2.0.1's final tip is c30ab32c (aa0e0f45 + the pool lane's pool/ and docs a54dffa3 + the release manifest f03-manifest-201 7437a31a merged at 800faaf7 + the hand-merged spec 09; no app, node pin or packaging change). The clean build from the manifest (R2-F03-R01) read green on c30ab32c at 21:14 UK on build-4: kaspad with igneum-pow, igneum-miner, igneum-pool, igneum-app, igneum-prove-host, and every component's own pin equals packaging/release-manifest.json. (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 lines 19:5x and 21:0x; the steward's build 21:14 | 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 |
|
||||
|
|
|
|||
48
docs/plans/igneum-2.0-same-work-test.md
Normal file
48
docs/plans/igneum-2.0-same-work-test.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# The cross-backend same-work test (F03, Review B; specified by the CI steward, 8 October 2026, 20:1x UK)
|
||||
|
||||
One job context, six readers, three phases around a transition, bit-for-bit agreement. Run by the fleet lane with the freeze object; the evidence recorded against POW-01 through the batch tools.
|
||||
|
||||
## The job context (one file, `job-context.json`, written once by the steward or the hash lane and copied to every reader)
|
||||
|
||||
| Field | Value tonight | Source |
|
||||
|---|---|---|
|
||||
| object | the class v5 freeze: igneum-pow 1c420786, fingerprint cbc5bd0aa10585c8576e71e37a8ee47a045ae51754e9ddf749d0c21e6a535f88 | packaging/release-manifest.json (generator) |
|
||||
| epoch seed bytes | edc4fa844da9dc98d37e965176f6558a31560e40502ab3ae5491b21aaaabfb07 (the genesis seeds) | the hash lane's P01 line |
|
||||
| day bytes | 69676e65756d2d6461792ffa50000000000000 | the hash lane's P01 line |
|
||||
| class | 5; era 0:<the epoch hex> | the pack's program.json |
|
||||
| prehash | 0000000000000000000000000000000000000000000000000000000000000001 | the P01 convention |
|
||||
| nonce range | 0 .. 2^20 per phase (three phases, three ranges: [0, 2^20), [2^20, 2^21), [2^21, 2^22)) | this page |
|
||||
| transition | the day boundary: day D for phase 1, the boundary block for phase 2 (the last 2^12 nonces of day D and the first 2^12 of day D+1 as the engine sees them under fast-time 60x), day D+1 for phase 3 | infra/fast-time/override-60x.json |
|
||||
|
||||
The transition is a dataset-day rotation (the class and era unchanged, the day bytes change), the one transition every live network crosses hourly at 60x; a class rotation (v5 to v6) is the same test with `class` and the second `day bytes` changed and runs only on a research object until a v6 floor is set.
|
||||
|
||||
## The six readers (every one writes `<reader>-<phase>.json` of the P01 driver's evidence shape: nonce, hash per line in the raw file; agree, disagree, missing, the first ten disagreements, the device line, the pack and program ids, the manifest sha in the JSON)
|
||||
|
||||
1. **node**: `igneumd` at the manifest's node sha, the engine's own re-check (`IgneumEngine::epoch_for` then `hash_bound` per nonce) through `igneum-miner --recheck-vectors` on the node binary's CPU path (the pool.rs seam), on build-2.
|
||||
2. **CPU reference**: `igneum-pow hash-bound --count 2^20 --nonce <start>` from the manifest's generator commit, on build-2 (the hash lane's reference files are this reader).
|
||||
3. **CUDA**: the kit's `igneum-worker-cuda --serve` driven by `tools/ci/p01-vectors.py` with the job context, on a 5090, 4090 and 3090 pod.
|
||||
4. **OpenCL**: `igneum-worker-opencl --serve` the same way, on the AMD pod when one exists, else PC 1's RX 7600 (the only OpenCL retail cell tonight).
|
||||
5. **Metal**: the Mac's own worker, the same driver, on the mini (the morning's run; never on this Mac).
|
||||
6. **pool**: `igneum-pool` at the manifest's sha with its vendored node at the manifest's node sha, the member-side re-check (`pool/src/node.rs` → `kaspa_pow::igneum::IgneumEngine`) on the same job lines, on build-2.
|
||||
|
||||
## The three phases
|
||||
|
||||
Phase 1 (before): every reader on range 1 under day D. Phase 2 (during): every reader on range 2 where the engine's day moves from D to D+1 inside the range at the boundary nonce the fast-time clock sets; every reader must switch program and dataset at the same nonce. Phase 3 (after): every reader on range 3 under day D+1.
|
||||
|
||||
## Acceptance (the registry row POW-01, cell `harness:same-work`, PASS only when all hold)
|
||||
|
||||
- every reader's hash equals the CPU reference's for every nonce of every phase (zero disagreements, zero missing);
|
||||
- the program id and the day each reader reports at phase 2's boundary are the same across readers (the transition is seen at one nonce);
|
||||
- the pool's accepted-share verdict for a sample of 64 nonces per phase equals the node's (the seam closes by a build: `release-manifest-check.sh` refuses a redefined EpochSeeds and an unpinned vendored node);
|
||||
- the run names the manifest sha (packaging/release-manifest.json), and the evidence sits at build-1:/srv/artefacts/tas/same-work-<run id>/.
|
||||
|
||||
A disagreement on any reader is a red to main within fifteen minutes (the coordinator's rule for the cross-checks).
|
||||
|
||||
## What is still to build (owners, defaults)
|
||||
|
||||
| Piece | Owner | Clock | Default if silent |
|
||||
|---|---|---|---|
|
||||
| `p01-vectors.py --job-context <file> --phase N` (the three ranges and the boundary assertion; the driver already drives the workers) | CI steward | 21:30 | built as specified |
|
||||
| the node reader (`igneum-miner --recheck-vectors`, the seam with a count) | node lane | 22:00 | the steward builds it on a branch of release-2.0.0-node under rule 24 |
|
||||
| the pool reader (the member-side re-check over a job-lines file) | pool lane | 22:30 | the pool's existing recheck_pack path over the context, driven by the steward |
|
||||
| the pods and the mini | fleet lane | on the artefact line | as P01 |
|
||||
|
|
@ -133,8 +133,8 @@ Rule: a case maps to a cell only where the cell's tests visibly answer it; cover
|
|||
|
||||
### bench:pc1-packs
|
||||
|
||||
- Command: `tools/ca3-v4-amend/pc1-ca4-packs.ps1 on PC 1 (RTX 5090, the signed-jobs channel, --cards-off, the Power Helper lock at 1,300 MHz, nvidia-smi at 1 Hz, 250 x 2^24 per row, the kit's CUDA worker --bench)`
|
||||
- Box class: PC 1 bench
|
||||
- Command: `tools/ca3-v4-amend/pc1-ca4-packs.ps1 on the project's own rig (RTX 5090, the signed-jobs channel, --cards-off, the Power Helper lock at 1,300 MHz, nvidia-smi at 1 Hz, 250 x 2^24 per row, the kit's CUDA worker --bench)`
|
||||
- Box class: the project's own rig bench
|
||||
- Fixtures: F0, F1
|
||||
- Cases:
|
||||
- GPU-02 Reproduce Ember clock-lock savings: partial: the paired stock and locked rows on the same board, host and workload (the rate held, 2.37 against 3.26 microjoules per hash on the class v5 pack); the historical 34 to 41 percent claim's full configuration set is owed
|
||||
|
|
@ -144,8 +144,8 @@ Rule: a case maps to a cell only where the cell's tests visibly answer it; cover
|
|||
|
||||
### bench:pc1-amd
|
||||
|
||||
- Command: `tools/ca3-v4-amend/pc1-amd-cardin-bench.ps1 on PC 1 (RX 7600, OpenCL, gfx1102, 30 dispatches of 2^24 per size, rate only)`
|
||||
- Box class: PC 1 bench
|
||||
- Command: `tools/ca3-v4-amend/pc1-amd-cardin-bench.ps1 on the project's own rig (RX 7600, OpenCL, gfx1102, 30 dispatches of 2^24 per size, rate only)`
|
||||
- Box class: the project's own rig bench
|
||||
- Fixtures: F0, F1
|
||||
- Cases:
|
||||
- GPU-01 Cover the declared commodity population: partial: the 8 GB AMD cell, fingerprints and rates at 1, 2, 4 and 5.5 GiB; one cell of P02's twelve
|
||||
|
|
@ -243,7 +243,7 @@ Rule: a case maps to a cell only where the cell's tests visibly answer it; cover
|
|||
- 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-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 the earlier devnet 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
|
||||
|
|
@ -336,24 +336,80 @@ Rule: a case maps to a cell only where the cell's tests visibly answer it; cover
|
|||
|
||||
### bench:amd-intel-energy
|
||||
|
||||
- Command: `the AMD-and-Intel energy lane jobs on the signed-jobs channel: run-ae-pc2-b580-energy-20261008 (PC 2, Arc B580, the class v6 kit worker --memprobe and --bench-pack on hl-v6-foldrw and hl-v6-all, Level Zero energy counter) and run-ae-pc1-7600-energy-20261008 (PC 1, RX 7600, the same with ADLX watts at stock, plimit -30, gmax -500 + plimit -30); scripts in docs/analysis/class-v6/amd-intel-energy/`
|
||||
- Box class: PC 1 and PC 2 bench (OpenCL)
|
||||
- Command: `the AMD-and-Intel energy lane jobs on the signed-jobs channel: run-ae-pc2-b580-energy-20261008 (PC 2, Arc B580, the class v6 kit worker --memprobe and --bench-pack on hl-v6-foldrw and hl-v6-all, Level Zero energy counter) and run-ae-pc1-7600-energy-20261008 (the project's own rig, RX 7600, the same with ADLX watts at stock, plimit -30, gmax -500 + plimit -30); scripts in docs/analysis/class-v6/amd-intel-energy/`
|
||||
- Box class: the project's own rig and PC 2 bench (OpenCL)
|
||||
- Fixtures: F0, F1
|
||||
- Cases:
|
||||
- GPU-03 Measure the real 64-register GPU cost: partial: the 64-register window on AMD and Intel, rate per unit of work (RX 7600 0 percent, Arc B580 -0.3 percent, kernel throughput, quiet) with the B580 fingerprints equal on both packs and the offline RDNA allocation (160 VGPRs, no spill); energy owed (the 7600 job queued, the B580 counter unsupported unelevated); team-run, not under the standard's paired protocol or a wall meter
|
||||
|
||||
### adversary:mf-placed
|
||||
|
||||
- Command: `cd tools/chip-model/mf && make flow-<design> power-<design> (inside openroad/orfs:latest on a rented CPU host; never the Mac); python3 flow/collect.py results; python3 flow/board.py <pJ> <lo> <hi> <leak> <mm2>; python3 flow/hash.py results <design> power`
|
||||
- Box class: rented CPU host (Vast, 48 to 72 cores) on the ORFS image
|
||||
- Fixtures: F0, F1
|
||||
- Cases:
|
||||
- ADV-01 Build a multi-family programmable opponent: the mf core (18 families, SRAM window and imem macros, operand isolation, 5-phase port) placed and routed on ASAP7 with SPEF and a gate-level VCD; k per family and per draw; outputs checked against the RTL simulation's checksums per tag, not yet against the POW vectors (owed)
|
||||
- ADV-03 Attack with data-local and hybrid execution: the data-local, hybrid and companion-host rows of multi-family-adversary.md sections 11, 13 and 14 (bit counts closed-form; the hop and wire energies claimed)
|
||||
- ADV-07 Evaluate lifetime without forced obsolescence: the lifetime table and the six-row transition matrix (sections 7 and 14) on the placed rows; the 32-lane genesis comparator placed on adv-g
|
||||
- ADV-08 Independently challenge the best-cost envelope: the best-cost envelope on the complete machine (section 6) and the SRAM-die ticket reconciliation with lane B (section 15); the unaffiliated second reviewer is not this lane's
|
||||
- POW-03 Test whether live state is unavoidable: partial: the live state held in an SRAM macro per 8 lanes with a time-multiplexed single port and operand isolation, the complete-system cost in section 6; the liveness trace itself is the connected-state lane's tool
|
||||
- POW-04 Evaluate connected-resource restructuring: partial: the 64-register window's cost on the macro core (sections 4 and 5, agreeing with the k lane's gated-flop row within 5 percent); the restructured candidate's GPU cost is the invention lane's
|
||||
- ADV-05 Validate physical and complete-board costs: partial: the SRAM macros, ports, wiring, clocking and the complete-board terms are modelled in sections 2.3, 5 and 6 with the unmodelled items carried as uncertainty; the calibration against an existing hardware block is the k lane's bare-lane row beside it, owed as a named comparison
|
||||
|
||||
### adversary:d2b
|
||||
|
||||
- Command: `cd tools/chip-model/mf && python3 flow/d2b.py results N5 1 && python3 flow/d2b.py results N3 1 (on a build box under lease pool or a rented host; reads results/table.csv)`
|
||||
- Box class: any box (a one-minute Python model on the placed rows)
|
||||
- Fixtures: F0
|
||||
- Cases:
|
||||
- ADV-02 Price shared, reduced and reconstructed memory: memory sharing, partial stores at the measured window-layer hit rates (and hit 0.50 with layer 8 off), recomputation and the set-up amortisation on the complete board (section 13); the formal memory model's terms (section 16)
|
||||
- ADV-04 Measure profitable selective participation: selective participation over 2,000 era draws under the layer 1 band with two reserve families live per epoch, the specialist's revenue against its ratio gain (section 13); downtime and re-entry priced as the DAA window's lag, not simulated against the difficulty rule itself (owed)
|
||||
|
||||
### pc:install-update
|
||||
|
||||
- Command: `signed jobs and relay runs on the project's own rig (ae432dc7) and PC 2 (1ccfe586): the installer over the running app (rule 14: installed versions, the process set, the first upload), the OTA path (update-now, the update-return read-back line), and the no-click resume after the install (relay run mining-on-after-200.ps1)`
|
||||
- Box class: PC (the two Windows PCs; nothing on the Mac)
|
||||
- Fixtures: F2
|
||||
- Cases:
|
||||
- UX-01 Onboard ordinary owners on native desktop apps: team-run evidence only (the two PCs taking the Windows entry through the app's own update path with no click and mining on every card); the P10 study is NOT RUN until unaffiliated participants are recruited
|
||||
- UX-07 Expose actionable failures and safe updates: partial: the install classes (the payload's stop step, the detached installer under a job, the installer refusing a wrong-version name, a stale install flag) and the no-click resume; the recovery-instruction and canary rows are the shipper's
|
||||
- OPS-03 Separate update distribution from consensus authority: partial: the update path's read-back and the signed manifest's verify on the PC (a tampered or rolled-back payload is refused by the engine's manifest check); authority separation is the shipper's review
|
||||
|
||||
### harness:release-manifest
|
||||
|
||||
- Command: `bash tools/ci/release-manifest-check.sh (self-test, then the tree; the pins, the proof guests' hashes on disk, the fork freeze, the pool's vendored node, the proving manifest's source_commit provenance) and bash tools/ci/build-from-manifest.sh --box N on the release tree (kaspad with igneum-pow, igneum-miner, igneum-pool, igneum-app, igneum-prove-host from packaging/release-manifest.json with the node vendored at the manifest's sha)`
|
||||
- Box class: gate
|
||||
- Fixtures: F0
|
||||
- Cases:
|
||||
- R2-F03-R01 Clean build from one manifest, including the pool and workers, with no unpublished vendor tree.: full for the components the tree builds (node, miner, pool, app, prove-host from one manifest, no unpublished vendor tree); the GPU workers are the kit's cross-build and read under the shipper's kit-isa line, not this cell
|
||||
|
||||
### harness:same-work
|
||||
|
||||
- Command: `tools/ci/p01-vectors.py --job-context <job-context.json> --phase 1|2|3 per reader (CUDA, OpenCL, Metal workers), igneum-miner --recheck-vectors (node), igneum-pow hash-bound (CPU reference), the pool's member-side re-check; docs/plans/igneum-2.0-same-work-test.md`
|
||||
- Box class: release (the readers on their pods and boxes)
|
||||
- Fixtures: F0
|
||||
- Cases:
|
||||
- R2-F03-R02 Same job context produces identical accepted work in node, CPU reference, CUDA, Metal, OpenCL and pool.: partial until every reader has run: bit-for-bit agreement per nonce across node, CPU reference, CUDA, OpenCL, Metal and pool on one job context
|
||||
- R2-F03-R03 Cross every scheduled transition with old/new client behavior documented and identical rule identities.: partial: the day-boundary transition in phase 2 (the one transition every live network crosses hourly at 60x); a class rotation is the same test on a research object until a v6 floor is set
|
||||
|
||||
### canary:fresh-install
|
||||
|
||||
- Command: `the rule 33 fresh-install canary on a release tip (the founder, 8 October 2026): install from the published artefact on a non-AVX-512 box with an empty datadir, sync genesis to tip, five minutes mining with zero refusals and an accepted block, one shard claimed, proved and paid or queued, a bounded quit, every line read back; the record tools/ci/canary/<sha>.json (one file per sha, an artefacts list: fleet, windows, mac, hive) read by tools/ci/canary-check.sh <sha> [--artefact kind], which publish-manifest.sh and publish-public.sh refuse without`
|
||||
- Box class: release (a non-AVX-512 box with an empty datadir)
|
||||
- Fixtures: F0
|
||||
- Cases:
|
||||
- INT-07 One v6 object agrees in node, pool, CPU verifier and each supported GPU host across activation.: partial: V6-12's clean-install half (one published object installed fresh, synced, mined, proved and paid on one host per artefact); the cross-host agreement half (node, pool, CPU verifier, each GPU host across activation) is harness:same-work's
|
||||
|
||||
## 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
|
||||
- GOV-04 Preserve raw and negative evidence: the evidence vault F9 (raw and negative evidence preserved) is the gate rule landing by 21:00: a PASS must carry its evidence file
|
||||
- GOV-08 Invalidate stale evidence and control public status: the stale-evidence rule (evidence older than the manifest sha reads NOT RUN) is the gate rule landing by 21:00
|
||||
- GPU-04 Find the memory-clock operating ladder: the memory-clock ladder has no harness tonight: PC 1 mines nothing under the Devnet 3 off order
|
||||
- GPU-04 Find the memory-clock operating ladder: the memory-clock ladder has no harness tonight: the project's own rig mines nothing under the the earlier devnet off order
|
||||
- GPU-06 Measure accepted work under ordinary connectivity: accepted work under ordinary connectivity needs the fault network F4
|
||||
- GPU-07 Survive sustained thermal and power operation: the sustained thermal and power soak has no harness tonight: PC 1 mines nothing under the Devnet 3 off order
|
||||
- GPU-07 Survive sustained thermal and power operation: the sustained thermal and power soak has no harness tonight: the project's own rig mines nothing under the the earlier devnet off order
|
||||
- POW-05 Prevent amortised cheap winning attempts: amortised cheap winning attempts are the attack lanes' grind and era harnesses (tools/attack/f7-era, f9-grind), not in the release matrix; their rows come from those lanes
|
||||
- ADV-04 Measure profitable selective participation: selective participation needs the economic model F7 and a live chain window
|
||||
- ADV-06 Separate process advantage from specialisation: process-advantage separation is the adversary lanes' chip study
|
||||
- ADV-07 Evaluate lifetime without forced obsolescence: lifetime rows are the adversary lanes' models
|
||||
- ROT-03 Test miner-voted bring-forward governance: miner-voted bring-forward needs a vote harness on the fault network F4
|
||||
- ROT-04 Resist seed selection and faster evaluators: seed-selection resistance is the census harness (the class v6 invention lane), not yet in the matrix
|
||||
- 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
|
||||
|
|
@ -440,7 +496,6 @@ Rule: a case maps to a cell only where the cell's tests visibly answer it; cover
|
|||
- INT-04 Corrupt existing pool state, disk-full and reorg cause safe recovery, not silent empty balances.: INT-04 (R1 integration gate): the gate's harness is the owner lane's (pool lane (a1c484c48a62948c2)); not yet named in the map
|
||||
- INT-05 The supplied finality implementation matches the approved anchor rule after >window healing.: INT-05 (R1 integration gate): the gate's harness is the owner lane's (node lane (a283f5f0d364ceef0)); not yet named in the map
|
||||
- INT-06 Recovery tests state and preserve their weaker fault bound; interfaces never label it as a stronger guarantee.: INT-06 (R1 integration gate): the gate's harness is the owner lane's (node lane (a283f5f0d364ceef0)); not yet named in the map
|
||||
- INT-07 One v6 object agrees in node, pool, CPU verifier and each supported GPU host across activation.: INT-07 (R1 integration gate): the gate's harness is the owner lane's (CI steward with the hash lane (a690540514aa453d7) and the worker lane (a9e87343f008e0edd)); not yet named in the map
|
||||
- INT-08 Census, production acceptance, schedule counters and live-dataset tests use the identical frozen contract.: INT-08 (R1 integration gate): the gate's harness is the owner lane's (CI steward with the hash lane (a690540514aa453d7) and the worker lane (a9e87343f008e0edd)); not yet named in the map
|
||||
- INT-09 Eight- and twelve-GiB epoch transitions recover deliberately without relying on repeated OOM/watchdog cycles.: INT-09 (R1 integration gate): the gate's harness is the owner lane's (app lane); not yet named in the map
|
||||
- INT-10 Metal/CUDA/OpenCL exact dataset geometry agrees; unsupported packs reject before launch.: INT-10 (R1 integration gate): the gate's harness is the owner lane's (CI steward with the hash lane (a690540514aa453d7) and the worker lane (a9e87343f008e0edd)); not yet named in the map
|
||||
|
|
@ -459,9 +514,6 @@ Rule: a case maps to a cell only where the cell's tests visibly answer it; cover
|
|||
- R2-F02-R01 Release build cannot activate test bypass.: R2-F02 (P0, the external review): the regression's harness is the owner lane's (proving lane, CI steward); not yet named in the map
|
||||
- R2-F02-R02 Missing oracle or pinned keys prevents service readiness after enforcement activation.: R2-F02 (P0, the external review): the regression's harness is the owner lane's (proving lane, CI steward); not yet named in the map
|
||||
- R2-F02-R03 Missing proof bytes retry without incorrectly marking a valid block permanently invalid.: R2-F02 (P0, the external review): the regression's harness is the owner lane's (proving lane, CI steward); not yet named in the map
|
||||
- R2-F03-R01 Clean build from one manifest, including the pool and workers, with no unpublished vendor tree.: R2-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
|
||||
- R2-F03-R02 Same job context produces identical accepted work in node, CPU reference, CUDA, Metal, OpenCL and pool.: R2-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
|
||||
- R2-F03-R03 Cross every scheduled transition with old/new client behavior documented and identical rule identities.: R2-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
|
||||
- R2-F04-R01 Native multi-node 40/40/20 partition, equivocation and dust-valid mining on both sides past the window.: R2-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
|
||||
- R2-F04-R02 Proof that the chosen recovery guarantee matches the public finality claim; two valid contradictory certificates are a hard failure for strong finality.: R2-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
|
||||
- R2-F04-R03 Pause-only resume with historical backfill, missing historical data and all old keys returning.: R2-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
|
||||
|
|
@ -499,4 +551,4 @@ Rule: a case maps to a cell only where the cell's tests visibly answer it; cover
|
|||
|
||||
## Count
|
||||
|
||||
171 automated cases: 69 mapped to a cell, 152 NOT RUN with a reason.
|
||||
171 automated cases: 82 mapped to a cell, 146 NOT RUN with a reason.
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -53,7 +53,7 @@ TOKEN_FILE="$HOME/.config/igneum/dl-token"
|
|||
SIGNER="$ROOT/app/igneum-app/target/release/igneum-ota-sign"
|
||||
PRODUCT="app"
|
||||
|
||||
VERSION="" MAC="" WIN="" NOTES="" ACTIVATION="" DEADLINE="" MIN_SUPPORTED="" CHANNEL="devnet" BASE="" DEST="" DEPLOY=0 VERIFY_ONLY=0 TRIES=12
|
||||
RELEASE_SHA="" CANARY_GUARD_SELF_TEST=0 VERSION="" MAC="" WIN="" NOTES="" ACTIVATION="" DEADLINE="" MIN_SUPPORTED="" CHANNEL="devnet" BASE="" DEST="" DEPLOY=0 VERIFY_ONLY=0 TRIES=12
|
||||
NODE_BIN="" NET_DIGEST="" MOVE_CLOCK="" DIGEST_GUARD_SELF_TEST=0
|
||||
OVERRIDE="" TUNING_FILE="" NO_TUNING=0 PUBLIC=0
|
||||
while [ $# -gt 0 ]; do
|
||||
|
|
@ -83,6 +83,8 @@ while [ $# -gt 0 ]; do
|
|||
--network-digest) NET_DIGEST="$2"; shift 2 ;; # the network's CURRENT digest (a 64-hex, or "log:<file or journal:unit>[@user@host]" read by tools/digest-read.sh on the hub)
|
||||
--move-clock) MOVE_CLOCK="$2"; shift 2 ;; # "HH:MM UTC, <reason>": the move's clock, when the entry's digest differs by design; logged in the notes
|
||||
--self-test-digest-guard) DIGEST_GUARD_SELF_TEST=1; shift ;;
|
||||
--release-sha) RELEASE_SHA="$2"; shift 2 ;; # rule 33 (8 Oct 2026): the release tip's commit; its fresh-install canary record must read PASS (tools/ci/canary-check.sh)
|
||||
--self-test-canary-guard) CANARY_GUARD_SELF_TEST=1; shift ;;
|
||||
*) echo "unknown argument: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
|
@ -109,6 +111,47 @@ if [ "$DIGEST_GUARD_SELF_TEST" = 1 ]; then
|
|||
echo "digest guard self-test: a differing digest is refused without a move clock and accepted with one; an equal digest passes; the digest reader parses"
|
||||
exit 0
|
||||
fi
|
||||
# Rule 33 (the founder, 8 October 2026, 21:3x UK, "no more lost time"): a release entry may not publish without a fresh-install
|
||||
# canary record for its sha (install from the artefact on a non-AVX-512 box with an empty datadir, genesis to tip, five minutes
|
||||
# mining with zero refusals and an accepted block, one shard claimed, proved and paid or queued, a bounded quit, every line read
|
||||
# back): tools/ci/canary/<sha>.json read by tools/ci/canary-check.sh. A write to a real downloads folder needs --release-sha and a
|
||||
# PASS record; a loopback --base-url test write and --verify-only do not publish and are not gated.
|
||||
canary_guard() { # <release sha> [kind ...] -> 0 pass (the lines printed), 1 refused; a kind (mac, windows) asks for that entry's own artefact block
|
||||
local sha="$1"; shift; local k
|
||||
[ -n "$sha" ] || { echo "canary guard: REFUSED: a release entry needs --release-sha <release tip commit> with a fresh-install canary record (rule 33; tools/ci/canary-check.sh --form)"; return 1; }
|
||||
if [ $# -eq 0 ]; then bash "$TOOLS/ci/canary-check.sh" "$sha" || { echo "canary guard: REFUSED: the entry's sha ${sha:0:12} has no PASS fresh-install canary record (rule 33)"; return 1; }; return 0; fi
|
||||
for k in "$@"; do bash "$TOOLS/ci/canary-check.sh" "$sha" --artefact "$k" || { echo "canary guard: REFUSED: the $k entry's sha ${sha:0:12} has no PASS fresh-install canary record for its own artefact (rule 33)"; return 1; }; done
|
||||
}
|
||||
if [ "$CANARY_GUARD_SELF_TEST" = 1 ]; then
|
||||
bash "$TOOLS/ci/canary-check.sh" --self-test >/dev/null || exit 1
|
||||
d=$(mktemp -d); trap 'rm -rf "$d"' EXIT; S=0123456789abcdef0123456789abcdef01234567
|
||||
canary_guard "" >/dev/null 2>&1 && { echo "canary guard self-test: FAIL: an entry with no --release-sha was accepted"; exit 1; }
|
||||
CANARY_DIR="$d" canary_guard "$S" >/dev/null 2>&1 && { echo "canary guard self-test: FAIL: a sha with no record was accepted"; exit 1; }
|
||||
bash "$TOOLS/ci/canary-check.sh" --form | python3 -c "
|
||||
import json,sys; r=json.load(sys.stdin); r['sha']='$S'; r['artefact']['sha256']='ab'*32; r['box']={'host':'build-4','isa_line':'kit-isa: clean'}
|
||||
r['sync'].update(tip_height=10, seconds=1); r['quit']['seconds']=1; r['recorded_at']='t'; r['recorded_by']='self-test'; json.dump(r, open('$d/$S.json','w'))"
|
||||
CANARY_DIR="$d" canary_guard "$S" >/dev/null 2>&1 || { echo "canary guard self-test: FAIL: a sha with a PASS record was refused"; exit 1; }
|
||||
python3 -c "import json; p='$d/$S.json'; r=json.load(open(p)); r['mining']['refusals']=1; json.dump(r, open(p,'w'))"
|
||||
CANARY_GUARD_FAILED=0; CANARY_DIR="$d" canary_guard "$S" >/dev/null 2>&1 && { echo "canary guard self-test: FAIL: a record with a refusal was accepted"; exit 1; }
|
||||
python3 -c "
|
||||
import json,copy; p='$d/$S.json'; r=json.load(open(p)); blk={k:r[k] for k in ('artefact','box','datadir','sync','mining','shard','quit','lines_read_back')}; blk['mining']['refusals']=0; arts=[]
|
||||
for kind in ('fleet','windows','mac'):
|
||||
b=copy.deepcopy(blk); b['kind']=kind; arts.append(b)
|
||||
arts[1]['mining']['refusals']=3
|
||||
json.dump({'sha':r['sha'],'artefacts':arts,'verdict':'PASS','recorded_at':'t','recorded_by':'self-test'}, open(p,'w'))"
|
||||
CANARY_DIR="$d" canary_guard "$S" mac >/dev/null 2>&1 || { echo "canary guard self-test: FAIL: the mac entry was refused though its own block passes"; exit 1; }
|
||||
CANARY_DIR="$d" canary_guard "$S" windows >/dev/null 2>&1 && { echo "canary guard self-test: FAIL: the windows entry passed on a failing windows block"; exit 1; }
|
||||
CANARY_DIR="$d" canary_guard "$S" mac windows >/dev/null 2>&1 && { echo "canary guard self-test: FAIL: a mac plus windows publish passed with the windows block failing"; exit 1; }
|
||||
echo "canary guard self-test: no sha, no record and a failing record are refused; a PASS record passes; an entry publishes on its own artefact's block (mac passes, windows refused on its own failing block); the record check's own self-test passes"
|
||||
exit 0
|
||||
fi
|
||||
case "${BASE:-}" in http://127.0.0.1*|http://localhost*|http://\[::1\]*) CANARY_GATED=0 ;; *) CANARY_GATED=1 ;; esac
|
||||
if [ "$VERIFY_ONLY" != 1 ] && [ "$CANARY_GATED" = 1 ] && [ -z "${IGNEUM_CANARY_SELF_TEST_SKIP:-}" ]; then
|
||||
KINDS=""; [ -n "$MAC" ] && KINDS="$KINDS mac"; [ -n "$WIN" ] && KINDS="$KINDS windows"
|
||||
# shellcheck disable=SC2086
|
||||
canary_guard "$RELEASE_SHA" $KINDS || exit 1
|
||||
NOTES="${NOTES:+$NOTES; }release: $RELEASE_SHA (fresh-install canary PASS)"
|
||||
fi
|
||||
if [ -n "$NODE_BIN" ] || [ -n "$NET_DIGEST" ]; then
|
||||
[ -n "$NODE_BIN" ] && [ -n "$NET_DIGEST" ] || { echo "the digest guard needs both --node-bin and --network-digest" >&2; exit 2; }
|
||||
ENTRY_DIGEST=$(bash "$TOOLS/digest-read.sh" binary "$NODE_BIN") || exit 1
|
||||
|
|
|
|||
|
|
@ -32,10 +32,11 @@ KEY="$CFG/ota-signing-key"; PUB_KEY="$CFG/ota-signing-key.pub"
|
|||
SIGNER="${IGNEUM_OTA_SIGN:-$ROOT/app/igneum-app/target/release/igneum-ota-sign}" # a built signer elsewhere (another worktree)
|
||||
HOST="https://dl.igneum.network"
|
||||
|
||||
DO_APP=0 DO_WALLET=0 HIVE="" DO_ALIASES=0 DRY=0 DEPLOY=0 VERIFY=0 PRUNE=1 DEST="" BASE="" TRIES=12
|
||||
RELEASE_SHA="" DO_APP=0 DO_WALLET=0 HIVE="" DO_ALIASES=0 DRY=0 DEPLOY=0 VERIFY=0 PRUNE=1 DEST="" BASE="" TRIES=12
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--app) DO_APP=1; shift ;;
|
||||
--release-sha) RELEASE_SHA="$2"; shift 2 ;; # rule 33: the release tip's commit (read from the app manifest's notes when not given); its canary record must read PASS
|
||||
--wallet) DO_WALLET=1; shift ;;
|
||||
--hive) HIVE="$2"; shift 2 ;;
|
||||
--aliases) DO_ALIASES=1; shift ;;
|
||||
|
|
@ -107,6 +108,26 @@ PY
|
|||
log " $name: $(python3 -c 'import json,sys; m=json.load(open(sys.argv[1])); print(m.get("version"), "+".join(m.get("platforms",{})))' "$out") written, signed, verified; URLs under $BASE_PUB"
|
||||
}
|
||||
|
||||
# Rule 33 (8 October 2026): nothing goes to dl/public/ for a release whose sha has no PASS fresh-install canary record. The app
|
||||
# manifest carries "release: <sha>" in its notes (publish-manifest.sh --release-sha writes it); --release-sha overrides or supplies it
|
||||
# for the HiveOS package. tools/ci/canary-check.sh reads tools/ci/canary/<sha>.json.
|
||||
canary_gate() { # <sha> <what> [kind ...]: each kind (mac, windows, hive) must have its own PASS artefact block; no kind = the whole record
|
||||
local sha="$1" what="$2"; shift 2; local k; local chk; chk="$(cd "$(dirname "$0")/../.." && pwd)/tools/ci/canary-check.sh"
|
||||
[ -n "$sha" ] || { echo "canary guard: REFUSED: $what names no release sha (publish the token entry with publish-manifest.sh --release-sha, or pass --release-sha here); rule 33" >&2; return 1; }
|
||||
if [ $# -eq 0 ]; then bash "$chk" "$sha" | scrub || { echo "canary guard: REFUSED: ${sha:0:12} has no PASS fresh-install canary record; rule 33" >&2; return 1; }; return 0; fi
|
||||
for k in "$@"; do bash "$chk" "$sha" --artefact "$k" | scrub || { echo "canary guard: REFUSED: $what: the $k entry's sha ${sha:0:12} has no PASS fresh-install canary record for its own artefact; rule 33" >&2; return 1; }; done
|
||||
}
|
||||
if [ "$DO_APP" = 1 ] && [ -z "${IGNEUM_CANARY_SELF_TEST_SKIP:-}" ]; then
|
||||
sha="$RELEASE_SHA"
|
||||
[ -n "$sha" ] || sha=$(python3 -c 'import json,re,sys; m=json.load(open(sys.argv[1])); x=re.search(r"release: ([0-9a-f]{8,40})", str(m.get("notes",""))); print(x.group(1) if x else "")' "$SRC/igneum-app-latest.json" 2>/dev/null || true)
|
||||
kinds=$(python3 -c 'import json,sys; m=json.load(open(sys.argv[1])); ks=[]
|
||||
for p in m.get("platforms",{}):
|
||||
ks.append("mac" if "mac" in p.lower() or "darwin" in p.lower() else "windows" if "win" in p.lower() else p)
|
||||
print(" ".join(ks))' "$SRC/igneum-app-latest.json" 2>/dev/null || true)
|
||||
# shellcheck disable=SC2086
|
||||
canary_gate "$sha" "the app manifest" $kinds || exit 1
|
||||
fi
|
||||
if [ -n "$HIVE" ] && [ -z "${IGNEUM_CANARY_SELF_TEST_SKIP:-}" ]; then canary_gate "$RELEASE_SHA" "the HiveOS package" hive || exit 1; fi
|
||||
if [ "$DO_APP" = 1 ]; then log "app manifest -> dl/public/"; publish_manifest igneum-app-latest.json; fi
|
||||
if [ "$DO_WALLET" = 1 ]; then log "wallet manifest -> dl/public/"; publish_manifest igneum-wallet-latest.json; fi
|
||||
if [ -n "$HIVE" ]; then
|
||||
|
|
|
|||
|
|
@ -16,8 +16,10 @@
|
|||
| the kit ISA check (`kit-isa-check.sh`; in the gate as a self-test, in `merge-to-master.sh` over any executable a landing adds under packaging/kits or bin, and in the shipper's cut gate over the kit) | Any binary whose disassembly carries a zmm register, an EVEX opmask or an EVEX-only mnemonic (AVX-512): a fleet binary comes only from the cross-build kit at the x86-64-v3 baseline, never from a box's native gate build (79 fleet hosts died on one, 8 Oct 2026) | 8 Oct 2026 |
|
||||
| kill-by-name rule 5 and the no-kill shim (`kill-by-name-check.sh`, `no-kill-shim/{pkill,killall}` exit 97 when first in PATH) | A `pgrep -f`/`pkill -f` pattern that is a bare path, a log name or an unanchored word; only `^`-anchored command patterns, the bracket form, a variable, -x or -F pass (fifteen Mac processes died to a grep, 8 Oct 2026) | 8 Oct 2026 |
|
||||
| a "cut" batch needs its read-back (`test-record.mjs`) | A batch declaring `cut` without the binary's build-N:/srv path, its commit string read back equal to the manifest sha, and the kit ISA check's clean line; a sha is cut only when its binary exists on build-1 with its commit string read back | 8 Oct 2026 |
|
||||
| rule 33: a release entry publishes only with a PASS fresh-install canary record for its sha (`canary-check.sh`; `tools/ci/canary/<sha>.json`, the form from `--form`; refused without by `packaging/ota/publish-manifest.sh --release-sha`, `publish-public.sh` and deploy-win.sh; the canary cell maps INT-07) | A release entry published before its sha had run a fresh install on a non-AVX-512 box with an empty datadir, synced, mined five minutes with zero refusals, claimed, proved and paid or queued one shard and quit inside a bound, every line read back (the founder's "no more lost time", 8 October 2026) |
|
||||
| the INT suite is generated from the master edition's integration gates (`int-suite.mjs`; the owner per the coordinator's crosswalk) and INT-17 is a rule of the writer: a cell declaring a missing oracle, pinned keys or mandatory real-proof fixture writes BLOCKED, never PASS | A registry whose INT suite drifts from traceability.json; a batch cell with `prereqs` where any is not "present" written as anything but BLOCKED | 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 |
|
||||
| F03 (Review B): one release manifest (`packaging/release-manifest.json` on a release branch) and every component built from it (`release-manifest-check.sh`, `build-from-manifest.sh`) | A tree whose own pins disagree with the manifest: the Windows node-source pin, the proving manifest's elf and vk sha256s and the files' hashes, the node fork's freeze fingerprint, the pool's vendored node checkout, a redefined EpochSeeds in the pool (the shadow_reps seam closes by a build against the pinned node); the build script puts the fork at the manifest's node sha and checks kaspad with igneum-pow (rule 19), the miner, the pool, the app and the prove host on a box | 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 |
|
||||
|
|
|
|||
20
tools/ci/batches/adversary-20261008-placed-8lane.json
Normal file
20
tools/ci/batches/adversary-20261008-placed-8lane.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"run_id": "adversary-20261008-placed-8lane",
|
||||
"manifest_sha": "3a8874fef",
|
||||
"evidence_dir": "docs/analysis/class-v6/multi-family-adversary.md",
|
||||
"cells": [
|
||||
{
|
||||
"cell": "adversary:mf-placed",
|
||||
"status": "RUNNING",
|
||||
"evidence": "docs/analysis/class-v6/multi-family-adversary.md",
|
||||
"method": "model"
|
||||
},
|
||||
{
|
||||
"cell": "adversary:d2b",
|
||||
"status": "RUNNING",
|
||||
"evidence": "docs/analysis/class-v6/multi-family-adversary.md",
|
||||
"method": "model",
|
||||
"note": "evidence cited at the document (section 13, D2(b) restated on the placed rows); the mf flow's results file tools/chip-model/mf/results/d2b-n5.md stays on the branch and is cited after it lands (the recorder cites only files in the tree)"
|
||||
}
|
||||
]
|
||||
}
|
||||
28
tools/ci/batches/canary-20261008-01.json
Normal file
28
tools/ci/batches/canary-20261008-01.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"run_id": "canary-20261008-01",
|
||||
"manifest_sha": "c30ab32c",
|
||||
"method": "team-reported",
|
||||
"evidence_dir": "tools/ci/canary (no record yet)",
|
||||
"boxes": [],
|
||||
"release_identity": {
|
||||
"commit": "c30ab32c (release-2.0.1 final tip; 2.0.2 open at c608b341)",
|
||||
"lockfile": "",
|
||||
"binary": "the shipped 2.0.1 entries on aa0e0f45's binaries",
|
||||
"network_object": "igneum-devnet-4, chain id 4465",
|
||||
"activation": "",
|
||||
"profile_hashes": ""
|
||||
},
|
||||
"claim_impact": "INT-07's clean-install half reads BLOCKED until a release tip carries a PASS fresh-install canary record; no published entry may move before one does (rule 33)",
|
||||
"note": "Rule 33 recorded at 21:4x UK: the canary cell exists, its check and the publish-path guards are in the tree; no 2.0.x sha has a record yet, so the case is BLOCKED (prerequisite: the shipper's 2.0.2 canary tonight writes tools/ci/canary/<sha>.json).",
|
||||
"cells": [
|
||||
{
|
||||
"cell": "canary:fresh-install",
|
||||
"cases": [
|
||||
"INT-07"
|
||||
],
|
||||
"status": "BLOCKED",
|
||||
"evidence": "tools/ci/canary-check.sh; packaging/ota/publish-manifest.sh; packaging/ota/publish-public.sh",
|
||||
"note": "no fresh-install canary record exists for any 2.0.x release tip; the 2.0.2 canary is the shipper's tonight"
|
||||
}
|
||||
]
|
||||
}
|
||||
39
tools/ci/batches/f03-manifest-20261008-01.json
Normal file
39
tools/ci/batches/f03-manifest-20261008-01.json
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"run_id": "f03-manifest-20261008-01",
|
||||
"manifest_sha": "c30ab32c",
|
||||
"method": "static",
|
||||
"evidence_dir": "build-1:/srv/artefacts/tas/f03-manifest-20261008-01 (build-from-manifest-c30ab32c-build4.log, sha256 6040ded8e99f0871\u2026); the build tree build-4:/srv/builds/igneum-wt-f03-201 at c30ab32c",
|
||||
"boxes": [
|
||||
"build-4"
|
||||
],
|
||||
"release_identity": {
|
||||
"commit": "c30ab32c",
|
||||
"lockfile": "the release tree's Cargo.lock files at c30ab32c",
|
||||
"binary": "cargo check only: no binary is claimed by this cell; the fleet binaries are the shipper's kit on aa0e0f45",
|
||||
"network_object": "igneum-devnet-4, chain id 4465",
|
||||
"activation": "none (a build fact)",
|
||||
"profile_hashes": "release-manifest-check: every component's own pin equals packaging/release-manifest.json"
|
||||
},
|
||||
"claim_impact": "F03's first rung: one manifest, one build, no unpublished vendor tree; the same-work rung (R02) and the transition rung (R03) stay NOT RUN until the readers run on the job context",
|
||||
"note": "R2-F03-R01, the clean build from one manifest: every component builds from packaging/release-manifest.json on release-2.0.1's final tip c30ab32c with the node vendored at the manifest's sha 7cfa422a as a real checkout (vendor/igneum-node and vendor/igneum-node-exec; a link ships as nothing); the pool builds only from the release tree (its igneum-pow is the class v5 freeze cbc5bd0a, rule 19; master's a65e4c5a refuses it). Earlier runs on aa0e0f45 were red on the pool (KeyReveal.sig_scheme, the pool-review-b shape not yet in the tree) and on prove-host (the exec vendor missing); both closed by the tree, not by the script. The workers are the kit's cross-build (the shipper's kit-isa line), not this build. The build: tools/ci/build-from-manifest.sh --box 4 on release-2.0.1 c30ab32c, 21:11 to 21:14 UK, kaspad with igneum-pow, igneum-miner, igneum-pool, igneum-app and igneum-prove-host all check green from packaging/release-manifest.json, and release-manifest-check reads every component's own pin equal to the manifest; the log on build-1 (sha256 6040ded8e99f0871...).",
|
||||
"cells": [
|
||||
{
|
||||
"cell": "harness:release-manifest",
|
||||
"cases": [
|
||||
"R2-F03-R01"
|
||||
],
|
||||
"status": "PASS",
|
||||
"evidence": "build-1:/srv/artefacts/tas/f03-manifest-20261008-01/build-from-manifest-c30ab32c-build4.log; build-4:/srv/builds/igneum-wt-f03-201; tools/ci/build-from-manifest.sh; tools/ci/release-manifest-check.sh"
|
||||
},
|
||||
{
|
||||
"cell": "check:freeze",
|
||||
"cases": [
|
||||
"GOV-01"
|
||||
],
|
||||
"status": "NOT RUN",
|
||||
"in_progress": true,
|
||||
"evidence": "docs/plans/igneum-2.0-f0-manifest.md; build-4:/srv/builds/igneum-wt-f03-201/vendor/igneum-node/packaging/pow-freeze.txt; build-4:/srv/builds/igneum-wt-f03-201",
|
||||
"note": "GOV-01 moves with its evidence page: the F0 manifest's cut-tip row gained the final tip c30ab32c and the 21:14 UK build-from-manifest fact; the freeze itself is unchanged (class v5 1c420786, fingerprint cbc5bd0a) and GOV-01 stays NOT RUN in progress until the signing block at 23:30"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -15,5 +15,5 @@
|
|||
"evidence": "docs/analysis/class-v6/mixed-fp32.md; docs/analysis/class-v6/rows/pow-07-fp32-unreachable.md"
|
||||
}
|
||||
],
|
||||
"note": " Evidence narrowed at 21:3x UK from the class-v6 directory to the two experiments' own files (a directory citation bound every file beneath it to these rows and refused every other lane's class-v6 landing); the verdicts are unchanged."
|
||||
"note": " Evidence narrowed at 21:3x UK from the class-v6 directory to the two experiments' own files (a directory citation bound every file beneath it to these rows and refused every other lane's class-v6 landing); the verdicts are unchanged. Replayed at the rule 33 landing (21:4x UK): the narrowed evidence reaches master's rows only through a replay, and the merge replayed added batches alone until this landing."
|
||||
}
|
||||
|
|
|
|||
14
tools/ci/batches/pool-2.0-20261008-03.json
Normal file
14
tools/ci/batches/pool-2.0-20261008-03.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"run_id": "pool-2.0-20261008-03",
|
||||
"manifest_sha": "986252e73",
|
||||
"method": "native",
|
||||
"evidence_dir": "docs/analysis/pool",
|
||||
"cells": [
|
||||
{
|
||||
"cell": "suite:pool",
|
||||
"status": "RUNNING",
|
||||
"method": "native",
|
||||
"evidence": "docs/analysis/pool/pool-pair-2026-10-08.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
14
tools/ci/batches/ux-01-20261008-win-2.0.0.json
Normal file
14
tools/ci/batches/ux-01-20261008-win-2.0.0.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"run_id": "ux-01-20261008-win-2.0.0",
|
||||
"manifest_sha": "aa354ed5",
|
||||
"evidence_dir": "docs/plans/evidence/UX-01-20261008.md",
|
||||
"cells": [
|
||||
{
|
||||
"cell": "pc:install-update",
|
||||
"status": "RUNNING",
|
||||
"evidence": "docs/plans/evidence/UX-01-20261008.md",
|
||||
"method": "team-reported"
|
||||
}
|
||||
],
|
||||
"method": "team-reported"
|
||||
}
|
||||
33
tools/ci/build-from-manifest.sh
Executable file
33
tools/ci/build-from-manifest.sh
Executable file
|
|
@ -0,0 +1,33 @@
|
|||
#!/usr/bin/env bash
|
||||
# F03 (Review B): every component is built from the one release manifest. Reads packaging/release-manifest.json of this tree,
|
||||
# puts the node fork at the manifest's node sha under vendor/igneum-node (the pool's path dependency, so the EpochSeeds seam closes
|
||||
# by a build against the pinned node), then on a box at gate priority (tools/build-remote.sh): cargo check of kaspad with the
|
||||
# igneum-pow feature (rule 19 proves the generator's fingerprint at build time), igneum-miner, the pool crate (igneum-pool), the app
|
||||
# crate (igneum-app) and the prove host; then tools/ci/release-manifest-check.sh over the tree and the fork. One red = exit 1.
|
||||
# tools/ci/build-from-manifest.sh [--box N] [--dry] from the release branch's worktree; --dry prints the plan
|
||||
set -euo pipefail
|
||||
ROOT=$(git rev-parse --show-toplevel); cd "$ROOT"; BOX=""; DRY=0
|
||||
while [ $# -gt 0 ]; do case "$1" in --box) BOX="$2"; shift 2 ;; --dry) DRY=1; shift ;; *) echo "unknown $1" >&2; exit 2 ;; esac; done
|
||||
M=packaging/release-manifest.json; [ -f "$M" ] || { echo "build-from-manifest: no $M on this tree" >&2; exit 2; }
|
||||
NODE=$(python3 -c "import json;print(json.load(open('$M'))['node']['sha'])"); FP=$(python3 -c "import json;print(json.load(open('$M'))['generator']['fingerprint'])")
|
||||
echo "build-from-manifest: node $NODE, generator fingerprint ${FP:0:16}, miner app $(git rev-parse --short HEAD)"
|
||||
FORK=vendor/igneum-node; MIRROR="${IGNEUM_NODE_MIRROR:-build@188.40.146.49:/srv/igneum-node.git}"
|
||||
if [ "$DRY" = 1 ]; then echo "plan: $FORK at $NODE from $MIRROR; cargo check kaspad(+igneum-pow), igneum-miner, igneum-pool, igneum-app, igneum-prove-host on box ${BOX:-auto} at gate priority; then release-manifest-check.sh $FORK"; exit 0; fi
|
||||
if [ -d "$FORK/.git" ] || [ -f "$FORK/.git" ]; then git -C "$FORK" fetch -q "$MIRROR" "$NODE" 2>/dev/null || git -C "$FORK" fetch -q "$MIRROR" release-2.0.0-node; git -C "$FORK" checkout -q --detach "$NODE"
|
||||
else mkdir -p vendor && git clone -q "$MIRROR" "$FORK" && git -C "$FORK" checkout -q --detach "$NODE"; fi
|
||||
# the proving workspace names the same fork vendor/igneum-node-exec (proving/igneum-prove/Cargo.toml): a second checkout at the same
|
||||
# sha, never a link (build-remote ships directories as overlays to the box; a link ships as nothing)
|
||||
if [ -L vendor/igneum-node-exec ]; then rm -f vendor/igneum-node-exec; fi
|
||||
if [ -d vendor/igneum-node-exec/.git ] || [ -f vendor/igneum-node-exec/.git ]; then git -C vendor/igneum-node-exec fetch -q "$MIRROR" "$NODE" 2>/dev/null || true; git -C vendor/igneum-node-exec checkout -q --detach "$NODE"
|
||||
else git -C "$FORK" worktree add -q --detach "$ROOT/vendor/igneum-node-exec" "$NODE" 2>/dev/null || git clone -q "$MIRROR" vendor/igneum-node-exec && git -C vendor/igneum-node-exec checkout -q --detach "$NODE"; fi
|
||||
echo "build-from-manifest: $FORK at $(git -C "$FORK" rev-parse --short HEAD); vendor/igneum-node-exec at $(git -C vendor/igneum-node-exec rev-parse --short HEAD)"
|
||||
run() { local name="$1" dir="$2"; shift 2; echo "build-from-manifest: $name"; ( cd "$dir" && IGNEUM_AGENT=f03 bash "$ROOT/tools/build-remote.sh" ${BOX:+--box "$BOX"} --no-fetch --priority gate -- "$@" ) > "/tmp/f03-$name.log" 2>&1 || { echo "build-from-manifest: RED: $name (see /tmp/f03-$name.log)" >&2; grep -m3 -E '^error|RED|panicked' "/tmp/f03-$name.log" | cut -c1-160 >&2; return 1; }; echo "build-from-manifest: $name ok"; }
|
||||
rc=0
|
||||
run kaspad "$FORK" check --release -p kaspad --features kaspad/igneum-pow || rc=1
|
||||
run igneum-miner "$FORK" check --release -p igneum-miner || rc=1
|
||||
run igneum-pool pool check --release || rc=1
|
||||
run igneum-app app/igneum-app check --release || rc=1
|
||||
run prove-host proving/igneum-prove check --release -p igneum-prove-host || rc=1
|
||||
bash tools/ci/release-manifest-check.sh "$FORK" || rc=1
|
||||
[ "$rc" = 0 ] && echo "build-from-manifest: every component builds from the manifest and every pin agrees"
|
||||
exit $rc
|
||||
174
tools/ci/canary-check.sh
Executable file
174
tools/ci/canary-check.sh
Executable file
|
|
@ -0,0 +1,174 @@
|
|||
#!/usr/bin/env bash
|
||||
# Rule 33 (the founder's "no more lost time", 8 October 2026, 21:3x UK): a release entry may not publish without a
|
||||
# fresh-install canary record for its sha. The record is one JSON file in the tree, tools/ci/canary/<sha>.json (the
|
||||
# release tip's commit, 8 to 40 hex), written by the lane that ran the canary, with the eight lines the founder named,
|
||||
# each read back with its evidence path on a box (build-N:/srv/... or /srv/...). This check reads the record and says
|
||||
# PASS or names the first line that is missing or failing; the publish path (packaging/ota/publish-manifest.sh,
|
||||
# packaging/ota/publish-public.sh, deploy-win.sh) refuses an entry whose sha has no PASS record.
|
||||
#
|
||||
# tools/ci/canary-check.sh <sha> [--artefact <kind>] exit 0: PASS (the line printed); 1: no record or a line fails (named); 2: bad args
|
||||
# a record holds one artefact block at the top level, or an "artefacts" list (one block per entry's artefact:
|
||||
# kind fleet | windows | mac | hive, each from its own non-AVX-512 box with its own eight lines); --artefact asks
|
||||
# for that kind's block, so a Mac entry publishes on the Mac canary and the Windows entry waits for its own
|
||||
# tools/ci/canary-check.sh --form prints the record form (every field, with what it must hold)
|
||||
# tools/ci/canary-check.sh --self-test
|
||||
#
|
||||
# The record (tools/ci/canary/<sha>.json):
|
||||
# sha the release tip's commit (the file name's sha, full or 8+)
|
||||
# artefact {url or path, sha256}: what was installed, the published artefact itself, not a box's native build
|
||||
# box {host, isa_line}: a non-AVX-512 box; isa_line is the kit-isa clean line read on it (tools/ci/kit-isa-check.sh)
|
||||
# or the host's cpu flags line showing no avx512
|
||||
# datadir {path, empty_at_start: true, read_back}: the empty datadir before the install
|
||||
# sync {genesis_height: 0, tip_height, seconds, read_back}: genesis to the network's tip
|
||||
# mining {minutes >= 5, refusals: 0, accepted_blocks >= 1, read_back}
|
||||
# shard {claimed: true, proved: true, paid_or_queued: "paid" | "queued", read_back}
|
||||
# quit {bounded: true, seconds, read_back}: the process ends on its own quit inside the bound
|
||||
# lines_read_back a list of the eight line names, each with an evidence path on a box
|
||||
# verdict "PASS" (anything else is not a canary record for publishing)
|
||||
# recorded_at, recorded_by UTC stamp, the lane
|
||||
set -euo pipefail
|
||||
HERE="$(cd "$(dirname "$0")" && pwd -P)"; ME="$HERE/$(basename "$0")"; ROOT="${CANARY_ROOT:-$(cd "$HERE/../.." && pwd -P)}"
|
||||
DIR="${CANARY_DIR:-$ROOT/tools/ci/canary}"
|
||||
|
||||
form() {
|
||||
cat <<'EOF'
|
||||
{
|
||||
"sha": "<release tip commit, full>",
|
||||
"artefact": {"url": "https://dl.igneum.network/public/<file>", "sha256": "<64 hex>"},
|
||||
"box": {"host": "build-N or <name>", "isa_line": "kit-isa: clean ... | cpu flags: no avx512"},
|
||||
"datadir": {"path": "/srv/canary/<sha>/data", "empty_at_start": true, "read_back": "build-N:/srv/canary/<sha>/01-datadir.txt"},
|
||||
"sync": {"genesis_height": 0, "tip_height": 0, "seconds": 0, "read_back": "build-N:/srv/canary/<sha>/02-sync.txt"},
|
||||
"mining": {"minutes": 5, "refusals": 0, "accepted_blocks": 1, "read_back": "build-N:/srv/canary/<sha>/03-mining.txt"},
|
||||
"shard": {"claimed": true, "proved": true, "paid_or_queued": "paid", "read_back": "build-N:/srv/canary/<sha>/04-shard.txt"},
|
||||
"quit": {"bounded": true, "seconds": 0, "read_back": "build-N:/srv/canary/<sha>/05-quit.txt"},
|
||||
"lines_read_back": [
|
||||
{"line": "install", "evidence": "build-N:/srv/canary/<sha>/00-install.txt"},
|
||||
{"line": "box", "evidence": "build-N:/srv/canary/<sha>/00-box.txt"},
|
||||
{"line": "datadir", "evidence": "build-N:/srv/canary/<sha>/01-datadir.txt"},
|
||||
{"line": "sync", "evidence": "build-N:/srv/canary/<sha>/02-sync.txt"},
|
||||
{"line": "mining", "evidence": "build-N:/srv/canary/<sha>/03-mining.txt"},
|
||||
{"line": "shard", "evidence": "build-N:/srv/canary/<sha>/04-shard.txt"},
|
||||
{"line": "quit", "evidence": "build-N:/srv/canary/<sha>/05-quit.txt"},
|
||||
{"line": "version", "evidence": "build-N:/srv/canary/<sha>/00-version.txt"}
|
||||
],
|
||||
"verdict": "PASS",
|
||||
"recorded_at": "<UTC>",
|
||||
"recorded_by": "<lane>"
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
check() { # <sha> [kind] -> prints the verdict line; 0 pass, 1 fail
|
||||
local sha="$1" kind="${2:-}" f
|
||||
case "$sha" in *[!0-9a-fA-F]*|"") echo "canary: REFUSED: '$sha' is not a commit sha"; return 1 ;; esac
|
||||
[ "${#sha}" -ge 8 ] || { echo "canary: REFUSED: the sha must be 8 hex or more"; return 1; }
|
||||
f=""; for c in "$DIR/$sha.json" "$DIR"/"${sha:0:8}"*.json; do [ -f "$c" ] && { f="$c"; break; }; done
|
||||
[ -n "$f" ] || { echo "canary: REFUSED: no fresh-install canary record for ${sha:0:12} (rule 33: tools/ci/canary/<sha>.json, the eight lines read back; tools/ci/canary-check.sh --form)"; return 1; }
|
||||
python3 - "$f" "$sha" "$kind" <<'PY'
|
||||
import json, sys
|
||||
f, sha = sys.argv[1], sys.argv[2].lower()
|
||||
try: r = json.load(open(f))
|
||||
except Exception as e: print(f"canary: REFUSED: {f} is not JSON: {e}"); sys.exit(1)
|
||||
def red(m): print(f"canary: REFUSED: {sha[:12]}: {m} ({f})"); sys.exit(1)
|
||||
def need(obj, key, typ=None):
|
||||
if key not in obj: red(f"the record has no '{key}'")
|
||||
v = obj[key]
|
||||
if typ and not isinstance(v, typ): red(f"'{key}' is not {typ.__name__ if not isinstance(typ, tuple) else '/'.join(t.__name__ for t in typ)}")
|
||||
return v
|
||||
rs = str(need(r, 'sha')).lower()
|
||||
if not (rs.startswith(sha) or sha.startswith(rs)) or len(rs) < 8: red(f"the record's sha {rs[:12]} is not {sha[:12]}")
|
||||
def box_path(p): return isinstance(p, str) and (':/' in p or p.startswith('/srv/'))
|
||||
want_kind = sys.argv[3] if len(sys.argv) > 3 else ''
|
||||
blocks = r['artefacts'] if isinstance(r.get('artefacts'), list) else [r]
|
||||
if not blocks: red('the artefacts list is empty')
|
||||
if want_kind:
|
||||
blocks = [b for b in blocks if str(b.get('kind', b.get('artefact', {}).get('kind', ''))).lower() == want_kind.lower()]
|
||||
if not blocks: red(f"no artefact block of kind '{want_kind}' (the entry's own canary: fleet, windows, mac or hive, each from its own box)")
|
||||
lines_out = []
|
||||
for r_ in blocks:
|
||||
r = r_
|
||||
a = need(r, 'artefact', dict)
|
||||
if not (a.get('url') or a.get('path')): red("artefact names no url or path (the published artefact, not a native build)")
|
||||
if not (isinstance(a.get('sha256'), str) and len(a['sha256']) == 64): red("artefact.sha256 is not 64 hex")
|
||||
b = need(r, 'box', dict)
|
||||
if not b.get('host'): red("box.host is empty")
|
||||
isa = str(b.get('isa_line', '')).lower()
|
||||
if not isa or ('clean' not in isa and 'no avx512' not in isa and 'no avx-512' not in isa): red("box.isa_line does not read a non-AVX-512 box (the kit-isa clean line or a cpu flags line with no avx512)")
|
||||
d = need(r, 'datadir', dict)
|
||||
if d.get('empty_at_start') is not True: red("datadir.empty_at_start is not true")
|
||||
if not box_path(d.get('read_back')): red("datadir.read_back is not a box path")
|
||||
s = need(r, 'sync', dict)
|
||||
if s.get('genesis_height') != 0: red("sync.genesis_height is not 0 (the sync starts at genesis)")
|
||||
if not (isinstance(s.get('tip_height'), int) and s['tip_height'] > 0): red("sync.tip_height is not a positive height")
|
||||
if not box_path(s.get('read_back')): red("sync.read_back is not a box path")
|
||||
m = need(r, 'mining', dict)
|
||||
if not (isinstance(m.get('minutes'), (int, float)) and m['minutes'] >= 5): red("mining.minutes is under 5")
|
||||
if m.get('refusals') != 0: red(f"mining.refusals is {m.get('refusals')!r}, not 0")
|
||||
if not (isinstance(m.get('accepted_blocks'), int) and m['accepted_blocks'] >= 1): red("mining.accepted_blocks is under 1")
|
||||
if not box_path(m.get('read_back')): red("mining.read_back is not a box path")
|
||||
h = need(r, 'shard', dict)
|
||||
if h.get('claimed') is not True or h.get('proved') is not True: red("shard.claimed and shard.proved must both be true")
|
||||
if h.get('paid_or_queued') not in ('paid', 'queued'): red("shard.paid_or_queued must be 'paid' or 'queued'")
|
||||
if not box_path(h.get('read_back')): red("shard.read_back is not a box path")
|
||||
q = need(r, 'quit', dict)
|
||||
if q.get('bounded') is not True: red("quit.bounded is not true")
|
||||
if not (isinstance(q.get('seconds'), (int, float)) and q['seconds'] >= 0): red("quit.seconds is not a number")
|
||||
if not box_path(q.get('read_back')): red("quit.read_back is not a box path")
|
||||
lines = need(r, 'lines_read_back', list)
|
||||
names = {str(x.get('line')) for x in lines if isinstance(x, dict)}
|
||||
want = {'install', 'box', 'datadir', 'sync', 'mining', 'shard', 'quit', 'version'}
|
||||
missing = sorted(want - names)
|
||||
if missing: red(f"lines_read_back lacks {', '.join(missing)}")
|
||||
for x in lines:
|
||||
if isinstance(x, dict) and x.get('line') in want and not box_path(x.get('evidence')): red(f"line {x.get('line')} has no box evidence path")
|
||||
top = json.load(open(f))
|
||||
v = r.get('verdict', top.get('verdict'))
|
||||
if v != 'PASS': red(f"verdict is {v!r}, not PASS")
|
||||
if not (r.get('recorded_at') or top.get('recorded_at')) or not (r.get('recorded_by') or top.get('recorded_by')): red("recorded_at or recorded_by is empty")
|
||||
lines_out.append(f"{r.get('kind', 'artefact')} on {b['host']} ({str(a.get('sha256'))[:12]}…): genesis to {s['tip_height']} in {s.get('seconds')} s, {m['minutes']} min mining, 0 refusals, {m['accepted_blocks']} accepted, one shard {h['paid_or_queued']}, quit in {q['seconds']} s; eight lines read back")
|
||||
print(f"canary: PASS: {sha[:12]} fresh-install canary: " + '; '.join(lines_out) + f"; recorded {blocks[0].get('recorded_at') or json.load(open(f)).get('recorded_at')} by {blocks[0].get('recorded_by') or json.load(open(f)).get('recorded_by')}")
|
||||
PY
|
||||
}
|
||||
|
||||
if [ "${1:-}" = --form ]; then form; exit 0; fi
|
||||
if [ "${1:-}" = --self-test ]; then
|
||||
d=$(mktemp -d); trap 'rm -rf "$d"' EXIT; fails=0; export CANARY_DIR="$d"
|
||||
SHA=0123456789abcdef0123456789abcdef01234567
|
||||
form | python3 -c "
|
||||
import json,sys; r=json.load(sys.stdin); r['sha']='$SHA'; r['artefact']['sha256']='ab'*32; r['box']={'host':'build-4','isa_line':'kit-isa: clean: no AVX-512 encoding in 3 binaries'}
|
||||
r['sync'].update(tip_height=3847, seconds=412); r['mining'].update(minutes=5, refusals=0, accepted_blocks=2); r['quit']['seconds']=3; r['recorded_at']='2026-10-08T21:30:00Z'; r['recorded_by']='shipper'
|
||||
json.dump(r, open('$d/$SHA.json','w'))"
|
||||
bash "$ME" "$SHA" >/dev/null 2>&1 || { echo "self-test failed: a complete PASS record was refused: $(bash "$ME" "$SHA" 2>&1)"; fails=1; }
|
||||
bash "$ME" "${SHA:0:12}" >/dev/null 2>&1 || { echo "self-test failed: the record was not found by its short sha"; fails=1; }
|
||||
out=$(bash "$ME" ffffffffffff 2>&1) && { echo "self-test failed: a sha with no record passed"; fails=1; }; case "$out" in *"no fresh-install canary record"*) ;; *) echo "self-test failed: the missing record was not named: $out"; fails=1 ;; esac
|
||||
mut() { python3 -c "
|
||||
import json,sys; p='$d/$SHA.json'; r=json.load(open(p)); exec(sys.argv[1]); json.dump(r, open(p,'w'))" "$1"; }
|
||||
for case in "r['mining']['refusals']=1|refusals" "r['mining']['minutes']=4|under 5" "r['mining']['accepted_blocks']=0|accepted_blocks" "r['shard']['proved']=False|shard.claimed and shard.proved" "r['shard']['paid_or_queued']='lost'|paid_or_queued" "r['quit']['bounded']=False|quit.bounded" "r['datadir']['empty_at_start']=False|empty_at_start" "r['sync']['genesis_height']=100|genesis" "r['box']['isa_line']='avx512f present'|non-AVX-512" "r['lines_read_back']=r['lines_read_back'][:7]|lacks version" "r['lines_read_back'][2]['evidence']='notes.txt'|no box evidence" "r['verdict']='FAIL'|not PASS" "r['artefact']={'url':'x','sha256':'short'}|sha256"; do
|
||||
cp "$d/$SHA.json" "$d/keep.json"; mut "${case%%|*}"
|
||||
out=$(bash "$ME" "$SHA" 2>&1) && { echo "self-test failed: a record with ${case%%|*} passed"; fails=1; }
|
||||
case "$out" in *"${case##*|}"*) ;; *) echo "self-test failed: the failing line was not named for ${case%%|*}: $out"; fails=1 ;; esac
|
||||
cp "$d/keep.json" "$d/$SHA.json"
|
||||
done
|
||||
# the multi-artefact form: one file per sha, an artefacts list (fleet, windows, mac), each block its own box and eight lines; --artefact picks one
|
||||
python3 -c "
|
||||
import json; p='$d/$SHA.json'; r=json.load(open(p)); blk={k:r[k] for k in ('artefact','box','datadir','sync','mining','shard','quit','lines_read_back')}
|
||||
import copy; arts=[]
|
||||
for kind,host in (('fleet','lp-4090-11'),('windows','pc-2'),('mac','mini')):
|
||||
b=copy.deepcopy(blk); b['kind']=kind; b['box']['host']=host; b['artefact']['url']='https://dl.igneum.network/public/'+kind; arts.append(b)
|
||||
m={'sha':r['sha'],'artefacts':arts,'verdict':'PASS','recorded_at':r['recorded_at'],'recorded_by':'shipper'}; json.dump(m, open(p,'w'))"
|
||||
bash "$ME" "$SHA" >/dev/null 2>&1 || { echo "self-test failed: a three-artefact record was refused: $(bash "$ME" "$SHA" 2>&1)"; fails=1; }
|
||||
bash "$ME" "$SHA" --artefact mac >/dev/null 2>&1 || { echo "self-test failed: the mac block of a three-artefact record was refused"; fails=1; }
|
||||
out=$(bash "$ME" "$SHA" --artefact hive 2>&1) && { echo "self-test failed: a kind with no block passed"; fails=1; }; case "$out" in *"no artefact block of kind 'hive'"*) ;; *) echo "self-test failed: the missing kind was not named: $out"; fails=1 ;; esac
|
||||
python3 -c "import json; p='$d/$SHA.json'; r=json.load(open(p)); r['artefacts'][1]['mining']['refusals']=2; json.dump(r, open(p,'w'))"
|
||||
out=$(bash "$ME" "$SHA" --artefact windows 2>&1) && { echo "self-test failed: a failing windows block passed under --artefact windows"; fails=1; }
|
||||
bash "$ME" "$SHA" --artefact mac >/dev/null 2>&1 || { echo "self-test failed: the mac block was refused because the windows block fails (each entry publishes on its own canary)"; fails=1; }
|
||||
out=$(bash "$ME" "$SHA" 2>&1) && { echo "self-test failed: the whole record passed with one failing block"; fails=1; }
|
||||
echo "not json" > "$d/$SHA.json"; bash "$ME" "$SHA" >/dev/null 2>&1 && { echo "self-test failed: a non-JSON record passed"; fails=1; }
|
||||
out=$(bash "$ME" "not-a-sha" 2>&1) && { echo "self-test failed: a non-sha argument passed"; fails=1; }
|
||||
[ "$fails" = 0 ] && echo "self-test passed: a complete fresh-install canary record is PASS and found by its short sha; no record, a refusal, under five minutes, no accepted block, an unproved or lost shard, an unbounded quit, a non-empty datadir, a sync not from genesis, an AVX-512 box, a missing read-back line, a non-box evidence path, a non-PASS verdict or a bad artefact hash is refused and named; a one-file-per-sha record with an artefacts list (fleet, windows, mac, hive) passes whole or per --artefact kind, and a failing block fails its own kind and the whole, never another kind"
|
||||
exit $fails
|
||||
fi
|
||||
KIND=""; SHA_ARG=""
|
||||
while [ $# -gt 0 ]; do case "$1" in --artefact) KIND="$2"; shift 2 ;; *) SHA_ARG="$1"; shift ;; esac; done
|
||||
[ -n "$SHA_ARG" ] || { echo "usage: $0 <sha> [--artefact <kind>] | --form | --self-test" >&2; exit 2; }
|
||||
check "$SHA_ARG" "$KIND"
|
||||
1
tools/ci/canary/README.md
Normal file
1
tools/ci/canary/README.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Rule 33 fresh-install canary records, one per release tip sha (tools/ci/canary-check.sh --form; the publish path refuses an entry without a PASS record here)
|
||||
|
|
@ -80,7 +80,9 @@ the registry's evidence rules: a PASS names evidence that exists, a touched evid
|
|||
the kit ISA check: a distribution kit's binaries carry no AVX-512 encoding (the x86-64-v3 baseline; self-test with a fake objdump)
|
||||
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)
|
||||
rule 33: a release entry publishes only with a PASS fresh-install canary record for its sha (the record check and the publish guard, self-tests)
|
||||
the REV suite is generated from Review B's findings and dispatch and matches them (self-test, then the tree)
|
||||
F03 (Review B): every component's own pin equals packaging/release-manifest.json where a release branch carries one (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)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export function suite(tr) {
|
|||
return { code: 'INT', title: 'INT: the master edition\'s integration gates (R1, the full-system review)', source: 'docs/plans/igneum-2.0-master/traceability.json integration_gates', gate: 'Integration gates closed', owner: 'the owner lanes per the coordinator\'s crosswalk', fixtures: ['F0', 'F5'], summary: `${tests.length} integration gates; each reads NOT RUN until its owner lane records a run`, tests };
|
||||
}
|
||||
export function merge(reg, s) { 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', 'in_progress_since', 'deferral_note', 'approvals']) if (k in o) t[k] = o[k]; }
|
||||
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', 'evidence_records', 'blocked_on', 'method_recorded', 'in_progress_since', 'deferral_note', 'approvals']) if (k in o) t[k] = o[k]; }
|
||||
if (old?.notes) s.notes = old.notes; reg.suites = [...(reg.suites || []).filter((x) => x.code !== s.code), s]; return reg; }
|
||||
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);
|
||||
function mapReasons(map, s) { const mapped = new Set(Object.values(map.cells || {}).flatMap((c) => c.cases || [])); map.not_run = map.not_run || {}; let n = 0;
|
||||
|
|
@ -33,8 +33,8 @@ if (args.includes('--self-test')) {
|
|||
let fails = 0; const tr = { integration_gates: [{ id: 'INT-01', requirement: 'r one', status: 'PROPOSED / NOT RUN', source: 'R1' }, { id: 'INT-05', requirement: 'r five', status: 'x', source: 'R1' }, { id: 'INT-07', requirement: 'r seven', status: 'x', source: 'R1' }] };
|
||||
const s = suite(tr); if (!(s.tests[2].definition && /V6-12/.test(s.tests[2].definition) && s.tests[2].accept === 'r seven')) { console.log('self-test failed: INT-07 does not carry V6-12 as a definition beside its verbatim requirement'); fails = 1; }
|
||||
if (!(s.tests.length === 3 && s.tests[0].id === 'INT-01' && s.tests[0].accept === 'r one' && /proving/.test(s.tests[0].owner_lane) && /node/.test(s.tests[1].owner_lane) && s.tests[0].run_status === 'NOT RUN' && s.tests[0].source[0] === 'R1')) { console.log('self-test failed: the INT cases are not shaped from the gates with the crosswalk owners'); fails = 1; }
|
||||
const reg = { suites: [{ code: 'INT', notes: [{ text: 'n' }], tests: [{ id: 'INT-01', run_status: 'NOT RUN', in_progress_since: 't', run_id: 'r' }] }] }; const m = merge(reg, s); const i = m.suites.find((x) => x.code === 'INT');
|
||||
if (!(i.tests[0].in_progress_since === 't' && i.tests[1].run_status === 'NOT RUN' && i.notes?.length === 1)) { console.log('self-test failed: regenerating lost live fields or notes'); fails = 1; }
|
||||
const reg = { suites: [{ code: 'INT', notes: [{ text: 'n' }], tests: [{ id: 'INT-01', run_status: 'NOT RUN', in_progress_since: 't', evidence_records: { 'c:x': { decision: 'PASS' } }, run_id: 'r' }] }] }; const m = merge(reg, s); const i = m.suites.find((x) => x.code === 'INT');
|
||||
if (!(i.tests[0].in_progress_since === 't' && i.tests[0].evidence_records?.['c:x']?.decision === 'PASS' && i.tests[1].run_status === 'NOT RUN' && i.notes?.length === 1)) { console.log('self-test failed: regenerating lost live fields or notes'); fails = 1; }
|
||||
const map = { cells: { c: { cases: ['INT-01'] } }, not_run: {} }; if (!(mapReasons(map, s) === 2 && !map.not_run['INT-01'] && /node lane/.test(map.not_run['INT-05']) && /CI steward/.test(map.not_run['INT-07']))) { console.log('self-test failed: the map reasons'); fails = 1; }
|
||||
if (!fails) console.log('self-test passed: one INT case per integration gate, the requirement verbatim, source R1, NOT RUN, the owner from the crosswalk; regenerating keeps live fields and notes; unmapped gates get a NOT RUN reason naming the owner'); process.exit(fails);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -283,7 +283,7 @@ success 4 u push run
|
|||
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"
|
||||
git checkout -q master; printf '{"run_id":"r-master","manifest_sha":"m","method":"native","cells":[{"cell":"c2","status":"NOT RUN","evidence":"tools/ci/test-map.json"}]}\n' > /tmp/r-master.json
|
||||
node tools/ci/test-record.mjs --record /tmp/r-master.json >/dev/null && git add -A && git -c user.name=t -c user.email=t@t commit -q -m "master row" ) >/dev/null 2>&1
|
||||
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 "
|
||||
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=AM "$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
|
||||
# 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
|
||||
|
|
@ -295,8 +295,17 @@ import json; d=json.load(open('docs/plans/igneum-2.0-test-registry.json')); t={c
|
|||
# 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","method":"native","cells":[{"cell":"c1","status":"NOT RUN","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')" )
|
||||
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=AM "$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
|
||||
# a MODIFIED batch replays too (8 October 2026, 21:4x UK: a re-record of the kills batch on a branch never reached master's rows because
|
||||
# the replay read added batches only): master carries r-mod.json, the branch re-records it as FAIL, the merge must move X-2 to FAIL
|
||||
( cd "$rb" && git checkout -q master && git tag premod && printf '{"run_id":"r-mod","manifest_sha":"m","method":"native","cells":[{"cell":"c2","status":"NOT RUN","evidence":"tools/ci/test-map.json"}]}\n' > tools/ci/batches/r-mod.json && node tools/ci/test-record.mjs --record tools/ci/batches/r-mod.json >/dev/null && git add -A && git -c user.name=t -c user.email=t@t commit -q -m r-mod && git tag mbase
|
||||
git checkout -q -b mod mbase && printf '{"run_id":"r-mod","manifest_sha":"m","method":"native","cells":[{"cell":"c2","status":"FAIL","evidence":"tools/ci/test-map.json"}]}\n' > tools/ci/batches/r-mod.json && node tools/ci/test-record.mjs --record tools/ci/batches/r-mod.json >/dev/null && git add -A && git -c user.name=t -c user.email=t@t commit -q -m mod
|
||||
git checkout -q master && printf '{"run_id":"r-m2","manifest_sha":"m","method":"native","cells":[{"cell":"c1","status":"NOT RUN","evidence":"tools/ci/test-map.json"}]}\n' > tools/ci/batches/r-m2.json && node tools/ci/test-record.mjs --record tools/ci/batches/r-m2.json >/dev/null && git add -A && git -c user.name=t -c user.email=t@t commit -q -m m2 ) >/dev/null 2>&1 || { echo "self-test failed: the modified-batch fixture did not build"; fails=1; }
|
||||
out=$( cd "$rb" && TIP=$(git rev-parse master) && SHA=$(git rev-parse mod) && BASE=$(git merge-base "$TIP" "$SHA") && BATCHES=$(git diff --name-only --diff-filter=AM "$BASE" "$SHA" -- tools/ci/batches/ | grep -E '\.json$') && MAP_CHANGED=0 && 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 mod" 2>&1 && python3 -c "
|
||||
import json; d=json.load(open('docs/plans/igneum-2.0-test-registry.json')); t={c['id']:c.get('run_status') for s in d['suites'] for c in s['tests']}; print('status', t)" )
|
||||
case "$out" in *"'X-2': 'FAIL'"*) ;; *) echo "self-test failed: a batch modified on the branch did not replay onto master's registry: $out"; fails=1 ;; esac
|
||||
( cd "$rb" && git checkout -q master && git reset -q --hard premod && git checkout -q both ) >/dev/null 2>&1 # the fixture back to where the later cases expect it
|
||||
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; }
|
||||
|
|
@ -313,7 +322,7 @@ error: failed to push some refs" && { echo "self-test failed: a red check was re
|
|||
rm -rf "$ld/master-landing"; unset IGNEUM_LOCK_SSH IGNEUM_LOCK_DIR IGNEUM_LOCK_CAP IGNEUM_LOCK_STALE IGNEUM_LOCK_POLL
|
||||
# the branch-side merge of master under the lock: master moved (c4 and a row), the branch (c3 and a batch) takes it with the transforms
|
||||
( cd "$rb" && git checkout -q both ) >/dev/null 2>&1
|
||||
out=$( cd "$rb" && git checkout -q both && TIP=$(git rev-parse master) && SHA=$(git rev-parse both) && BASE0=$(git merge-base "$TIP" "$SHA") && BATCHES=$(git diff --name-only --diff-filter=A "$BASE0" "$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 master into both" branch 2>&1 && git merge-base --is-ancestor master HEAD && echo ancestor && 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']) and any(c.get('run_id')=='r-master' for s in d['suites'] for c in s['tests']) else 'rows-lost')" )
|
||||
out=$( cd "$rb" && git checkout -q both && TIP=$(git rev-parse master) && SHA=$(git rev-parse both) && BASE0=$(git merge-base "$TIP" "$SHA") && BATCHES=$(git diff --name-only --diff-filter=AM "$BASE0" "$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 master into both" branch 2>&1 && git merge-base --is-ancestor master HEAD && echo ancestor && 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']) and any(c.get('run_id')=='r-master' for s in d['suites'] for c in s['tests']) else 'rows-lost')" )
|
||||
case "$out" in *ancestor*2*ok*) ;; *) echo "self-test failed: the branch-side merge of master under the lock did not carry master's rows and cells plus the branch's: $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; a map change regenerates the harness page at the merge; a push that lost the ref race retries without re-running the hook; the master-landing lock holds for the whole landing and master merges into the branch under it"
|
||||
exit $fails
|
||||
|
|
@ -349,7 +358,7 @@ git fetch -q "$REMOTE" master
|
|||
if ! git merge-base --is-ancestor "$REMOTE/master" "$SHA"; then
|
||||
echo "merge-to-master: master moved since the branch point ($(git rev-parse --short "$REMOTE/master")); merging it into $BRANCH under the lock"
|
||||
PRE_SHA="$SHA"; BASE0=$(git merge-base "$SHA" "$REMOTE/master")
|
||||
BATCHES=$(git diff --name-only --diff-filter=A "$BASE0" "$SHA" -- tools/ci/batches/ | grep -E '\.json$' || true); NOTES=$(git diff --name-only --diff-filter=A "$BASE0" "$SHA" -- tools/ci/notes/ | grep -E '\.json$' || true)
|
||||
BATCHES=$(git diff --name-only --diff-filter=AM "$BASE0" "$SHA" -- tools/ci/batches/ | grep -E '\.json$' || true); NOTES=$(git diff --name-only --diff-filter=AM "$BASE0" "$SHA" -- tools/ci/notes/ | grep -E '\.json$' || true)
|
||||
MAP_CHANGED=0; git diff --quiet "$BASE0" "$SHA" -- "${MAP_PATH:-tools/ci/test-map.json}" 2>/dev/null || MAP_CHANGED=1
|
||||
REGISTRY_PATH="${REGISTRY_PATH:-docs/plans/igneum-2.0-test-registry.json}"; MAP_PATH="${MAP_PATH:-tools/ci/test-map.json}"; PAGE_PATH="${PAGE_PATH:-docs/plans/igneum-2.0-test-harness-map.md}"
|
||||
[ -n "$NOTES" ] || [ "$MAP_CHANGED" = 1 ] || git diff --quiet "$BASE0" "$SHA" -- "$REGISTRY_PATH" 2>/dev/null || BATCHES="${BATCHES:-.}"
|
||||
|
|
@ -366,8 +375,8 @@ bash tools/ci/rule24-crate-gate.sh "$BASE" "$SHA" || { echo "merge-to-master: RE
|
|||
# every batch the branch added onto master's copy of the registry (tools/ci/test-record.mjs --record, idempotent), so the branch's
|
||||
# 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
|
||||
BATCHES=$(git diff --name-only --diff-filter=AM "$BASE" "$SHA" -- tools/ci/batches/ | grep -E '\.json$' || true)
|
||||
NOTES=$(git diff --name-only --diff-filter=AM "$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 tools/ci/int-suite.mjs docs/analysis/review-2026-10-08-b/findings.json docs/analysis/review-2026-10-08-b/dispatch.md docs/plans/igneum-2.0-master/traceability.json 2>/dev/null || REVGEN=1 # the REV and INT suites regenerate 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"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,13 @@ the worker never answered.
|
|||
tools/ci/p01-vectors.py --worker <bin> [--worker-arg=X ...] --pack <dir> --reference <file> --count 1000000
|
||||
(a worker argument that itself starts with "--" must be given as --worker-arg=--serve; argparse reads "--worker-arg --serve" as two options)
|
||||
[--start 0] [--job-nonces 1048576] [--prehash <64 hex>] [--manifest <sha>] --out <evidence.json>
|
||||
tools/ci/p01-vectors.py --worker <bin> [--worker-arg X ...] --job-context <job-context.json> --phase 1|2|3 --out <evidence.json>
|
||||
tools/ci/p01-vectors.py --self-test
|
||||
The job context (the same-work test, docs/plans/igneum-2.0-same-work-test.md) names two packs and two references (day D and
|
||||
day D+1), the prehash, the range width (range_log2, 20) and the boundary nonce; phase N runs nonces [(N-1)*2^w, N*2^w): phase 1
|
||||
under day D, phase 3 under day D+1, phase 2 under day D up to the boundary nonce and day D+1 from it, no job line crossing the
|
||||
boundary, and the evidence carries the boundary block (the nonce, the program id and day bytes on each side, the two hashes
|
||||
either side) that the readers are compared on.
|
||||
The job line is pool.rs's: `job <seq> <prehash hex> <share_target64 hex16> <start nonce> <nonces> <epoch seed bytes hex> <day bytes hex> class=<c> era=<era hex>`.
|
||||
"""
|
||||
import argparse, json, os, subprocess, sys, tempfile, time
|
||||
|
|
@ -31,9 +37,36 @@ def pack_fields(pack):
|
|||
cls = j.get('program_class', '?'); era = j.get('era_seed_bytes', '')
|
||||
return j.get('seed_bytes', ''), j['dataset']['day_bytes'], cls, era, j.get('program_id', '?'), j.get('generator', '?')
|
||||
|
||||
def segments_for(a):
|
||||
"""[(start, end, pack dir, reference path)] with no job crossing a segment edge; one segment for the plain form."""
|
||||
if not a.job_context: return [(a.start, a.start + a.count, a.pack, a.reference)], None
|
||||
jc = json.load(open(a.job_context)); w = int(jc.get('range_log2', 20)); n = int(a.phase)
|
||||
if n not in (1, 2, 3): raise SystemExit('p01-vectors: --phase must be 1, 2 or 3')
|
||||
base = os.path.dirname(os.path.abspath(a.job_context))
|
||||
pth = lambda x: x if os.path.isabs(x) else os.path.join(base, x)
|
||||
packs, refs = jc['packs'], jc['references']
|
||||
s0, e0 = (n - 1) << w, n << w
|
||||
if n == 1: segs = [(s0, e0, pth(packs['D']), pth(refs['D']))]
|
||||
elif n == 3: segs = [(s0, e0, pth(packs['D1']), pth(refs['D1']))]
|
||||
else:
|
||||
b = int(jc['boundary_nonce'])
|
||||
if not (s0 < b < e0): raise SystemExit(f'p01-vectors: the boundary nonce {b} is not inside phase 2 [{s0}, {e0})')
|
||||
segs = [(s0, b, pth(packs['D']), pth(refs['D'])), (b, e0, pth(packs['D1']), pth(refs['D1']))]
|
||||
a.start, a.count = s0, e0 - s0
|
||||
if jc.get('prehash'): a.prehash = jc['prehash']
|
||||
if jc.get('manifest') and not a.manifest: a.manifest = jc['manifest']
|
||||
return segs, jc
|
||||
|
||||
def run(a):
|
||||
ref = read_reference(a.reference)
|
||||
seed_bytes, day_bytes, cls, era, program_id, generator = pack_fields(a.pack)
|
||||
segs, jc = segments_for(a)
|
||||
ref = {}; seg_fields = []
|
||||
for (ss, se, pack, rpath) in segs:
|
||||
r = read_reference(rpath); ref.update({k: v for k, v in r.items() if ss <= k < se}); seg_fields.append((ss, se, pack_fields(pack)))
|
||||
seed_bytes, day_bytes, cls, era, program_id, generator = seg_fields[0][2]
|
||||
def fields_at(n):
|
||||
for (ss, se, f) in seg_fields:
|
||||
if ss <= n < se: return ss, se, f
|
||||
return None
|
||||
p = subprocess.Popen([a.worker] + a.worker_arg, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1)
|
||||
ready = None; t0 = time.time()
|
||||
for line in p.stdout:
|
||||
|
|
@ -44,8 +77,9 @@ def run(a):
|
|||
def feed():
|
||||
nonlocal seq, start
|
||||
while start < end and len(pending) < 4:
|
||||
n = min(a.job_nonces, end - start); seq += 1
|
||||
p.stdin.write(f"job {seq} {a.prehash} {'f'*16} {start} {n} {seed_bytes} {day_bytes} class={cls} era={era}\n"); p.stdin.flush()
|
||||
ss, se, (sb, db, c, er, _pid, _gen) = fields_at(start)
|
||||
n = min(a.job_nonces, end - start, se - start); seq += 1 # a job never crosses a segment edge (the day boundary)
|
||||
p.stdin.write(f"job {seq} {a.prehash} {'f'*16} {start} {n} {sb} {db} class={c} era={er}\n"); p.stdin.flush()
|
||||
pending.add(seq); start += n
|
||||
feed()
|
||||
for line in p.stdout:
|
||||
|
|
@ -72,7 +106,15 @@ def run(a):
|
|||
'worker': a.worker, 'worker_args': a.worker_arg, 'device_line': ready, 'manifest_sha': a.manifest, 'prehash': a.prehash,
|
||||
'nonces': {'start': a.start, 'count': a.count}, 'answered': len(got), 'agree': agree, 'disagree': disagree_count[0], 'missing': len(missing),
|
||||
'first_disagreements': disagree, 'first_missing': missing[:10], 'worker_errors': errors[:10], 'seconds': round(time.time() - t0, 1), 'at': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}
|
||||
ev['verdict'] = 'PASS' if (agree == a.count and not disagree and not missing) else 'FAIL'
|
||||
if jc is not None:
|
||||
ev['job_context'] = os.path.abspath(a.job_context); ev['phase'] = int(a.phase); ev['object'] = jc.get('object')
|
||||
ev['segments'] = [{'start': ss, 'end': se, 'program_id': f[4], 'day_bytes': f[1], 'class': f[2]} for (ss, se, f) in seg_fields]
|
||||
if len(seg_fields) == 2:
|
||||
b = seg_fields[1][0]; fb, fa = seg_fields[0][2], seg_fields[1][2]
|
||||
ev['boundary'] = {'nonce': b, 'program_id_before': fb[4], 'program_id_after': fa[4], 'day_bytes_before': fb[1], 'day_bytes_after': fa[1],
|
||||
'hash_before': got.get(b - 1), 'hash_after': got.get(b), 'reference_before': ref.get(b - 1), 'reference_after': ref.get(b),
|
||||
'switched': bool(fb[1] != fa[1] and got.get(b - 1) == ref.get(b - 1) and got.get(b) == ref.get(b))}
|
||||
ev['verdict'] = 'PASS' if (agree == a.count and not disagree and not missing and (jc is None or len(seg_fields) < 2 or ev['boundary']['switched'])) else 'FAIL'
|
||||
return ev, (0 if ev['verdict'] == 'PASS' else 1)
|
||||
|
||||
disagree_count = [0]; errors = []
|
||||
|
|
@ -92,6 +134,7 @@ for line in sys.stdin:
|
|||
for k in range(start, start + n):
|
||||
if os.environ.get("DROP") == "1" and k == 9: continue
|
||||
h = (k * 0x9e3779b97f4a7c15) % (1 << 64)
|
||||
if p[7] == "dd" * 19: h ^= 0xdd00dd00dd00dd00 # day D+1's bytes hash differently (a reader on the wrong day disagrees)
|
||||
if os.environ.get("WRONG") == "1" and k == 7: h ^= 1
|
||||
print(f"found {seq} {k} {h:016x}", flush=True)
|
||||
print(f"done {seq} {n} 1.0", flush=True)
|
||||
|
|
@ -107,15 +150,40 @@ for line in sys.stdin:
|
|||
if not (rc == 1 and ev.get('verdict') == 'FAIL' and ev['disagree'] == 1 and ev['first_disagreements'][0]['nonce'] == 7): print(f"self-test failed: one wrong hash was not a FAIL naming nonce 7: rc={rc} {ev.get('first_disagreements')}"); fails = 1
|
||||
rc, ev = go({'DROP': '1'}, 'drop')
|
||||
if not (rc == 1 and ev['missing'] == 1 and ev['first_missing'] == [9]): print(f"self-test failed: an unanswered nonce was not a FAIL naming nonce 9: rc={rc} {ev.get('first_missing')}"); fails = 1
|
||||
if not fails: print('self-test passed: a clean million-shape run is PASS with the counts; one wrong hash is FAIL naming the nonce, the gpu and cpu hashes; an unanswered nonce is FAIL naming it; the job lines carry the pack fields and the all-pass target')
|
||||
# the job-context form: two packs (day D cc.., day D+1 dd..), two references, range_log2 4 (16 nonces a phase), boundary 24 inside phase 2
|
||||
pack2 = os.path.join(d, 'pack2'); os.makedirs(pack2)
|
||||
json.dump({'seed_bytes': 'aa' * 32, 'program_class': 'v5', 'era_seed_bytes': 'bb' * 32, 'program_id': '0x2', 'generator': 5, 'dataset': {'day_bytes': 'dd' * 19, 'log2_words': 20}}, open(os.path.join(pack2, 'program.json'), 'w'))
|
||||
refD = os.path.join(d, 'refD.txt'); open(refD, 'w').write(''.join(f"{k} {(k * 0x9e3779b97f4a7c15) % (1 << 64):016x}\n" for k in range(0, 48)))
|
||||
refD1 = os.path.join(d, 'refD1.txt'); open(refD1, 'w').write(''.join(f"{k} {((k * 0x9e3779b97f4a7c15) % (1 << 64)) ^ 0xdd00dd00dd00dd00:016x}\n" for k in range(0, 48)))
|
||||
jc = os.path.join(d, 'job-context.json')
|
||||
json.dump({'object': 'fake', 'prehash': '00' * 31 + '01', 'range_log2': 4, 'boundary_nonce': 24, 'manifest': 'deadbeef', 'packs': {'D': pack, 'D1': pack2}, 'references': {'D': refD, 'D1': refD1}}, open(jc, 'w'))
|
||||
def gojc(phase, tag, boundary=None):
|
||||
if boundary is not None:
|
||||
j = json.load(open(jc)); j['boundary_nonce'] = boundary; json.dump(j, open(jc, 'w'))
|
||||
out = os.path.join(d, f'{tag}.json')
|
||||
r = subprocess.run([sys.executable, __file__, '--worker', sys.executable, '--worker-arg', w, '--job-context', jc, '--phase', str(phase), '--job-nonces', '8', '--out', out], capture_output=True, text=True)
|
||||
return r.returncode, (json.load(open(out)) if os.path.exists(out) else {}), r.stdout + r.stderr
|
||||
for ph, s0, pid in ((1, 0, '0x1'), (3, 32, '0x2')):
|
||||
rc, ev, o = gojc(ph, f'jc{ph}')
|
||||
if not (rc == 0 and ev.get('verdict') == 'PASS' and ev['nonces'] == {'start': s0, 'count': 16} and ev['segments'][0]['program_id'] == pid and 'boundary' not in ev): print(f"self-test failed: phase {ph} of the job context was not a PASS on its range and pack: rc={rc} {ev.get('nonces')} {ev.get('segments')} {o[-200:]}"); fails = 1
|
||||
rc, ev, o = gojc(2, 'jc2')
|
||||
b = ev.get('boundary', {})
|
||||
if not (rc == 0 and ev.get('verdict') == 'PASS' and ev['nonces'] == {'start': 16, 'count': 16} and b.get('nonce') == 24 and b.get('program_id_before') == '0x1' and b.get('program_id_after') == '0x2' and b.get('switched') is True and len(ev['segments']) == 2): print(f"self-test failed: phase 2 did not switch pack at the boundary nonce 24 with the boundary block: rc={rc} {b} {o[-200:]}"); fails = 1
|
||||
rc, ev, o = gojc(2, 'jc2bad', boundary=40)
|
||||
if rc == 0 or 'not inside phase 2' not in o: print(f"self-test failed: a boundary outside phase 2 was not refused: rc={rc} {o[-200:]}"); fails = 1
|
||||
if not fails: print('self-test passed: a clean million-shape run is PASS with the counts; one wrong hash is FAIL naming the nonce, the gpu and cpu hashes; an unanswered nonce is FAIL naming it; the job-context form runs each phase on its range and pack, splits phase 2 at the boundary nonce with the boundary block, and refuses a boundary outside phase 2; the job lines carry the pack fields and the all-pass target')
|
||||
return fails
|
||||
|
||||
if __name__ == '__main__':
|
||||
if '--self-test' in sys.argv: sys.exit(self_test())
|
||||
ap = argparse.ArgumentParser(); ap.add_argument('--worker', required=True); ap.add_argument('--worker-arg', action='append', default=[]); ap.add_argument('--pack', required=True)
|
||||
ap.add_argument('--reference', required=True); ap.add_argument('--count', type=int, default=1_000_000); ap.add_argument('--start', type=int, default=0); ap.add_argument('--job-nonces', type=int, default=1 << 20)
|
||||
ap = argparse.ArgumentParser(); ap.add_argument('--worker', required=True); ap.add_argument('--worker-arg', action='append', default=[]); ap.add_argument('--pack', default='')
|
||||
ap.add_argument('--reference', default=''); ap.add_argument('--count', type=int, default=1_000_000); ap.add_argument('--start', type=int, default=0); ap.add_argument('--job-nonces', type=int, default=1 << 20)
|
||||
ap.add_argument('--prehash', default='00' * 31 + '01'); ap.add_argument('--manifest', default=''); ap.add_argument('--out', required=True)
|
||||
a = ap.parse_args(); ev, rc = run(a)
|
||||
ap.add_argument('--job-context', default=''); ap.add_argument('--phase', type=int, default=0)
|
||||
a = ap.parse_args()
|
||||
if a.job_context and not a.phase: ap.error('--job-context needs --phase 1|2|3')
|
||||
if not a.job_context and not (a.pack and a.reference): ap.error('--pack and --reference, or --job-context with --phase')
|
||||
ev, rc = run(a)
|
||||
os.makedirs(os.path.dirname(os.path.abspath(a.out)), exist_ok=True); json.dump(ev, open(a.out, 'w'), indent=2)
|
||||
print(f"p01-vectors: {ev.get('verdict', 'ERROR')}: answered {ev.get('answered')} agree {ev.get('agree')} disagree {ev.get('disagree')} missing {ev.get('missing')} in {ev.get('seconds')} s -> {a.out}")
|
||||
sys.exit(rc)
|
||||
|
|
|
|||
|
|
@ -179,7 +179,9 @@ tree_checks() {
|
|||
run "the kit ISA check: a distribution kit's binaries carry no AVX-512 encoding (the x86-64-v3 baseline; self-test with a fake objdump)" bash tools/ci/kit-isa-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 "rule 33: a release entry publishes only with a PASS fresh-install canary record for its sha (the record check and the publish guard, self-tests)" bash -c 'bash tools/ci/canary-check.sh --self-test >/dev/null && bash packaging/ota/publish-manifest.sh --self-test-canary-guard >/dev/null'
|
||||
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 "F03 (Review B): every component's own pin equals packaging/release-manifest.json where a release branch carries one (self-test, then the tree)" bash -c 'bash tools/ci/release-manifest-check.sh --self-test >/dev/null && bash tools/ci/release-manifest-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
|
||||
|
|
|
|||
104
tools/ci/release-manifest-check.sh
Executable file
104
tools/ci/release-manifest-check.sh
Executable file
|
|
@ -0,0 +1,104 @@
|
|||
#!/usr/bin/env bash
|
||||
# F03 (Review B, 8 October 2026): one release manifest (packaging/release-manifest.json) pins node, generator, dataset policy,
|
||||
# acceptance rule, host ABI, miner app, pool, proof guests, verifying keys and activation; every component's own pin must equal it:
|
||||
# 1. packaging/windows/node-source.pin == manifest.node.sha
|
||||
# 2. the node fork's packaging/pow-freeze.txt carries manifest.generator.fingerprint (the fork at <fork dir>, when given)
|
||||
# 3. proving/igneum-prove/elf/manifest.json's elf and vk sha256s == manifest.proof_guests, and the files on disk hash to them
|
||||
# 4. the pool's vendored node checkout (pool/../vendor/igneum-node, when present) is at manifest.node.sha
|
||||
# tools/ci/release-manifest-check.sh [--tree <dir>] [<fork dir>] exit 0 when every pin agrees, 1 with every disagreement named
|
||||
# tools/ci/release-manifest-check.sh --self-test
|
||||
set -euo pipefail
|
||||
HERE="$(cd "$(dirname "$0")" && pwd -P)"; ME="$HERE/$(basename "$0")"
|
||||
check() { # <tree> [<fork dir>] ; prints "red ..." lines
|
||||
local tree="$1" fork="${2:-}" rc=0
|
||||
python3 - "$tree" "$fork" <<'PY' || rc=1
|
||||
import json, sys, os, hashlib, subprocess
|
||||
tree, fork = sys.argv[1], sys.argv[2]; red = []
|
||||
m = json.load(open(os.path.join(tree, 'packaging/release-manifest.json')))
|
||||
pin = os.path.join(tree, 'packaging/windows/node-source.pin')
|
||||
if os.path.exists(pin):
|
||||
p = open(pin).read().split()[0] if open(pin).read().split() else ''
|
||||
if not (p.startswith(m['node']['sha']) or m['node']['sha'].startswith(p)): red.append(f"red node-source.pin reads {p}, the manifest pins node {m['node']['sha']}")
|
||||
else: red.append('red packaging/windows/node-source.pin is missing')
|
||||
pm_path = os.path.join(tree, 'proving/igneum-prove/elf/manifest.json')
|
||||
if os.path.exists(pm_path):
|
||||
pm = json.load(open(pm_path))
|
||||
for g in ('shard', 'aggregator'):
|
||||
for k in ('elf_sha256', 'vk_sha256'):
|
||||
if pm[g][k] != m['proof_guests'][g][k]: red.append(f"red proof guest {g} {k}: the proving manifest reads {pm[g][k][:18]}, the release manifest {m['proof_guests'][g][k][:18]}")
|
||||
for fk, sk in (('elf', 'elf_sha256'), ('vk', 'vk_sha256')):
|
||||
fp = os.path.join(tree, 'proving/igneum-prove/elf', pm[g][fk])
|
||||
if os.path.exists(fp):
|
||||
h = '0x' + hashlib.sha256(open(fp, 'rb').read()).hexdigest()
|
||||
if h != m['proof_guests'][g][sk]: red.append(f"red proof guest {g} {fk} on disk hashes to {h[:18]}, the release manifest pins {m['proof_guests'][g][sk][:18]}")
|
||||
else: red.append('red proving/igneum-prove/elf/manifest.json is missing')
|
||||
# provenance (V6-10, the proving lane's class, 8 October 2026): a proving manifest that names its source_commit must name a commit
|
||||
# the tree's HEAD contains; a manifest pinned from a commit this tree never carried (the 5 October manifest had no provenance at all)
|
||||
# is red. A manifest without source_commit is a note, not a red, until igneum-prove-pin writes one everywhere.
|
||||
if os.path.exists(pm_path) and os.path.isdir(os.path.join(tree, '.git')) or os.path.exists(pm_path) and os.path.isfile(os.path.join(tree, '.git')):
|
||||
sc = pm.get('source_commit')
|
||||
if sc:
|
||||
r = subprocess.run(['git', '-C', tree, 'merge-base', '--is-ancestor', sc, 'HEAD'], capture_output=True, text=True)
|
||||
if r.returncode != 0: red.append(f"red proving manifest source_commit {sc[:12]} is not an ancestor of this tree's HEAD (pinned from a commit this branch never carried)")
|
||||
else: print('release-manifest: note: the proving manifest names no source_commit (pre-provenance pin)')
|
||||
if fork:
|
||||
fz = os.path.join(fork, 'packaging/pow-freeze.txt')
|
||||
if os.path.exists(fz):
|
||||
if m['generator']['fingerprint'] not in open(fz).read(): red.append(f"red the fork's packaging/pow-freeze.txt does not carry the manifest's generator fingerprint {m['generator']['fingerprint'][:16]}")
|
||||
else: red.append(f'red {fz} is missing')
|
||||
try:
|
||||
head = subprocess.run(['git', '-C', fork, 'rev-parse', 'HEAD'], capture_output=True, text=True).stdout.strip()
|
||||
if head and not head.startswith(m['node']['sha']): red.append(f"red the fork checkout is at {head[:8]}, the manifest pins node {m['node']['sha']}")
|
||||
except Exception: pass
|
||||
vend = os.path.join(tree, 'vendor/igneum-node')
|
||||
if os.path.isdir(vend):
|
||||
head = subprocess.run(['git', '-C', vend, 'rev-parse', 'HEAD'], capture_output=True, text=True).stdout.strip()
|
||||
if head and not head.startswith(m['node']['sha']): red.append(f"red the pool's vendored node checkout is at {head[:8]}, the manifest pins node {m['node']['sha']} (the shadow_reps seam closes by a build against the pinned node)")
|
||||
nodeas = os.path.join(tree, 'pool/src/node.rs')
|
||||
if os.path.exists(nodeas) and 'struct EpochSeeds' in open(nodeas).read(): red.append('red pool/src/node.rs redefines EpochSeeds; it must import kaspa_pow::igneum::EpochSeeds')
|
||||
# V6-12 (R1 p. 213, the master): the signed manifest binds lockfile hashes, kernel and host binaries, supported devices and drivers, the prover
|
||||
# server patch, memory thresholds and tuner identity; a manifest without the block reads BLOCKED on INT-07, never a pass of it
|
||||
v = m.get('v6_12') or {}
|
||||
for k in ('lockfile_sha256', 'kernel_binaries', 'host_binaries', 'supported_devices', 'supported_drivers', 'prover_server_patch', 'memory_thresholds', 'tuner_identity', 'signature'):
|
||||
if k not in v: print(f'note V6-12 field {k} is not in the manifest (INT-07 reads BLOCKED until it is)')
|
||||
for r in red: print(r)
|
||||
sys.exit(1 if red else 0)
|
||||
PY
|
||||
return $rc
|
||||
}
|
||||
if [ "${1:-}" = --self-test ]; then
|
||||
d=$(mktemp -d); trap 'rm -rf "$d"' EXIT; fails=0
|
||||
mk() { local t="$d/$1"; mkdir -p "$t/packaging/windows" "$t/proving/igneum-prove/elf" "$t/pool/src"; printf 'elf' > "$t/proving/igneum-prove/elf/a.elf"; printf 'vk' > "$t/proving/igneum-prove/elf/a.vk"
|
||||
local es="0x$(printf 'elf' | shasum -a 256 | cut -d' ' -f1)" vs="0x$(printf 'vk' | shasum -a 256 | cut -d' ' -f1)"
|
||||
printf '{"shard":{"elf":"a.elf","elf_sha256":"%s","vk":"a.vk","vk_sha256":"%s"},"aggregator":{"elf":"a.elf","elf_sha256":"%s","vk":"a.vk","vk_sha256":"%s"}}\n' "$es" "$vs" "$es" "$vs" > "$t/proving/igneum-prove/elf/manifest.json"
|
||||
printf '{"node":{"sha":"%s"},"generator":{"fingerprint":"ffff0000"},"proof_guests":{"shard":{"elf_sha256":"%s","vk_sha256":"%s"},"aggregator":{"elf_sha256":"%s","vk_sha256":"%s"}}}\n' "$2" "$es" "$vs" "$es" "$vs" > "$t/packaging/release-manifest.json"
|
||||
printf '%s\n' "$3" > "$t/packaging/windows/node-source.pin"; printf 'use kaspa_pow::igneum::EpochSeeds;\n' > "$t/pool/src/node.rs"; }
|
||||
mk agree abcd1234 abcd1234; mk pinoff abcd1234 ffff1234
|
||||
bash "$ME" --tree "$d/agree" >/dev/null 2>&1 || { echo "self-test failed: agreeing pins were refused: $(bash "$ME" --tree "$d/agree" 2>&1)"; fails=1; }
|
||||
out=$(bash "$ME" --tree "$d/pinoff" 2>&1) && { echo "self-test failed: a node-source pin off the manifest passed"; fails=1; }; case "$out" in *"node-source.pin reads ffff1234"*) ;; *) echo "self-test failed: the pin was not named: $out"; fails=1 ;; esac
|
||||
printf 'elf2' > "$d/agree/proving/igneum-prove/elf/a.elf"; out=$(bash "$ME" --tree "$d/agree" 2>&1) && { echo "self-test failed: a guest file that hashes off the manifest passed"; fails=1; }; case "$out" in *"on disk hashes to"*) ;; *) echo "self-test failed: the hash drift was not named: $out"; fails=1 ;; esac
|
||||
printf 'elf' > "$d/agree/proving/igneum-prove/elf/a.elf"; mkdir -p "$d/fork/packaging"; printf '# f\nffff0000 c1 v5 2026\n' > "$d/fork/packaging/pow-freeze.txt"
|
||||
bash "$ME" --tree "$d/agree" "$d/fork" >/dev/null 2>&1 || { echo "self-test failed: a fork carrying the fingerprint was refused: $(bash "$ME" --tree "$d/agree" "$d/fork" 2>&1)"; fails=1; }
|
||||
printf '# f\n00000000 c1 v5 2026\n' > "$d/fork/packaging/pow-freeze.txt"; out=$(bash "$ME" --tree "$d/agree" "$d/fork" 2>&1) && { echo "self-test failed: a fork without the fingerprint passed"; fails=1; }
|
||||
printf 'pub struct EpochSeeds {}\n' > "$d/agree/pool/src/node.rs"; mkdir -p "$d/agree/vendor/igneum-node" && ( cd "$d/agree/vendor/igneum-node" && git init -q && git -c user.name=t -c user.email=t@t commit -q --allow-empty -m x ) >/dev/null 2>&1
|
||||
out=$(bash "$ME" --tree "$d/agree" 2>&1) && { echo "self-test failed: a redefined EpochSeeds and an unpinned vendored node passed"; fails=1; }; case "$out" in *"redefines EpochSeeds"*) ;; *) echo "self-test failed: the seam was not named: $out"; fails=1 ;; esac
|
||||
# provenance: a git tree whose proving manifest names a source_commit HEAD contains passes; one naming a commit HEAD never carried is red
|
||||
mk prov abcd1234 abcd1234; ( cd "$d/prov" && git init -q && git add -A && git -c user.name=t -c user.email=t@t commit -q -m x ) >/dev/null 2>&1; sc=$(git -C "$d/prov" rev-parse HEAD)
|
||||
python3 - "$d/prov/proving/igneum-prove/elf/manifest.json" "$sc" <<'PY2'
|
||||
import json,sys; p=sys.argv[1]; d=json.load(open(p)); d['source_commit']=sys.argv[2]; json.dump(d,open(p,'w'))
|
||||
PY2
|
||||
bash "$ME" --tree "$d/prov" >/dev/null 2>&1 || { echo "self-test failed: a manifest whose source_commit HEAD contains was refused: $(bash "$ME" --tree "$d/prov" 2>&1)"; fails=1; }
|
||||
python3 - "$d/prov/proving/igneum-prove/elf/manifest.json" <<'PY2'
|
||||
import json,sys; p=sys.argv[1]; d=json.load(open(p)); d['source_commit']='0'*40; json.dump(d,open(p,'w'))
|
||||
PY2
|
||||
out=$(bash "$ME" --tree "$d/prov" 2>&1) && { echo "self-test failed: a manifest pinned from a commit the tree never carried passed"; fails=1; }; case "$out" in *"not an ancestor"*) ;; *) echo "self-test failed: the provenance red was not named: $out"; fails=1 ;; esac
|
||||
[ "$fails" = 0 ] && echo "self-test passed: agreeing pins pass; a node-source pin, a guest file's hash, a fork freeze file, the pool's vendored node checkout, a redefined EpochSeeds or a proving manifest pinned from a commit the tree never carried is red and named"
|
||||
exit $fails
|
||||
fi
|
||||
TREE="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"; FORK=""
|
||||
while [ $# -gt 0 ]; do case "$1" in --tree) TREE="$2"; shift 2 ;; *) FORK="$1"; shift ;; esac; done
|
||||
[ -f "$TREE/packaging/release-manifest.json" ] || { echo "release-manifest: no packaging/release-manifest.json on this tree: not a release branch, nothing to pin"; exit 0; }
|
||||
rc=0; out=$(check "$TREE" "$FORK") || rc=1
|
||||
printf '%s\n' "$out" | sed -n 's/^red /release-manifest: RED: /p; s/^note /release-manifest: /p' | grep . || true
|
||||
[ "$rc" = 0 ] && echo "release-manifest: every component's own pin equals packaging/release-manifest.json"
|
||||
exit $rc
|
||||
|
|
@ -36,7 +36,7 @@ function suite(findings, dispatchMd, prefix, sourceNote, master) {
|
|||
}
|
||||
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', 'in_progress_since', 'deferral_note', 'approvals']) if (k in o) t[k] = o[k]; }
|
||||
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', 'evidence_records', 'blocked_on', 'method_recorded', 'in_progress_since', 'deferral_note', 'approvals']) 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')) {
|
||||
|
|
@ -49,9 +49,9 @@ if (args.includes('--self-test')) {
|
|||
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 === 'R2-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: 'R2-F01-R01', run_status: 'NOT RUN', in_progress_since: 't', run_id: 'r9' }] }] };
|
||||
const reg = { suites: [{ code: 'GOV', tests: [] }, { code: 'REV', tests: [{ id: 'R2-F01-R01', run_status: 'NOT RUN', in_progress_since: 't', evidence_records: { 'c:x': { decision: 'PASS' } }, 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].in_progress_since === 't' && 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; }
|
||||
if (!(m.suites.length === 2 && rev.tests.length === 3 && rev.tests[0].in_progress_since === 't' && rev.tests[0].evidence_records?.['c:x']?.decision === 'PASS' && 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: ['R2-F01-R01'] } }, not_run: { 'REV-F02-1': 'old' } }; const n = mapReasons(map, s);
|
||||
if (!(n === 2 && !('R2-F01-R01' 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');
|
||||
|
|
|
|||
|
|
@ -225,8 +225,8 @@
|
|||
}
|
||||
},
|
||||
"bench:pc1-packs": {
|
||||
"command": "tools/ca3-v4-amend/pc1-ca4-packs.ps1 on PC 1 (RTX 5090, the signed-jobs channel, --cards-off, the Power Helper lock at 1,300 MHz, nvidia-smi at 1 Hz, 250 x 2^24 per row, the kit's CUDA worker --bench)",
|
||||
"box_class": "PC 1 bench",
|
||||
"command": "tools/ca3-v4-amend/pc1-ca4-packs.ps1 on the project's own rig (RTX 5090, the signed-jobs channel, --cards-off, the Power Helper lock at 1,300 MHz, nvidia-smi at 1 Hz, 250 x 2^24 per row, the kit's CUDA worker --bench)",
|
||||
"box_class": "the project's own rig bench",
|
||||
"fixtures": [
|
||||
"F0",
|
||||
"F1"
|
||||
|
|
@ -245,8 +245,8 @@
|
|||
}
|
||||
},
|
||||
"bench:pc1-amd": {
|
||||
"command": "tools/ca3-v4-amend/pc1-amd-cardin-bench.ps1 on PC 1 (RX 7600, OpenCL, gfx1102, 30 dispatches of 2^24 per size, rate only)",
|
||||
"box_class": "PC 1 bench",
|
||||
"command": "tools/ca3-v4-amend/pc1-amd-cardin-bench.ps1 on the project's own rig (RX 7600, OpenCL, gfx1102, 30 dispatches of 2^24 per size, rate only)",
|
||||
"box_class": "the project's own rig bench",
|
||||
"fixtures": [
|
||||
"F0",
|
||||
"F1"
|
||||
|
|
@ -422,7 +422,7 @@
|
|||
"VER-08"
|
||||
],
|
||||
"coverage": {
|
||||
"VER-03": "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-03": "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 the earlier devnet fixtures, the 2.0 devnet re-run waits for its first paid segment whose last block carries a transaction",
|
||||
"VER-04": "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": "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": "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",
|
||||
|
|
@ -576,8 +576,8 @@
|
|||
}
|
||||
},
|
||||
"bench:amd-intel-energy": {
|
||||
"command": "the AMD-and-Intel energy lane jobs on the signed-jobs channel: run-ae-pc2-b580-energy-20261008 (PC 2, Arc B580, the class v6 kit worker --memprobe and --bench-pack on hl-v6-foldrw and hl-v6-all, Level Zero energy counter) and run-ae-pc1-7600-energy-20261008 (PC 1, RX 7600, the same with ADLX watts at stock, plimit -30, gmax -500 + plimit -30); scripts in docs/analysis/class-v6/amd-intel-energy/",
|
||||
"box_class": "PC 1 and PC 2 bench (OpenCL)",
|
||||
"command": "the AMD-and-Intel energy lane jobs on the signed-jobs channel: run-ae-pc2-b580-energy-20261008 (PC 2, Arc B580, the class v6 kit worker --memprobe and --bench-pack on hl-v6-foldrw and hl-v6-all, Level Zero energy counter) and run-ae-pc1-7600-energy-20261008 (the project's own rig, RX 7600, the same with ADLX watts at stock, plimit -30, gmax -500 + plimit -30); scripts in docs/analysis/class-v6/amd-intel-energy/",
|
||||
"box_class": "the project's own rig and PC 2 bench (OpenCL)",
|
||||
"fixtures": [
|
||||
"F0",
|
||||
"F1"
|
||||
|
|
@ -588,19 +588,116 @@
|
|||
"coverage": {
|
||||
"GPU-03": "partial: the 64-register window on AMD and Intel, rate per unit of work (RX 7600 0 percent, Arc B580 -0.3 percent, kernel throughput, quiet) with the B580 fingerprints equal on both packs and the offline RDNA allocation (160 VGPRs, no spill); energy owed (the 7600 job queued, the B580 counter unsupported unelevated); team-run, not under the standard's paired protocol or a wall meter"
|
||||
}
|
||||
},
|
||||
"adversary:mf-placed": {
|
||||
"command": "cd tools/chip-model/mf && make flow-<design> power-<design> (inside openroad/orfs:latest on a rented CPU host; never the Mac); python3 flow/collect.py results; python3 flow/board.py <pJ> <lo> <hi> <leak> <mm2>; python3 flow/hash.py results <design> power",
|
||||
"box_class": "rented CPU host (Vast, 48 to 72 cores) on the ORFS image",
|
||||
"fixtures": [
|
||||
"F0",
|
||||
"F1"
|
||||
],
|
||||
"cases": [
|
||||
"ADV-01",
|
||||
"ADV-03",
|
||||
"ADV-07",
|
||||
"ADV-08",
|
||||
"POW-03",
|
||||
"POW-04",
|
||||
"ADV-05"
|
||||
],
|
||||
"coverage": {
|
||||
"ADV-01": "the mf core (18 families, SRAM window and imem macros, operand isolation, 5-phase port) placed and routed on ASAP7 with SPEF and a gate-level VCD; k per family and per draw; outputs checked against the RTL simulation's checksums per tag, not yet against the POW vectors (owed)",
|
||||
"ADV-03": "the data-local, hybrid and companion-host rows of multi-family-adversary.md sections 11, 13 and 14 (bit counts closed-form; the hop and wire energies claimed)",
|
||||
"ADV-07": "the lifetime table and the six-row transition matrix (sections 7 and 14) on the placed rows; the 32-lane genesis comparator placed on adv-g",
|
||||
"ADV-08": "the best-cost envelope on the complete machine (section 6) and the SRAM-die ticket reconciliation with lane B (section 15); the unaffiliated second reviewer is not this lane's",
|
||||
"POW-03": "partial: the live state held in an SRAM macro per 8 lanes with a time-multiplexed single port and operand isolation, the complete-system cost in section 6; the liveness trace itself is the connected-state lane's tool",
|
||||
"POW-04": "partial: the 64-register window's cost on the macro core (sections 4 and 5, agreeing with the k lane's gated-flop row within 5 percent); the restructured candidate's GPU cost is the invention lane's",
|
||||
"ADV-05": "partial: the SRAM macros, ports, wiring, clocking and the complete-board terms are modelled in sections 2.3, 5 and 6 with the unmodelled items carried as uncertainty; the calibration against an existing hardware block is the k lane's bare-lane row beside it, owed as a named comparison"
|
||||
}
|
||||
},
|
||||
"adversary:d2b": {
|
||||
"command": "cd tools/chip-model/mf && python3 flow/d2b.py results N5 1 && python3 flow/d2b.py results N3 1 (on a build box under lease pool or a rented host; reads results/table.csv)",
|
||||
"box_class": "any box (a one-minute Python model on the placed rows)",
|
||||
"fixtures": [
|
||||
"F0"
|
||||
],
|
||||
"cases": [
|
||||
"ADV-02",
|
||||
"ADV-04"
|
||||
],
|
||||
"coverage": {
|
||||
"ADV-02": "memory sharing, partial stores at the measured window-layer hit rates (and hit 0.50 with layer 8 off), recomputation and the set-up amortisation on the complete board (section 13); the formal memory model's terms (section 16)",
|
||||
"ADV-04": "selective participation over 2,000 era draws under the layer 1 band with two reserve families live per epoch, the specialist's revenue against its ratio gain (section 13); downtime and re-entry priced as the DAA window's lag, not simulated against the difficulty rule itself (owed)"
|
||||
}
|
||||
},
|
||||
"pc:install-update": {
|
||||
"command": "signed jobs and relay runs on the project's own rig (ae432dc7) and PC 2 (1ccfe586): the installer over the running app (rule 14: installed versions, the process set, the first upload), the OTA path (update-now, the update-return read-back line), and the no-click resume after the install (relay run mining-on-after-200.ps1)",
|
||||
"box_class": "PC (the two Windows PCs; nothing on the Mac)",
|
||||
"fixtures": [
|
||||
"F2"
|
||||
],
|
||||
"cases": [
|
||||
"UX-01",
|
||||
"UX-07",
|
||||
"OPS-03"
|
||||
],
|
||||
"coverage": {
|
||||
"UX-01": "team-run evidence only (the two PCs taking the Windows entry through the app's own update path with no click and mining on every card); the P10 study is NOT RUN until unaffiliated participants are recruited",
|
||||
"UX-07": "partial: the install classes (the payload's stop step, the detached installer under a job, the installer refusing a wrong-version name, a stale install flag) and the no-click resume; the recovery-instruction and canary rows are the shipper's",
|
||||
"OPS-03": "partial: the update path's read-back and the signed manifest's verify on the PC (a tampered or rolled-back payload is refused by the engine's manifest check); authority separation is the shipper's review"
|
||||
}
|
||||
},
|
||||
"harness:release-manifest": {
|
||||
"command": "bash tools/ci/release-manifest-check.sh (self-test, then the tree; the pins, the proof guests' hashes on disk, the fork freeze, the pool's vendored node, the proving manifest's source_commit provenance) and bash tools/ci/build-from-manifest.sh --box N on the release tree (kaspad with igneum-pow, igneum-miner, igneum-pool, igneum-app, igneum-prove-host from packaging/release-manifest.json with the node vendored at the manifest's sha)",
|
||||
"box_class": "gate",
|
||||
"fixtures": [
|
||||
"F0"
|
||||
],
|
||||
"cases": [
|
||||
"R2-F03-R01"
|
||||
],
|
||||
"coverage": {
|
||||
"R2-F03-R01": "full for the components the tree builds (node, miner, pool, app, prove-host from one manifest, no unpublished vendor tree); the GPU workers are the kit's cross-build and read under the shipper's kit-isa line, not this cell"
|
||||
}
|
||||
},
|
||||
"harness:same-work": {
|
||||
"command": "tools/ci/p01-vectors.py --job-context <job-context.json> --phase 1|2|3 per reader (CUDA, OpenCL, Metal workers), igneum-miner --recheck-vectors (node), igneum-pow hash-bound (CPU reference), the pool's member-side re-check; docs/plans/igneum-2.0-same-work-test.md",
|
||||
"box_class": "release (the readers on their pods and boxes)",
|
||||
"fixtures": [
|
||||
"F0"
|
||||
],
|
||||
"cases": [
|
||||
"R2-F03-R02",
|
||||
"R2-F03-R03"
|
||||
],
|
||||
"coverage": {
|
||||
"R2-F03-R02": "partial until every reader has run: bit-for-bit agreement per nonce across node, CPU reference, CUDA, OpenCL, Metal and pool on one job context",
|
||||
"R2-F03-R03": "partial: the day-boundary transition in phase 2 (the one transition every live network crosses hourly at 60x); a class rotation is the same test on a research object until a v6 floor is set"
|
||||
}
|
||||
},
|
||||
"canary:fresh-install": {
|
||||
"command": "the rule 33 fresh-install canary on a release tip (the founder, 8 October 2026): install from the published artefact on a non-AVX-512 box with an empty datadir, sync genesis to tip, five minutes mining with zero refusals and an accepted block, one shard claimed, proved and paid or queued, a bounded quit, every line read back; the record tools/ci/canary/<sha>.json (one file per sha, an artefacts list: fleet, windows, mac, hive) read by tools/ci/canary-check.sh <sha> [--artefact kind], which publish-manifest.sh and publish-public.sh refuse without",
|
||||
"box_class": "release (a non-AVX-512 box with an empty datadir)",
|
||||
"fixtures": [
|
||||
"F0"
|
||||
],
|
||||
"cases": [
|
||||
"INT-07"
|
||||
],
|
||||
"coverage": {
|
||||
"INT-07": "partial: V6-12's clean-install half (one published object installed fresh, synced, mined, proved and paid on one host per artefact); the cross-host agreement half (node, pool, CPU verifier, each GPU host across activation) is harness:same-work's"
|
||||
}
|
||||
}
|
||||
},
|
||||
"not_run": {
|
||||
"GOV-02": "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",
|
||||
"GOV-04": "the evidence vault F9 (raw and negative evidence preserved) is the gate rule landing by 21:00: a PASS must carry its evidence file",
|
||||
"GOV-08": "the stale-evidence rule (evidence older than the manifest sha reads NOT RUN) is the gate rule landing by 21:00",
|
||||
"GPU-04": "the memory-clock ladder has no harness tonight: PC 1 mines nothing under the Devnet 3 off order",
|
||||
"GPU-04": "the memory-clock ladder has no harness tonight: the project's own rig mines nothing under the the earlier devnet off order",
|
||||
"GPU-06": "accepted work under ordinary connectivity needs the fault network F4",
|
||||
"GPU-07": "the sustained thermal and power soak has no harness tonight: PC 1 mines nothing under the Devnet 3 off order",
|
||||
"GPU-07": "the sustained thermal and power soak has no harness tonight: the project's own rig mines nothing under the the earlier devnet off order",
|
||||
"POW-05": "amortised cheap winning attempts are the attack lanes' grind and era harnesses (tools/attack/f7-era, f9-grind), not in the release matrix; their rows come from those lanes",
|
||||
"ADV-04": "selective participation needs the economic model F7 and a live chain window",
|
||||
"ADV-06": "process-advantage separation is the adversary lanes' chip study",
|
||||
"ADV-07": "lifetime rows are the adversary lanes' models",
|
||||
"ROT-03": "miner-voted bring-forward needs a vote harness on the fault network F4",
|
||||
"ROT-04": "seed-selection resistance is the census harness (the class v6 invention lane), not yet in the matrix",
|
||||
"EVM-01": "no EVM conformance-vector harness is mapped tonight; the exec suite does not run the reference test vectors",
|
||||
|
|
@ -687,7 +784,6 @@
|
|||
"INT-04": "INT-04 (R1 integration gate): the gate's harness is the owner lane's (pool lane (a1c484c48a62948c2)); not yet named in the map",
|
||||
"INT-05": "INT-05 (R1 integration gate): the gate's harness is the owner lane's (node lane (a283f5f0d364ceef0)); not yet named in the map",
|
||||
"INT-06": "INT-06 (R1 integration gate): the gate's harness is the owner lane's (node lane (a283f5f0d364ceef0)); not yet named in the map",
|
||||
"INT-07": "INT-07 (R1 integration gate): the gate's harness is the owner lane's (CI steward with the hash lane (a690540514aa453d7) and the worker lane (a9e87343f008e0edd)); not yet named in the map",
|
||||
"INT-08": "INT-08 (R1 integration gate): the gate's harness is the owner lane's (CI steward with the hash lane (a690540514aa453d7) and the worker lane (a9e87343f008e0edd)); not yet named in the map",
|
||||
"INT-09": "INT-09 (R1 integration gate): the gate's harness is the owner lane's (app lane); not yet named in the map",
|
||||
"INT-10": "INT-10 (R1 integration gate): the gate's harness is the owner lane's (CI steward with the hash lane (a690540514aa453d7) and the worker lane (a9e87343f008e0edd)); not yet named in the map",
|
||||
|
|
@ -706,9 +802,6 @@
|
|||
"R2-F02-R01": "R2-F02 (P0, the external review): the regression's harness is the owner lane's (proving lane, CI steward); not yet named in the map",
|
||||
"R2-F02-R02": "R2-F02 (P0, the external review): the regression's harness is the owner lane's (proving lane, CI steward); not yet named in the map",
|
||||
"R2-F02-R03": "R2-F02 (P0, the external review): the regression's harness is the owner lane's (proving lane, CI steward); not yet named in the map",
|
||||
"R2-F03-R01": "R2-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",
|
||||
"R2-F03-R02": "R2-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",
|
||||
"R2-F03-R03": "R2-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",
|
||||
"R2-F04-R01": "R2-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",
|
||||
"R2-F04-R02": "R2-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",
|
||||
"R2-F04-R03": "R2-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",
|
||||
|
|
|
|||
77
tools/fleet/box-dn3.sh
Normal file
77
tools/fleet/box-dn3.sh
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
#!/usr/bin/env bash
|
||||
# A Devnet 3 box (7 October 2026, the founder's clock: a fresh chain from genesis on 0.3.22, network igneum-devnet-3, every activation at 0,
|
||||
# NO override file). Modelled on box-dn2.sh. The node runs with NET_ARGS (default "--devnet --devnet-suffix=3": own handshake magic,
|
||||
# own data directory $F/$APPDIR; a live-devnet or Devnet 2 peer refuses it at the handshake), peered with SEED (comma list); one miner on
|
||||
# card 0 with the box's vote key; PROVER=1 adds the segment prover loop (CHAIN_NAME igneum-devnet-3). NODE_BIN names the igneumd.
|
||||
# FRESH=1 wipes $F/$APPDIR (a new genesis); UNSYNCED=1 adds --enable-unsynced-mining (the genesis boxes: a fresh chain's nodes start
|
||||
# unsynced and must mine anyway). RPC_PORT, P2P_PORT, EVM_PORT: other ports on a box whose live node holds 26610/26611/26790
|
||||
# (a standing box running a second node for Devnet 3: 36610/36611/36790). Nothing here touches the live devnet's node or miner.
|
||||
set -uo pipefail
|
||||
mkdir -p /root/fleet/pids; echo $$ > /root/fleet/pids/loop.pid # 15:3xZ 8 Oct 2026: kills by pid file only (main): loop.pid, node.pid, miner.pid under /root/fleet/pids
|
||||
F=/root/fleet; OUT=$F/out; mkdir -p $F/in $OUT $F/$APPDIR $F/mine/packs; exec >> $OUT/dn3.log 2>&1
|
||||
stamp() { date -u +%Y-%m-%dT%H:%M:%SZ; }
|
||||
LABEL="${LABEL:-dn3}"; WALLET="${WALLET:-0x1919191919191919191919191919191919191919}"; SEED="${SEED:-}"; NODE_BIN="${NODE_BIN:-$F/in/igneumd-0322}"
|
||||
APPDIR="${APPDIR:-dn3}"; PACK="${PACK:-dn3}"; CHAIN_NAME="${CHAIN_NAME:-igneum-devnet-3}" # 15:4xZ 8 Oct 2026: devnet-4 takes APPDIR=dn4 PACK=dn4 CHAIN_NAME=igneum-devnet-4 NET_ARGS="--devnet --devnet-suffix=4"
|
||||
PROVER="${PROVER:-0}"; UNSYNCED="${UNSYNCED:-}"; RPC_PORT="${RPC_PORT:-26610}"; P2P_PORT="${P2P_PORT:-26611}"; EVM_PORT="${EVM_PORT:-26790}"; JSON_PORT="${JSON_PORT:-$((RPC_PORT+2080))}"; MINE="${MINE:-1}"
|
||||
NET_ARGS="${NET_ARGS:---devnet --devnet-suffix=3}"; P2P_LISTEN="${P2P_LISTEN:-0.0.0.0:$P2P_PORT}"; MINER_ONLY="${MINER_ONLY:-0}"; ANNOUNCE="${ANNOUNCE:-}"; export -n MINER_ONLY # 21:3xZ 7 Oct 2026: never in the loop's environment (the puller restarts from it; MINER_ONLY=1 there would leave the node down at the next move) # MINER_ONLY=1: the node stays, only the miner loop restarts (with --announce ip:port when ANNOUNCE is set: the peer directory)
|
||||
JSONF="--rpclisten-json=127.0.0.1:$JSON_PORT"; case " $NET_ARGS ${EXTRA_ARGS:-} " in *rpclisten-json*) JSONF="";; esac # 13:3xZ 8 Oct: the pool boxes already carry it in NET_ARGS (dn3-pool-b: "cannot be used multiple times")
|
||||
MINER_BIN="${MINER_BIN:-$F/in/igneum-miner-0322}" # the 0.3.22 miner (sub-version 3 draw); the hive package's 0.3.20 miner is the live devnet's and never mines Devnet 3 (16:5xZ: every block BlockInvalid)
|
||||
[ -x "$NODE_BIN" ] || { echo "RESULT dn3_failed $(stamp) no node binary at $NODE_BIN"; exit 2; }
|
||||
[ "${MINE:-1}" = 1 ] && { [ -x "$MINER_BIN" ] || { echo "RESULT dn3_failed $(stamp) no 0.3.22 miner at $MINER_BIN"; exit 2; }; }
|
||||
# the pack-id gate (main through the shipper, 7 Oct 2026 17:0xZ): BEFORE the miner starts, the worker's pack and the node's engine must come
|
||||
# from the same igneum-pow pin. Tonight's shape (the shipper, 17:05Z): BY CONSTRUCTION, the pack the worker hashes is EXPORTED on this box by the
|
||||
# paired 0.3.22 miner (MINER_BIN's sha equals PAIR_MINER_SHA16, default 07246920fd9fe895 = igneum-pow 017e7037, the node's pin), never a copied kit;
|
||||
# a different miner sha or a pre-shipped pack directory is UNREADABLE and holds the miner. The id-equality read over RPC (a785001687d8688a against
|
||||
# the kit's id) replaces it from the next cut when the node lane names the method (NODE_ID_CMD / PACK_ID_CMD).
|
||||
pack_gate() {
|
||||
local msha want; msha=$(sha256sum "$MINER_BIN" | cut -c1-16); want="${PAIR_MINER_SHA16:-07246920fd9fe895 c29f33bbbd284a12 dfdc6883aa79a76f fb147dd1754cbfc0 cfa9f5ca382e0efc} $(cat $F/in/pair-miners.txt 2>/dev/null | tr '\n' ' ')" # 8 Oct 2026 09:4xZ: the list is also a FILE the puller appends every move's miner sha to (the 10:05 move: 18 miners refused for a sha only in a hand-edited default) # the two paired 0.3.22 miners (hands / node-lane builds, both igneum-pow 017e7037)
|
||||
if [ -n "${NODE_ID_CMD:-}" ] && [ -n "${PACK_ID_CMD:-}" ]; then
|
||||
local pid nid; pid=$(eval "$PACK_ID_CMD" 2>/dev/null); nid=$(eval "$NODE_ID_CMD" 2>/dev/null)
|
||||
[ -n "$pid" ] && [ -n "$nid" ] || { echo "RESULT dn3_pack_gate $(stamp) UNREADABLE pack_id=${pid:-none} node_id=${nid:-none}"; return 1; }
|
||||
[ "$pid" = "$nid" ] || { echo "RESULT dn3_pack_gate $(stamp) REFUSED pack_id=$pid node_id=$nid"; return 1; }
|
||||
echo "RESULT dn3_pack_gate $(stamp) PASS by id pack_id=$pid node_id=$nid"; return 0
|
||||
fi
|
||||
case " $want " in *" $msha "*) ;; *) echo "RESULT dn3_pack_gate $(stamp) UNREADABLE miner=$msha is not a paired miner ($want); a pre-shipped kit or another miner"; return 1;; esac
|
||||
[ -e $F/mine/packs/$PACK ] && { echo "RESULT dn3_pack_gate $(stamp) UNREADABLE packs/$PACK already present before this export (a copied kit?)"; return 1; }
|
||||
echo "RESULT dn3_pack_gate $(stamp) PASS by construction (miner $msha = pair, pack exported here by it, generator v4 sub-version 3)"; return 0
|
||||
}
|
||||
echo "RESULT dn3_start $(stamp) label=$LABEL node=$(sha256sum $NODE_BIN | cut -c1-16) miner=$(sha256sum $MINER_BIN 2>/dev/null | cut -c1-16) seed=${SEED:-none} prover=$PROVER mine=$MINE net=\"$NET_ARGS\" ports=$RPC_PORT/$P2P_PORT/$EVM_PORT"
|
||||
command -v curl >/dev/null || { apt-get update -qq >/dev/null 2>&1; apt-get install -y -qq curl ca-certificates python3 >/dev/null 2>&1; }
|
||||
if [ ! -x /opt/igneum/pkg/bin/igneum-miner ]; then
|
||||
read -r PKG_PATH PKG_SHA PKG_VER <<< "$(curl -fsSL -m 30 https://dl.igneum.network/dl/public/igneum-downloads.json | python3 -c 'import sys,json; d=json.load(sys.stdin)["files"]["miner-hive"]; print(d["path"], d["sha256"], d["version"])')"
|
||||
curl -fsSL -o $F/pkg.tgz "https://dl.igneum.network$PKG_PATH" && echo "$PKG_SHA $F/pkg.tgz" | sha256sum -c - >/dev/null && mkdir -p /opt/igneum/pkg && tar -C /opt/igneum/pkg --strip-components=1 -xzf $F/pkg.tgz || { echo "RESULT dn3_failed package"; exit 2; }
|
||||
fi
|
||||
B=/opt/igneum/pkg/bin
|
||||
# the Devnet 3 node and its miner only (anchored on the dn3 appdir and this RPC port), never the live node beside it
|
||||
# kills by pid file only (founder 8 Oct 2026, by construction: pkill and killall are shimmed to exit 97 on every box)
|
||||
pidkill() { local P; P=$(cat "$F/pids/$1.pid" 2>/dev/null); [ -n "$P" ] && kill -0 "$P" 2>/dev/null && { kill -TERM "$P" 2>/dev/null; sleep 2; kill -0 "$P" 2>/dev/null && kill -9 "$P" 2>/dev/null; }; return 0; }
|
||||
[ "$MINER_ONLY" = 1 ] || pidkill node; pidkill miner; sleep 2
|
||||
[ "${FRESH:-0}" = 1 ] && { rm -rf $F/$APPDIR; mkdir -p $F/$APPDIR; [ -s $F/$APPDIR-node.log ] && mv $F/$APPDIR-node.log $F/$APPDIR-node.prev.log; echo "RESULT dn3_fresh $(stamp) appdir wiped for the genesis"; }
|
||||
PEER=""; for sd in ${SEED//,/ }; do PEER="$PEER --addpeer=$sd"; done
|
||||
UNS=""; [ "$UNSYNCED" = 1 ] && UNS="--enable-unsynced-mining"
|
||||
if [ "$MINER_ONLY" != 1 ]; then
|
||||
echo "=== dn3 node start $(stamp) $(sha256sum $NODE_BIN | cut -c1-16)" >> $F/$APPDIR-node.log
|
||||
IGNEUM_PROOF_PROGRAM_IDS="${PROGRAM_IDS:-}" IGNEUM_PROOF_VERIFIER="${HOST_VERIFIER:-}" setsid nohup $NODE_BIN $NET_ARGS --appdir=$F/$APPDIR --rpclisten=0.0.0.0:$RPC_PORT --evm-rpclisten=127.0.0.1:$EVM_PORT $JSONF --listen=$P2P_LISTEN $PEER $UNS --unsaferpc --nodnsseed --disable-upnp --nologfiles --yes </dev/null >> $F/$APPDIR-node.log 2>&1 &
|
||||
echo $! > $F/pids/node.pid
|
||||
sleep 10
|
||||
echo "RESULT dn3_node $(stamp) pid=$(pgrep -f '[/]root/fleet/in/igneumd-[0-9a-f]+ .* --appdir=/root/fleet/$APPDIR ' | head -1) version=$(grep -oE 'igneumd/[0-9]+\.[0-9]+\.[0-9]+-[0-9a-f]+' $F/$APPDIR-node.log | tail -1) digest=$(grep -o 'digest: [0-9a-f]*' $F/$APPDIR-node.log | tail -1 | awk '{print substr($2,1,16)}') network=$(grep -oiE 'igneum-devnet-[0-9][^ ,]*' $F/$APPDIR-node.log | head -1) genesis=$(grep -oiE 'genesis[^\n]{0,120}' $F/$APPDIR-node.log | grep -oE '[0-9a-f]{64}' | head -1 | cut -c1-16)"
|
||||
for i in $(seq 1 30); do w="$($B/igneum-miner watch 1 grpc://127.0.0.1:$RPC_PORT 2>/dev/null | grep -o 'blocks=[0-9]*.*synced=[a-z]*' | tail -1)"; [ -n "$w" ] && break; sleep 5; done
|
||||
echo "RESULT dn3_watch $(stamp) $(printf '%s' "$w" | sed -E 's/difficulty=[0-9.]* sink=[0-9a-f]* //')"
|
||||
fi
|
||||
rm -rf $F/mine/packs/$PACK.prev; [ -e $F/mine/packs/$PACK ] && mv $F/mine/packs/$PACK $F/mine/packs/$PACK.prev # the previous run's export, moved aside so the gate sees a clean slot
|
||||
if [ "$MINE" = 1 ] && ! pack_gate; then echo "RESULT dn3_miner_refused $(stamp) the pack-id gate refused the place; node runs, miner off"; MINE=0; fi
|
||||
if [ "$MINE" = 1 ]; then
|
||||
cd $F/mine && rm -rf packs/$PACK && $MINER_BIN export-pack grpc://127.0.0.1:$RPC_PORT packs/$PACK > $OUT/dn3-export-pack.log 2>&1
|
||||
( echo $BASHPID > $F/pids/miner-loop.pid; while :; do $MINER_BIN mine grpc://127.0.0.1:$RPC_PORT 1 100000000 "$LABEL" --exec-rpc http://127.0.0.1:$EVM_PORT --worker $WORKER_BIN --worker-args "--device 0 --pack packs/$PACK" --prepare-packs packs/$PACK-prepare --exit-on-seed-change ${ANNOUNCE:+--announce $ANNOUNCE} --evm-address "$WALLET" --payout-label "$LABEL" --status-secs 30 >> $OUT/dn3-miner.log 2>&1 & echo $! > $F/pids/miner.pid; wait $(cat $F/pids/miner.pid); sleep 5; done ) </dev/null >/dev/null 2>&1 &
|
||||
echo "RESULT dn3_miner_started $(stamp) miner=$(sha256sum $MINER_BIN | cut -c1-16) announce=${ANNOUNCE:-none}"
|
||||
# the engine's program id as the paired miner prints it ("class v4 program id <16 hex>", the node lane 17:0xZ): a785001687d8688a on sub-version 3,
|
||||
# 1a4230699a6b9c60 is the 0.3.20 kit (known-failed); logged as the gate's id line, refused on the known-failed value
|
||||
# the worker form prints no id (the node lane, main.rs 1471): a second one-thread CPU miner beside the worker for 20 s prints "class v4 program id <16 hex>"
|
||||
timeout 60 $MINER_BIN mine grpc://127.0.0.1:$RPC_PORT 1 20 idread --engine igneum-pow --no-vote --stall-secs 0 > $OUT/dn3-idread.log 2>&1 </dev/null
|
||||
pidl=$(grep -oE 'program id [0-9a-f]{16}' $OUT/dn3-idread.log | tail -n 1 | awk '{print $3}')
|
||||
if [ "$pidl" = "1a4230699a6b9c60" ] || [ "$pidl" = "a785001687d8688a" ]; then pidkill miner; echo "RESULT dn3_program_id $(stamp) REFUSED $pidl is the shared devnet's id (edc4fa84 seeds), not Devnet 3's; miner stopped"; else echo "RESULT dn3_program_id $(stamp) ${pidl:-unread} (want ${WANT_PROGRAM_ID:-fce15bf61030be57}, the epoch-0 id of genesis 4020cb43; the id changes with the epoch seed every 3,600 DAA)"; fi
|
||||
fi
|
||||
if [ "$PROVER" = 1 ] && [ -x /opt/igneum-floor/bin/igneum-prove-host ]; then
|
||||
cd $F && LABEL="$LABEL" WALLET="$WALLET" EXPORT_FROM=0 CHAIN_NAME=$CHAIN_NAME MINER=none RUN_HOURS=48 EVM="http://127.0.0.1:$EVM_PORT" GRPC="grpc://127.0.0.1:$RPC_PORT" setsid nohup python3 -u $F/in/box-prover.py </dev/null >> $OUT/dn3-prover-launch.log 2>&1 &
|
||||
echo "RESULT dn3_prover_started $(stamp)"
|
||||
fi
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
. /root/fleet/in/pidkill.sh # kills by pid only (founder 8 Oct 2026)
|
||||
# Ember Tune's two-knob ladder on a rented NVIDIA card (docs/plans/ember-tune.md, branch ember-tune): the miner runs
|
||||
# throughout; the power ladder 100, 90, 80, 70, 60, 50% of the default limit at the unlocked clock (clamped at the
|
||||
# card's reported minimum), then the clock ladder 90, 80, 70, 60% of the maximum graphics clock at the power the
|
||||
|
|
@ -15,7 +16,7 @@ stamp() { date -u +%Y-%m-%dT%H:%M:%SZ; }
|
|||
say() { echo "$(stamp) $*"; }
|
||||
q() { nvidia-smi --query-gpu="$1" --format=csv,noheader,nounits -i 0 2>/dev/null | head -1 | tr -d ' '; }
|
||||
NAME="$(q name)"; DRV="$(q driver_version)"; PDEF="$(q power.default_limit)"; PMIN="$(q power.min_limit)"; PMAX="$(q power.max_limit)"; CMAX="$(q clocks.max.graphics)"
|
||||
pkill -x sp1-gpu-server 2>/dev/null; rm -f /tmp/sp1-cuda-*.sock
|
||||
. /root/fleet/in/pidkill.sh; kill_sock_owner 'sp1-cuda-[0-9]*\.sock'
|
||||
echo "RESULT start $(stamp) card=$NAME driver=$DRV power_default_w=$PDEF min_w=$PMIN max_w=$PMAX clock_max_mhz=$CMAX"
|
||||
# can we set anything?
|
||||
nvidia-smi -i 0 -pl "$PDEF" >/dev/null 2>&1 && PL_OK=1 || PL_OK=0
|
||||
|
|
@ -27,7 +28,7 @@ rm -rf packs/devnet; $B/igneum-miner export-pack grpc://127.0.0.1:26610 packs/de
|
|||
nohup $B/igneum-miner mine grpc://127.0.0.1:26610 1 100000000 "$LABEL" --worker $B/igneum-worker-cuda --worker-args "--device 0 --pack packs/devnet" \
|
||||
--prepare-packs packs/prepare --exit-on-seed-change --evm-address "$WALLET" --payout-label "$LABEL" --status-secs 10 > $OUT/ember-miner.log 2>&1 &
|
||||
MPID=$!; cd $F
|
||||
cleanup() { nvidia-smi -i 0 -rgc >/dev/null 2>&1; [ "$PL_OK" = 1 ] && nvidia-smi -i 0 -pl "$PDEF" >/dev/null 2>&1; kill $MPID 2>/dev/null; pkill -f '[/]opt/igneum/pkg/bin/igneum-worker-cuda' 2>/dev/null; }
|
||||
cleanup() { nvidia-smi -i 0 -rgc >/dev/null 2>&1; [ "$PL_OK" = 1 ] && nvidia-smi -i 0 -pl "$PDEF" >/dev/null 2>&1; kill_children $MPID; kill $MPID 2>/dev/null; }
|
||||
trap cleanup EXIT
|
||||
say "miner warming 90 s"; sleep 90
|
||||
STEPS=$OUT/ember-steps.jsonl; : > $STEPS
|
||||
|
|
@ -38,7 +39,7 @@ step() { # <label> <power_pct> <limit_w> <clock_cap_mhz or 0>
|
|||
sleep "$SETTLE"
|
||||
local n0; n0="$(grep -c 'STATUS' $OUT/ember-miner.log)"
|
||||
( while :; do nvidia-smi --query-gpu=power.draw,clocks.sm,clocks.mem,temperature.gpu --format=csv,noheader,nounits -i 0 2>/dev/null; sleep 1; done ) > $OUT/ember-samp-$lab.csv & local sp=$!
|
||||
sleep "$HOLD"; pkill -P $sp 2>/dev/null; kill $sp 2>/dev/null; sleep 1; kill -9 $sp 2>/dev/null
|
||||
sleep "$HOLD"; kill_children $sp; kill $sp 2>/dev/null; sleep 1; kill -9 $sp 2>/dev/null
|
||||
local n1; n1="$(grep -c 'STATUS' $OUT/ember-miner.log)"
|
||||
local mhs; mhs="$(grep STATUS $OUT/ember-miner.log | grep -o ' now=[0-9.]*' | tail -n $((n1 - n0 > 0 ? n1 - n0 : 1)) | cut -d= -f2 | awk '{s+=$1; n++} END {if (n) printf "%.2f", s/n; else print 0}')"
|
||||
local w gclk mclk tmax; read -r w gclk mclk tmax <<< "$(awk -F', *' '{w+=$1; g+=$2; m+=$3; if ($4+0 > t) t=$4+0; n++} END {if (n) printf "%.1f %.0f %.0f %d", w/n, g/n, m/n, t; else print "0 0 0 0"}' $OUT/ember-samp-$lab.csv)"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
. /root/fleet/in/pidkill.sh # kills by pid only (founder 8 Oct 2026)
|
||||
# The prover-floor agent's known-failed case (6 October 2026, 13:05Z): rebuild sp1-gpu-server from patch v5 (the panic
|
||||
# hook that turns a failed device allocation into "FLOOR abort ..." and exit 70) and re-run the 2^27 compressed point
|
||||
# alone on a card it cannot fit; report the host's failing line, the server's FLOOR abort line and the seconds.
|
||||
|
|
@ -19,10 +20,10 @@ echo "RESULT v5_build_exit $rc time_s=$(( $(date +%s) - t0 ))"
|
|||
[ $rc -eq 0 ] || { grep -n -A6 '^error' $FLOOR/logs/build-server-v5.log | head -30; echo "RESULT v5_failed build"; exit 2; }
|
||||
mkdir -p $FLOOR/home-v5/.sp1/bin && cp $FLOOR/target/release/sp1-gpu-server $FLOOR/home-v5/.sp1/bin/ && chmod +x $FLOOR/home-v5/.sp1/bin/sp1-gpu-server
|
||||
echo "RESULT v5_server sha256=$(sha256sum $FLOOR/home-v5/.sp1/bin/sp1-gpu-server | cut -c1-16) bytes=$(stat -c %s $FLOOR/home-v5/.sp1/bin/sp1-gpu-server)"
|
||||
pkill -9 -x sp1-gpu-server; rm -f /tmp/sp1-cuda-*.sock; sleep 2
|
||||
kill_sock_owner 'sp1-cuda-[0-9]*\.sock'; sleep 2
|
||||
t1=$(date +%s)
|
||||
env HOME=$FLOOR/home-v5 SP1_PROVER=cuda RUST_LOG=off SP1_GPU_FLOOR_LOG=1 SP1_GPU_ELEMENT_THRESHOLD=134217728 timeout 900 $HOST $FLOOR/prove/proving/fixtures/fees-v1-shards2.json --mode compressed --shard 0 --out $OUT/v5-point.json > $OUT/v5-point.log 2>&1; rc=$?
|
||||
echo "RESULT v5_point rc=$rc wall_s=$(( $(date +%s) - t1 ))"
|
||||
grep -n -E 'FLOOR abort|panicked|Error|error|RESULT' $OUT/v5-point.log | head -12 | cut -c1-300
|
||||
pkill -9 -x sp1-gpu-server; rm -f /tmp/sp1-cuda-*.sock
|
||||
kill_sock_owner 'sp1-cuda-[0-9]*\.sock'
|
||||
echo "RESULT v5_done $(stamp)"
|
||||
|
|
|
|||
|
|
@ -1,15 +1,11 @@
|
|||
#!/usr/bin/env bash
|
||||
# Stops every stage process on a box (the matrix, Ember, the prover loop, their miners, workers, samplers and SP1
|
||||
# servers); never the node. Run as a FILE (bash in/box-kill.sh) so that no pattern below can match the shell that runs
|
||||
# it (an inline `pkill -f '[i]gneum-prove-host'` killed its own ssh shell on 6 October 2026, 12:38Z).
|
||||
for i in 1 2; do
|
||||
pkill -9 -f '[b]ash in/box-matrix.sh' ; pkill -9 -f '[b]ash in/box-ember.sh'; pkill -9 -f '[b]ash in/box-prover.sh'; pkill -9 -f '[p]ython3 -u /root/fleet/in/box-prover.py'
|
||||
pkill -9 -x igneum-miner; pkill -9 -f '[/]opt/igneum/pkg/bin/igneum-worker-cuda'; pkill -9 -x sp1-gpu-server; pkill -9 -f '[/]opt/igneum-floor/(bin|target|prove)/.*igneum-prove-(host|export)' # pkill -x cannot match a name over 15 characters (igneum-worker-cuda, igneum-prove-host)
|
||||
pkill -9 -f '[n]vidia-smi --query'
|
||||
sleep 2
|
||||
done
|
||||
rm -f /tmp/sp1-cuda-*.sock
|
||||
# Stops every stage process on a box through pid files only (the founder, 8 Oct 2026: pkill and killall exit 97 on every box);
|
||||
# never the node. The stage scripts write their pids under /root/fleet/pids (matrix, ember, prover, prover-loop, miner-loop, miner,
|
||||
# sampler, sp1-server); a child the scripts spawned is reached through its parent's pid (kill_children walks by parent pid).
|
||||
. /root/fleet/in/pidkill.sh
|
||||
for n in matrix ember prover-loop prover miner-loop miner sampler; do p=$(cat /root/fleet/pids/$n.pid 2>/dev/null); [ -n "$p" ] && kill_children "$p" -9; kill_pidfile $n -9 0; done
|
||||
kill_sock_owner 'sp1-cuda-[0-9]*\.sock'
|
||||
nvidia-smi -i 0 -rgc >/dev/null 2>&1
|
||||
d="$(nvidia-smi --query-gpu=power.default_limit --format=csv,noheader,nounits -i 0 | tr -d ' ' | cut -d. -f1)"; [ -n "$d" ] && nvidia-smi -i 0 -pl "$d" >/dev/null 2>&1
|
||||
cd /root/fleet/out 2>/dev/null && for f in matrix.log ember.log prover.log prover-launch.log rows.jsonl; do [ -f "$f" ] && mv "$f" "$f.$(date +%s).old"; done
|
||||
echo "count=$(pgrep -c -f '[b]ash in/box-|^python3 -u /root/fleet/in/box-prover')+$(pgrep -c -x igneum-miner)+$(pgrep -c -f '[/]opt/igneum/pkg/bin/igneum-worker-cuda')+$(pgrep -c -x sp1-gpu-server)+$(pgrep -c -f '[/]opt/igneum-floor/(bin|target|prove)/.*igneum-prove-host') mem=$(nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits -i 0 | tr -d ' ')"
|
||||
echo "stopped by pid files: $(ls /root/fleet/pids 2>/dev/null | tr '\n' ' ') mem=$(nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits -i 0 2>/dev/null)"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
. /root/fleet/in/pidkill.sh # kills by pid only (founder 8 Oct 2026)
|
||||
# Phase 1 on one rented card: the memory matrix of docs/analysis/prover-floor.md on the real card. Every point is one
|
||||
# igneum-prove-host run against a fresh sp1-gpu-server (killed and its socket unlinked around every point), with an
|
||||
# nvidia-smi sampler at 1 s (memory.used, power.draw, utilization); peak = max memory.used, base = the reading just
|
||||
|
|
@ -29,10 +30,10 @@ SAMP=""
|
|||
# one nvidia-smi call a second in a loop: `nvidia-smi -l 1` buffers its file output in 4 KB chunks and ignored SIGTERM
|
||||
# on the 3080 box (12:19Z), which hung the first matrix in sampler_stop
|
||||
sampler_start() { ( while :; do nvidia-smi --query-gpu=timestamp,memory.used,power.draw,utilization.gpu --format=csv,noheader,nounits -i 0 2>/dev/null; sleep 1; done ) > "$1" & SAMP=$!; }
|
||||
sampler_stop() { [ -n "$SAMP" ] && { pkill -P $SAMP 2>/dev/null; kill $SAMP 2>/dev/null; sleep 1; kill -9 $SAMP 2>/dev/null; }; SAMP=""; }
|
||||
sampler_stop() { [ -n "$SAMP" ] && { kill_children $SAMP; kill $SAMP 2>/dev/null; sleep 1; kill -9 $SAMP 2>/dev/null; }; SAMP=""; }
|
||||
peak_of() { awk -F', *' 'NR>0 {if ($2+0 > m) m=$2+0} END {print m+0}' "$1"; }
|
||||
mean_col() { awk -F', *' -v c="$2" '{s+=$c; n++} END {if (n) printf "%.1f", s/n; else print 0}' "$1"; }
|
||||
kill_server() { pkill -x sp1-gpu-server 2>/dev/null; sleep 2; pkill -9 -x sp1-gpu-server 2>/dev/null; rm -f /tmp/sp1-cuda-*.sock; }
|
||||
kill_server() { kill_sock_owner 'sp1-cuda-[0-9]*\.sock'; }
|
||||
idle_mib() { sleep 3; q memory.used; }
|
||||
|
||||
# 1. idle
|
||||
|
|
@ -55,7 +56,7 @@ miner_start() {
|
|||
--prepare-packs packs/prepare --exit-on-seed-change --evm-address "$WALLET" --payout-label "$LABEL" --status-secs 10 > $OUT/miner.log 2>&1 &
|
||||
MPID=$!; cd $F
|
||||
}
|
||||
miner_stop() { [ -n "$MPID" ] && { kill $MPID 2>/dev/null; sleep 2; kill -9 $MPID 2>/dev/null; }; pkill -f '[/]opt/igneum/pkg/bin/igneum-worker-cuda' 2>/dev/null; pkill -x igneum-miner 2>/dev/null; MPID=""; sleep 3; }
|
||||
miner_stop() { [ -n "$MPID" ] && { kill_children $MPID; kill $MPID 2>/dev/null; sleep 2; kill -9 $MPID 2>/dev/null; }; MPID=""; sleep 3; }
|
||||
miner_rate() { # mean of the STATUS now= values in the last N lines
|
||||
grep STATUS $OUT/miner.log | grep -o ' now=[0-9.]*' | tail -n "${1:-10}" | cut -d= -f2 | awk '{s+=$1; n++} END {if (n) printf "%.2f", s/n; else print 0}'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -190,7 +190,18 @@ def fnv1a(s):
|
|||
return h
|
||||
def kill_server():
|
||||
if CARD: subprocess.run(f"rm -f /tmp/sp1-cuda-{DEV}.sock", shell=True) # a rig: never another card's server
|
||||
else: subprocess.run("pkill -x sp1-gpu-server; sleep 1; rm -f /tmp/sp1-cuda-*.sock", shell=True)
|
||||
else:
|
||||
# kills by pid only (founder 8 Oct 2026, by construction): the server is found through the pid that owns its unix socket
|
||||
# (ss -xlp on /tmp/sp1-cuda-*.sock), written to /root/fleet/pids/sp1-server.pid, then signalled by that pid; never by name
|
||||
o=subprocess.run("ss -xlp 2>/dev/null | grep -E '/tmp/sp1-cuda-[0-9]*\\.sock' | grep -oE 'pid=[0-9]+' | cut -d= -f2 | sort -u", shell=True, capture_output=True, text=True).stdout.split()
|
||||
os.makedirs("/root/fleet/pids", exist_ok=True)
|
||||
for s in o:
|
||||
try:
|
||||
pid=int(s); open("/root/fleet/pids/sp1-server.pid","w").write(f"{pid}\n"); os.kill(pid, 15); time.sleep(1)
|
||||
try: os.kill(pid, 9)
|
||||
except ProcessLookupError: pass
|
||||
except Exception: pass
|
||||
subprocess.run("rm -f /tmp/sp1-cuda-*.sock", shell=True)
|
||||
MPROC = None
|
||||
def miner_start():
|
||||
global MPROC
|
||||
|
|
@ -204,7 +215,7 @@ def miner_start():
|
|||
def miner_stop():
|
||||
global MPROC
|
||||
if MPROC: MPROC.terminate(); time.sleep(2); MPROC.kill(); MPROC = None
|
||||
subprocess.run(f"pkill -f '[/]opt/igneum/pkg/bin/igneum-worker-cuda --device {DEV} '", shell=True)
|
||||
pass # the worker is the miner's child; MPROC.terminate() above ends it (no kill by name: founder 8 Oct 2026)
|
||||
def miner_rate(n=6):
|
||||
try:
|
||||
vals = [float(l.split(" now=")[1].split()[0]) for l in open(f"{OUT}/prover-miner.log").read().split("\n") if "STATUS" in l and " now=" in l][-n:]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
. /root/fleet/in/pidkill.sh # kills by pid only (founder 8 Oct 2026)
|
||||
# Phase 3 on an 8-card rig (RunPod pod, a container: no systemd, so the rig installer's unit steps are exercised with
|
||||
# --preflight-only and --dry-run and the units' own scripts are run by hand). After box-setup.sh (node, workers, the
|
||||
# patched server for the rig's arch, the cuda host):
|
||||
|
|
@ -35,7 +36,7 @@ for d in $(seq 0 $((N-1))); do
|
|||
done
|
||||
cd $F; sleep 60
|
||||
( while :; do nvidia-smi --query-gpu=index,power.draw,memory.used,utilization.gpu --format=csv,noheader,nounits; sleep 1; done ) > $OUT/rig-samp-mine.csv & SP=$!
|
||||
sleep "$MINE_SECS"; pkill -P $SP; kill $SP 2>/dev/null
|
||||
sleep "$MINE_SECS"; kill_children $SP; kill $SP 2>/dev/null
|
||||
sum=0
|
||||
for d in $(seq 0 $((N-1))); do
|
||||
r=$(grep STATUS $OUT/rig-miner-$d.log | grep -o ' now=[0-9.]*' | tail -n 10 | cut -d= -f2 | awk '{s+=$1; n++} END {if (n) printf "%.2f", s/n; else print 0}')
|
||||
|
|
@ -43,10 +44,10 @@ for d in $(seq 0 $((N-1))); do
|
|||
echo "RESULT miner card=$d mhs=$r watts=$w"; sum=$(awk -v a=$sum -v b=$r 'BEGIN {print a+b}')
|
||||
done
|
||||
echo "RESULT rig_miners $(stamp) cards=$N sum_mhs=$sum watts=$(awk -F', *' '{s+=$2; n++} END {printf "%.0f", s/n*'$N'}' $OUT/rig-samp-mine.csv)"
|
||||
for p in "${pids[@]}"; do kill $p 2>/dev/null; done; pkill -f '[/]opt/igneum/pkg/bin/igneum-worker-cuda'; sleep 3
|
||||
for p in "${pids[@]}"; do kill_children $p; kill $p 2>/dev/null; done; sleep 3
|
||||
fi
|
||||
# C + D. seven core-only provers and one compressing card, in parallel
|
||||
pkill -9 -x sp1-gpu-server; rm -f /tmp/sp1-cuda-*.sock
|
||||
kill_sock_owner 'sp1-cuda-[0-9]*\.sock'
|
||||
prove_loop() { # <device> <mode> <threshold> <seconds>
|
||||
local d=$1 mode=$2 thr=$3 secs=$4 t0=$(date +%s) n=0 tot=0
|
||||
while [ $(( $(date +%s) - t0 )) -lt $secs ]; do
|
||||
|
|
@ -63,9 +64,9 @@ lp=()
|
|||
for d in $(seq 0 $((N-2))); do prove_loop $d core 33554432 "$PROVE_SECS" & lp+=($!); done
|
||||
prove_loop $((N-1)) compressed 67108864 "$PROVE_SECS" & lp+=($!)
|
||||
for p in "${lp[@]}"; do wait $p; done
|
||||
pkill -P $SP; kill $SP 2>/dev/null
|
||||
kill_children $SP; kill $SP 2>/dev/null
|
||||
echo "RESULT rig_prove $(stamp) host_ram_used_mb_max=$(awk '{if ($2>m) m=$2} END {print m}' $OUT/rig-samp-prove.csv) core_cards=$((N-1)) core_per_min=$(grep 'RESULT prove_loop' $OUT/rig.log | grep 'mode=core' | grep -o 'per_min=[0-9.]*' | cut -d= -f2 | awk '{s+=$1} END {printf "%.2f", s}') compressed_per_min=$(grep 'RESULT prove_loop' $OUT/rig.log | grep 'mode=compressed' | grep -o 'per_min=[0-9.]*' | cut -d= -f2)"
|
||||
pkill -9 -x sp1-gpu-server; rm -f /tmp/sp1-cuda-*.sock
|
||||
kill_sock_owner 'sp1-cuda-[0-9]*\.sock'
|
||||
# E. the hand-off cost by file
|
||||
cf=$(ls -S $F/mine/*.bin $OUT/*.bin 2>/dev/null | head -1); [ -z "$cf" ] && cf=$(find / -name 'block-*-core.bin' -size +1M 2>/dev/null | head -1)
|
||||
if [ -n "$cf" ]; then
|
||||
|
|
@ -80,5 +81,5 @@ t0=$(date +%s)
|
|||
env HOME=$FLOOR/home IGNEUM_CUDA_DEVICE=$((N-1)) SP1_PROVER=cuda RUST_LOG=off timeout 1800 $HOST --mode chain --chain "$list" --prover "$WALLET" --save-shards --out $OUT/rig-chain.json > $OUT/rig-chain.log 2>&1; rc=$?
|
||||
echo "RESULT chain rc=$rc wall_s=$(( $(date +%s) - t0 )) blocks=$(echo "$list" | tr ',' '\n' | wc -l) $(grep -E '^RESULT chain' $OUT/rig-chain.log | tail -1 | cut -c1-200)"
|
||||
else echo "RESULT chain skipped: no consecutive live fixtures yet (needs the exec layer)"; fi
|
||||
pkill -9 -x sp1-gpu-server; rm -f /tmp/sp1-cuda-*.sock
|
||||
kill_sock_owner 'sp1-cuda-[0-9]*\.sock'
|
||||
echo "RESULT rig_done $(stamp)"
|
||||
|
|
|
|||
3
tools/fleet/dn3-kill.sh
Normal file
3
tools/fleet/dn3-kill.sh
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/env bash
|
||||
# kills by pid file only (founder 8 Oct 2026): the old name-pattern kills are gone; fleet-stop.sh all stops node, miner and loop through /root/fleet/pids.
|
||||
bash /root/fleet/in/fleet-stop.sh all
|
||||
111
tools/fleet/fleet-puller.sh
Normal file
111
tools/fleet/fleet-puller.sh
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
#!/usr/bin/env bash
|
||||
# Igneum fleet puller (main's order 21:5x BST 7 Oct 2026): the box fetches a SIGNED move file from the fleet file host on a
|
||||
# one-minute timer, verifies the signature against the fleet key this box already trusts (its authorized_keys), downloads the
|
||||
# named binary pair, checks every sha, and at the named minute (UTC epoch) restarts its Devnet 3 node and miner together from
|
||||
# the running box-dn3.sh's own environment, then reads the version string and digest back and posts the line to the intake.
|
||||
# A proxy outage never blocks a digest-moving release again: nothing here needs ssh. Env: BASE (file host prefix), LABEL,
|
||||
# INTAKE_URL + INTAKE_KEY (the report-only intake key that every miner package carries). State under /root/fleet/move.
|
||||
set -u
|
||||
F=/root/fleet; M=$F/move; mkdir -p $M; cd $M
|
||||
LABEL="${LABEL:?}"; BASE="${BASE:?}"; INTAKE_URL="${INTAKE_URL:-}"; INTAKE_KEY="${INTAKE_KEY:-}"
|
||||
stamp(){ date -u +%Y-%m-%dT%H:%M:%SZ; }
|
||||
log(){ echo "$(stamp) $*" >> $M/puller.log; }
|
||||
post(){ # post "<title>" "<body>"
|
||||
echo "$(stamp) $1 | $2" >> $M/readback.log
|
||||
[ -n "$INTAKE_URL" ] && [ -n "$INTAKE_KEY" ] && python3 - "$1" "$2" "$LABEL" "$INTAKE_URL" "$INTAKE_KEY" <<'PY' >/dev/null 2>&1
|
||||
import sys, json, urllib.request
|
||||
t,b,l,u,k=sys.argv[1:]
|
||||
req=urllib.request.Request(u, data=json.dumps({"from":"fleet:"+l,"kind":"note","title":t[:300],"body":b[:4000]}).encode(), headers={"Content-Type":"application/json","x-igneum-key":k}, method="POST")
|
||||
try: urllib.request.urlopen(req, timeout=20).read()
|
||||
except Exception as e: print(e)
|
||||
PY
|
||||
true; }
|
||||
awk '{print "igneum-fleet-move " $1, $2}' /root/.ssh/authorized_keys > $M/allowed_signers
|
||||
jq_(){ python3 -c 'import sys,json; d=json.load(open(sys.argv[1])); v=d
|
||||
for k in sys.argv[2].split("."): v=v[k] if isinstance(v,dict) else v[int(k)]
|
||||
print(v if not isinstance(v,(list,dict)) else json.dumps(v))' "$@" 2>/dev/null; }
|
||||
log "puller start label=$LABEL base=$BASE"
|
||||
while :; do
|
||||
if curl -fsS -m 25 -o $M/move.json.tmp "$BASE/move.json" && curl -fsS -m 25 -o $M/move.json.sig.tmp "$BASE/move.json.sig"; then
|
||||
if ssh-keygen -Y verify -f $M/allowed_signers -I igneum-fleet-move -n igneum-fleet-move -s $M/move.json.sig.tmp < $M/move.json.tmp >/dev/null 2>&1; then
|
||||
mv -f $M/move.json.tmp $M/move.json; mv -f $M/move.json.sig.tmp $M/move.json.sig
|
||||
else log "BAD SIGNATURE on move.json, ignored"; rm -f $M/move.json.tmp $M/move.json.sig.tmp; sleep 60; continue; fi
|
||||
else [ -f $M/move.json ] || { sleep 60; continue; }; fi
|
||||
id=$(jq_ $M/move.json id); at=$(jq_ $M/move.json at_epoch); [ -n "$id" ] || { log "move.json without id"; sleep 60; continue; }
|
||||
# a staggered move (8 Oct 2026, the 0.3.25 re-execution from genesis): "delays": {"<label>": <seconds>} adds to at_epoch for that box; absent = 0
|
||||
dly=$(jq_ $M/move.json delays.$LABEL 2>/dev/null); case "$dly" in ''|*[!0-9]*) dly=0;; esac
|
||||
if [ "${at:-0}" -gt 0 ] 2>/dev/null; then at=$((at+dly)); fi
|
||||
if [ -e $M/applied-$id ]; then sleep 60; continue; fi
|
||||
pair=hands; for l in $(jq_ $M/move.json node_lane_labels | tr -d '[]",'); do [ "$l" = "$LABEL" ] && pair=node_lane; done
|
||||
url=$(jq_ $M/move.json pairs.$pair.url); tsha=$(jq_ $M/move.json pairs.$pair.sha); dsha=$(jq_ $M/move.json pairs.$pair.igneumd_sha); msha=$(jq_ $M/move.json pairs.$pair.miner_sha)
|
||||
if [ ! -e $M/fetched-$id ]; then
|
||||
mkdir -p $M/$id && curl -fsS -m 600 -o $M/$id/pair.tgz "$url" || { log "fetch failed $url"; sleep 60; continue; }
|
||||
echo "$tsha $M/$id/pair.tgz" | sha256sum -c - >/dev/null 2>&1 || { log "TARBALL SHA MISMATCH $id"; rm -f $M/$id/pair.tgz; sleep 60; continue; }
|
||||
tar -xzf $M/$id/pair.tgz -C $M/$id && [ "$(sha256sum $M/$id/igneumd | cut -c1-64)" = "$dsha" ] && [ "$(sha256sum $M/$id/igneum-miner | cut -c1-64)" = "$msha" ] || { log "BINARY SHA MISMATCH $id"; rm -rf $M/$id; sleep 60; continue; }
|
||||
chmod +x $M/$id/igneumd $M/$id/igneum-miner; touch $M/fetched-$id
|
||||
# the pack gate's pair list by file: this move's miner sha (16 hex) appended once, so box-dn3.sh accepts the pair at the minute without a hand edit
|
||||
grep -qx "${msha:0:16}" $F/in/pair-miners.txt 2>/dev/null || echo "${msha:0:16}" >> $F/in/pair-miners.txt
|
||||
post "FLEET MOVE $id FETCHED $LABEL" "pair=$pair igneumd=${dsha:0:16} miner=${msha:0:16} at_epoch=$at"
|
||||
log "fetched $id pair=$pair"
|
||||
fi
|
||||
p0=$(pgrep -of '[b]ash in/box-dn3.sh'); [ -n "$p0" ] && tr '\0' '\n' < /proc/$p0/environ | grep -E '^[A-Z_][A-Z0-9_]*=' | grep -vE '^(PWD|OLDPWD|SHLVL|_|TERM|SHELL|LS_COLORS|HOSTNAME|SSH_[A-Z_]+|LANG|LC_[A-Z_]+|LOGNAME|USER|MAIL|MINER_ONLY)=' > $M/env-last.tmp && [ -s $M/env-last.tmp ] && mv -f $M/env-last.tmp $M/env-last
|
||||
now=$(date +%s)
|
||||
if [ "${at:-0}" -le 0 ] 2>/dev/null; then sleep 60; continue; fi
|
||||
if [ "$now" -lt "$at" ]; then d=$((at-now)); [ $d -gt 60 ] && d=60; sleep $d; continue; fi
|
||||
# 12:1xZ 8 Oct 2026 (shipper): a box still re-walking at the minute waits for its own restart until its state reads right. move.json may carry
|
||||
# "require_root": {"height":"0x6687","root":"0x3f53a7b9"}: the box's own EVM (EVM_PORT from env-last, default 26790) must answer that root
|
||||
# at that height before this box applies; otherwise HELD (posted once every 10 minutes) and re-checked each minute. No EVM answer = held too.
|
||||
# 15:0xZ 8 Oct 2026 (node lane, the acaf08b0 move): "require_replay_done": true holds a box whose executor has not reached its sink since
|
||||
# the node's last start (the log's last of "replaying from genesis"/"no snapshot to resume" vs "records up to the tip N are continuous")
|
||||
if [ "$(jq_ $M/move.json require_replay_done)" = "True" ] || [ "$(jq_ $M/move.json require_replay_done)" = "true" ]; then
|
||||
last=$(grep -anE 'replaying from genesis|no snapshot to resume from|records up to the tip [0-9]+ are continuous' $F/dn3-node.log 2>/dev/null | tail -n 1)
|
||||
case "$last" in *"are continuous"*) ;; *) hl=$M/held-replay-$id-stamp; if [ ! -f $hl ] || [ $(( $(date +%s) - $(stat -c %Y $hl) )) -ge 900 ]; then post "FLEET MOVE $id HELD $LABEL" "replay not done: $(echo "$last" | cut -c1-120)"; touch $hl; fi; sleep 60; continue;; esac
|
||||
fi
|
||||
rh=$(jq_ $M/move.json require_root.height); rr=$(jq_ $M/move.json require_root.root)
|
||||
if [ -n "$rh" ] && [ -n "$rr" ]; then
|
||||
ep=$(grep -oE '^EVM_PORT=.*' $M/env-last 2>/dev/null | head -n 1 | cut -d= -f2 | tr -d "'\""); ep=${ep:-26790}
|
||||
blk=$(curl -s -m 8 -X POST -H 'content-type: application/json' --data "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_getBlockByNumber\",\"params\":[\"$rh\",false]}" http://127.0.0.1:$ep 2>/dev/null)
|
||||
got=$(echo "$blk" | grep -oE '"stateRoot":"0x[0-9a-f]+"' | cut -d'"' -f4); gh=$(echo "$blk" | grep -oE '"hash":"0x[0-9a-f]+"' | head -n 1 | cut -d'"' -f4); rhash=$(jq_ $M/move.json require_root.hash)
|
||||
if [ "${got:0:${#rr}}" != "$rr" ] || { [ -n "$rhash" ] && [ "${gh:2:${#rhash}}" != "$rhash" ]; }; then
|
||||
hl=$M/held-$id-stamp; if [ ! -f $hl ] || [ $(( $(date +%s) - $(stat -c %Y $hl) )) -ge 600 ]; then post "FLEET MOVE $id HELD $LABEL" "block $rh reads hash ${gh:2:8} root ${got:-none}, wanted ${rhash:-any}/$rr (re-walk not done); re-checked each minute"; touch $hl; fi
|
||||
sleep 60; continue
|
||||
fi
|
||||
fi
|
||||
# THE MINUTE: node and miner together, from the running box-dn3.sh's own environment
|
||||
pid=$(cat /root/fleet/pids/loop.pid 2>/dev/null); [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null || pid=$(pgrep -of '[b]ash in/box-dn3.sh')
|
||||
# the restart environment: the running box-dn3.sh's (never MINER_ONLY: a loop restarted alone carries it, and it would leave the node down), else env-last (written by every hand start since 21:4xZ 7 Oct 2026)
|
||||
if [ -n "$pid" ]; then tr '\0' '\n' < /proc/$pid/environ | grep -E '^[A-Z_][A-Z0-9_]*=' | grep -vE '^(PWD|OLDPWD|SHLVL|_|TERM|SHELL|LS_COLORS|HOSTNAME|SSH_[A-Z_]+|LANG|LC_[A-Z_]+|LOGNAME|USER|MAIL|MINER_ONLY)=' > $M/env-$id; else log "no running box-dn3.sh; using last env"; [ -f $M/env-last ] && grep -vE '^MINER_ONLY=' $M/env-last > $M/env-$id; fi
|
||||
[ -s $M/env-$id ] || { post "FLEET MOVE $id FAILED $LABEL" "no box-dn3.sh environment to restart from"; touch $M/applied-$id; sleep 60; continue; }
|
||||
cp $M/env-$id $M/env-last
|
||||
RPC=$(grep -E '^RPC_PORT=' $M/env-$id | cut -d= -f2); RPC=${RPC:-26610}
|
||||
bash $F/in/fleet-stop.sh all >/dev/null 2>&1; sleep 2
|
||||
true
|
||||
cd $F/in && NB=$(grep -oE '^NODE_BIN=.*' $M/env-last | cut -d= -f2 | tr -d "'\"" | xargs -n1 basename 2>/dev/null); NB=${NB:-igneumd-0322}; MB=$(grep -oE '^MINER_BIN=.*' $M/env-last | cut -d= -f2 | tr -d "'\"" | xargs -n1 basename 2>/dev/null); MB=${MB:-igneum-miner-0322}; mv -f $NB $NB.pre-$id 2>/dev/null; mv -f $MB $MB.pre-$id 2>/dev/null
|
||||
cp $M/$id/igneumd $NB && cp $M/$id/igneum-miner $MB && chmod +x $NB $MB; cd $M
|
||||
APPDIR_LOG=$(grep -oE '^APPDIR=.*' $M/env-last 2>/dev/null | head -n 1 | cut -d= -f2 | tr -d "'\""); APPDIR_LOG=${APPDIR_LOG:-dn3} # 17:1xZ 8 Oct: the marker and the read-back follow the env's APPDIR (devnet-4 logs to dn4-node.log; the 16:50Z APPLIED lines read version= digest= empty for this)
|
||||
mk="=== fleet move $id $(date -u +%T)"; echo "$mk" >> $F/$APPDIR_LOG-node.log
|
||||
# every value quoted before sourcing (09:05Z 8 Oct 2026: a bare NET_ARGS=--devnet --devnet-suffix=3 line ran "--devnet-suffix=3" as a command and nine boxes came up with no node)
|
||||
python3 - "$M/env-$id" <<'PY' > $M/env-$id.quoted
|
||||
import sys, shlex
|
||||
for line in open(sys.argv[1]):
|
||||
line=line.rstrip("\n")
|
||||
if "=" not in line or not line.split("=",1)[0].replace("_","").isalnum(): continue
|
||||
k,v=line.split("=",1); v=v.strip()
|
||||
if len(v)>=2 and v[0]==v[-1] and v[0] in "'\"": v=v[1:-1]
|
||||
print(f"{k}={shlex.quote(v)}")
|
||||
PY
|
||||
(cd $F && set -a && . $M/env-$id.quoted && set +a && setsid nohup bash in/box-dn3.sh </dev/null >/dev/null 2>&1 &)
|
||||
want_v=$(jq_ $M/move.json want_version); want_d=$(jq_ $M/move.json want_digest); rb=""; APPDIR_LOG=$(grep -oE '^APPDIR=.*' $M/env-last 2>/dev/null | head -n 1 | cut -d= -f2 | tr -d "'\""); APPDIR_LOG=${APPDIR_LOG:-dn3} # 16:0xZ 8 Oct: devnet-4 logs to dn4-node.log
|
||||
for i in $(seq 1 18); do sleep 10
|
||||
v=$(awk -v m="$mk" '$0==m{f=1} f' $F/${APPDIR_LOG:-dn3}-node.log | grep -oE 'igneumd/[0-9]+\.[0-9]+\.[0-9]+-[0-9a-f]+' | tail -n 1); d=$(awk -v m="$mk" '$0==m{f=1} f' $F/${APPDIR_LOG:-dn3}-node.log | grep -o 'digest: [0-9a-f]*' | tail -n 1 | cut -c9-24)
|
||||
w=$(/opt/igneum/pkg/bin/igneum-miner watch 1 grpc://127.0.0.1:$RPC 2>/dev/null | grep -oE 'blocks=[0-9]+|peers=[0-9]+|synced=[a-z]+' | tr '\n' ' ')
|
||||
[ -n "$v" ] && [ -n "$d" ] && case "$w" in *synced=true*) [[ "$w" != *peers=0* ]] && break;; esac
|
||||
done
|
||||
m=$(pgrep -fc "igneum-miner(-0322)? mine grpc://127.0.0.1:$RPC "); ok=MISMATCH; [ "$v" = "$want_v" ] && [ "$d" = "$want_d" ] && ok=MATCH
|
||||
fp=$(grep -oE 'igneum-pow fingerprint [0-9a-f]{16}[^ ]*' $F/dn3-node.log 2>/dev/null | tail -n 1); fpb=$(grep -aoE 'IGNEUM_POW_FINGERPRINT=[0-9a-f]{16}' $M/$id/igneumd 2>/dev/null | head -n 1); fp="${fp:-igneum-pow fingerprint none} binary ${fpb:-IGNEUM_POW_FINGERPRINT=none}" # 12:5xZ 8 Oct (shipper): the freeze's crate fingerprint from the node's start line; another value is a stop
|
||||
post "FLEET MOVE $id APPLIED $LABEL $ok" "version=$v digest=$d $w miner_procs=$m want=$want_v/$want_d rpc=$RPC ${fp:-igneum-pow fingerprint none}"
|
||||
touch $M/applied-$id; log "applied $id $ok $v $d $w"
|
||||
# 12:2xZ 8 Oct (node lane): bans persist in the node's DB; once this box's state at the reference block reads equal, unban every address it holds
|
||||
if [ -n "$rh" ] && [ -n "$rr" ]; then ( cd /root/fleet && EVM_PORT=$(grep -oE '^EVM_PORT=.*' $M/env-last | cut -d= -f2 | tr -d "'\"") RPC_PORT=$RPC setsid nohup bash in/unban-all.sh "$rh" "$rr" "$rhash" </dev/null >> $M/unban.out 2>&1 & ); fi
|
||||
sleep 60
|
||||
done
|
||||
13
tools/fleet/fleet-stop.sh
Executable file
13
tools/fleet/fleet-stop.sh
Executable file
|
|
@ -0,0 +1,13 @@
|
|||
#!/usr/bin/env bash
|
||||
# 15:3xZ 8 Oct 2026 (main: kills by pid file only, never by name): stops the box's Devnet 3 pieces by the pids box-dn3.sh wrote under
|
||||
# /root/fleet/pids: fleet-stop.sh miner | node | loop | all. The miner process itself is the miner-loop's child (read from the loop pid).
|
||||
P=/root/fleet/pids; what="${1:-all}"
|
||||
kp(){ f=$P/$1.pid; [ -s $f ] || return 0; pid=$(cat $f); kill -0 $pid 2>/dev/null || { rm -f $f; return 0; }; kill ${2:--TERM} $pid 2>/dev/null; sleep ${3:-1}; kill -0 $pid 2>/dev/null && kill -9 $pid 2>/dev/null; rm -f $f; echo "stopped $1 pid $pid"; }
|
||||
kids(){ [ -s $P/$1.pid ] && pgrep -P $(cat $P/$1.pid) 2>/dev/null; }
|
||||
case "$what" in
|
||||
loop) kp loop -9 0;;
|
||||
miner) for c in $(kids miner-loop); do kill -9 $c 2>/dev/null; for g in $(pgrep -P $c 2>/dev/null); do kill -9 $g 2>/dev/null; done; done; kp miner-loop -9 0;;
|
||||
node) kp node -TERM 6;;
|
||||
all) kp loop -9 0; for c in $(kids miner-loop); do kill -9 $c 2>/dev/null; for g in $(pgrep -P $c 2>/dev/null); do kill -9 $g 2>/dev/null; done; done; kp miner-loop -9 0; kp node -TERM 6;;
|
||||
esac
|
||||
echo "left: node=$(pgrep -fc '[a]ppdir=/root/fleet/dn') miners=$(pgrep -fc '[i]gneum-miner(-0322)? mine') loops=$(pgrep -fc '^bash in/box-dn3.sh')"
|
||||
3
tools/fleet/kill-node.sh
Normal file
3
tools/fleet/kill-node.sh
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/env bash
|
||||
# kill-node.sh <sha>: stops the box's node through its pid file only (founder 8 Oct 2026); the sha argument is kept for the callers' form and read back, not matched.
|
||||
. /root/fleet/in/pidkill.sh; kill_pidfile node -TERM 4; true
|
||||
|
|
@ -78,7 +78,7 @@ class Box:
|
|||
return f"{F}/in/{name}"
|
||||
# ---- node ----
|
||||
def stop_node(self):
|
||||
self.run(f"pkill -f '[/]root/fleet/in/igneumd|[/]opt/igneum/pkg/bin/igneumd'; sleep 3; pkill -9 -f '[/]root/fleet/in/igneumd|[/]opt/igneum/pkg/bin/igneumd' 2>/dev/null; true", 40)
|
||||
self.run(f"bash {F}/in/fleet-stop.sh node >/dev/null 2>&1; . {F}/in/pidkill.sh; kill_pidfile node -TERM 3; true", 40) # by pid file only (founder 8 Oct 2026)
|
||||
def start_node(self, binary, override_local, peers, devnet_suffix=None, verifier=None, extra=(), appdir=None, log=None, unsynced_mining=False):
|
||||
"""Writes the override file, kills the old node (anchored on its path), starts the new one; returns the digest it prints."""
|
||||
appdir = appdir or (f"{F}/dn2" if devnet_suffix else f"{F}/node"); log = log or (f"{F}/dn2-node.log" if devnet_suffix else f"{F}/node.log")
|
||||
|
|
@ -132,11 +132,11 @@ class Box:
|
|||
log = log or f"{F}/out/miner-{device}.log"
|
||||
self.check(f"cd {F}/mine 2>/dev/null || mkdir -p {F}/mine/packs && cd {F}/mine; [ -d packs/{pack} ] || {B}/igneum-miner export-pack {grpc} packs/{pack} >/dev/null 2>&1; setsid nohup {B}/igneum-miner mine {grpc} 1 100000000 {shlex.quote(label)} --worker {B}/igneum-worker-cuda --worker-args '--device {device} --pack packs/{pack}' --prepare-packs packs/prepare-{device} --exit-on-seed-change --evm-address {wallet} --payout-label {shlex.quote(label)} --status-secs 30 </dev/null >> {log} 2>&1 & echo $!", 90)
|
||||
return int(self.run("pgrep -x igneum-miner | tail -1", 20)[1].strip() or 0)
|
||||
def stop_miners(self): self.run("pkill -x igneum-miner; pkill -f '[/]opt/igneum/pkg/bin/igneum-worker-cuda'; true", 30)
|
||||
def stop_miners(self): self.run(f"bash {F}/in/fleet-stop.sh miner >/dev/null 2>&1; . {F}/in/pidkill.sh; kill_pidfile miner; true", 30) # by pid file only
|
||||
def start_prover(self, label, wallet, hub_peer="", threshold="", miner="keep"):
|
||||
self.check(f"cd {F} && chmod +x in/box-prover.sh && LABEL={shlex.quote(label)} WALLET={wallet} HUB_PEER={hub_peer} THRESHOLD={threshold} MINER={miner} setsid nohup in/box-prover.sh </dev/null >/dev/null 2>&1 & echo started", 60)
|
||||
def stop_all(self):
|
||||
self.run(f"bash {F}/in/box-kill.sh >/dev/null 2>&1; pkill -f '[p]ython3 -u /root/fleet/in/box-prover.py'; pkill -x igneum-miner; pkill -f '[/]opt/igneum/pkg/bin/igneum-worker-cuda'; pkill -x sp1-gpu-server; true", 60)
|
||||
self.run(f"bash {F}/in/box-kill.sh >/dev/null 2>&1; true", 60) # box-kill.sh stops by pid files
|
||||
# ---- provider ----
|
||||
def destroy(self):
|
||||
import sys; sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
|
|
|||
9
tools/fleet/lib/pidkill.sh
Normal file
9
tools/fleet/lib/pidkill.sh
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#!/usr/bin/env bash
|
||||
# Kills by pid only (the founder, 8 Oct 2026, by construction: pkill and killall exit 97 on every box and pod).
|
||||
# kill_pidfile NAME [SIG] [WAIT] the pid in $F/pids/NAME.pid (TERM, then KILL after WAIT s); removes a stale file
|
||||
# kill_children PID [SIG] every descendant of PID by pid (pgrep -P walks the tree by parent pid, not by name), deepest first
|
||||
# kill_sock_owner GLOB the pid that owns a listening unix socket matching GLOB (ss -xlp), written to $F/pids/sp1-server.pid, then killed
|
||||
F=${F:-/root/fleet}; mkdir -p $F/pids
|
||||
kill_pidfile() { local f=$F/pids/$1.pid p; [ -s "$f" ] || return 0; p=$(cat "$f"); kill -0 "$p" 2>/dev/null || { rm -f "$f"; return 0; }; kill ${2:--TERM} "$p" 2>/dev/null; sleep ${3:-2}; kill -0 "$p" 2>/dev/null && kill -9 "$p" 2>/dev/null; rm -f "$f"; return 0; }
|
||||
kill_children() { local c; for c in $(pgrep -P "$1" 2>/dev/null); do kill_children "$c" "$2"; kill ${2:--TERM} "$c" 2>/dev/null; done; return 0; }
|
||||
kill_sock_owner() { local p; for p in $(ss -xlp 2>/dev/null | grep -E -- "$1" | grep -oE 'pid=[0-9]+' | cut -d= -f2 | sort -u); do echo "$p" > $F/pids/sp1-server.pid; kill -TERM "$p" 2>/dev/null; sleep 1; kill -0 "$p" 2>/dev/null && kill -9 "$p" 2>/dev/null; done; rm -f /tmp/sp1-cuda-*.sock; return 0; }
|
||||
|
|
@ -84,7 +84,7 @@ def update(label):
|
|||
"""The version follow: the manifest's hive package onto the box, the node pointer rewritten, the supervisor restarts it."""
|
||||
m = manifest(); b = Box.from_registry(label)
|
||||
if not m: raise SshError("no manifest")
|
||||
rc, out, err = b.run(f"set -e; cd {F}; curl -fsSL -m 600 -o pkg-{m['version']}.tgz https://dl.igneum.network{m['path']}; echo '{m['sha256']} pkg-{m['version']}.tgz' | sha256sum -c - >/dev/null; rm -rf /opt/igneum/pkg.new; mkdir -p /opt/igneum/pkg.new; tar -C /opt/igneum/pkg.new --strip-components=1 -xzf pkg-{m['version']}.tgz; rm -rf /opt/igneum/pkg.prev; mv /opt/igneum/pkg /opt/igneum/pkg.prev; mv /opt/igneum/pkg.new /opt/igneum/pkg; echo /opt/igneum/pkg/bin/igneumd > {F}/standing.node; pkill -9 -f '[/]opt/igneum/pkg/bin/igneumd' 2>/dev/null; pkill -9 -f '[/]opt/igneum/pkg.prev/bin/igneumd' 2>/dev/null; echo updated-to-{m['version']}", 900)
|
||||
rc, out, err = b.run(f"set -e; cd {F}; curl -fsSL -m 600 -o pkg-{m['version']}.tgz https://dl.igneum.network{m['path']}; echo '{m['sha256']} pkg-{m['version']}.tgz' | sha256sum -c - >/dev/null; rm -rf /opt/igneum/pkg.new; mkdir -p /opt/igneum/pkg.new; tar -C /opt/igneum/pkg.new --strip-components=1 -xzf pkg-{m['version']}.tgz; rm -rf /opt/igneum/pkg.prev; mv /opt/igneum/pkg /opt/igneum/pkg.prev; mv /opt/igneum/pkg.new /opt/igneum/pkg; echo /opt/igneum/pkg/bin/igneumd > {F}/standing.node; bash /root/fleet/in/fleet-stop.sh node >/dev/null 2>&1; bash /root/fleet/in/fleet-stop.sh node >/dev/null 2>&1; echo updated-to-{m['version']}", 900)
|
||||
iid, _ = Registry.find(label); Registry.patch(iid, version_wanted=m["version"], updated_at=now()); return out.strip()
|
||||
def rerent(label):
|
||||
"""Same shape again on the same provider; the old row is marked destroyed. Returns the new label or None."""
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ live=[r["label"] for r in standing.roster() if r["role"]=="live"]
|
|||
def move(l):
|
||||
try:
|
||||
b=Box.from_registry(l); b.put([F16], "/root/fleet/override-16.json")
|
||||
rc,out,err=b.run("cd /root/fleet && cp override.json override-13.json && cp override-16.json override.json && python3 -c \"import json; d=json.load(open('/root/fleet/override.json')); print('fields', len(d))\" && pkill -9 -f '[/](opt/igneum/pkg/bin|root/fleet/in)/igneumd(-0313|-[0-9a-f]{16})? --devnet --appdir=' ; echo killed-for-restart $(date -u +%T)", 60)
|
||||
rc,out,err=b.run("cd /root/fleet && cp override.json override-13.json && cp override-16.json override.json && python3 -c \"import json; d=json.load(open('/root/fleet/override.json')); print('fields', len(d))\" && bash /root/fleet/in/fleet-stop.sh node >/dev/null 2>&1; ; echo killed-for-restart $(date -u +%T)", 60)
|
||||
return l, out.replace("\n"," ").strip()+(" ERR "+err[:60] if err.strip() else "")
|
||||
except Exception as e: return l, "EXC "+str(e)[:60]
|
||||
print(st(), "moving", len(live), "boxes", flush=True)
|
||||
|
|
|
|||
6
tools/fleet/retired/README.md
Normal file
6
tools/fleet/retired/README.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Retired fleet scripts (8 Oct 2026)
|
||||
|
||||
Devnet 2 and 0.3.1x-era scripts that stop processes by name (pkill -f / pkill -x). Since the founder's word of 20:21 BST 8 Oct 2026
|
||||
every box and pod refuses pkill and killall by construction (exit 97), so none of these can run as written; they are kept for the
|
||||
record only. The live stop forms are tools/fleet/fleet-stop.sh (pid files under /root/fleet/pids), tools/fleet/lib/pidkill.sh
|
||||
(kill_pidfile, kill_children, kill_sock_owner) and tools/fleet/box-kill.sh.
|
||||
Loading…
Reference in a new issue