Merge master 67d594c02 into cae8635124 under the master-landing lock
This commit is contained in:
commit
dedbc740cb
51 changed files with 2924 additions and 229 deletions
|
|
@ -104,7 +104,7 @@ stock and at the 1,300 MHz lock, and 6.9 pJ per counted op on the Apple M5 Max (
|
|||
| lop3 (8-bit truth table) | 7,274 | 1.42 | 0.99 | 0.72 | 0.52 | 24.1 / 13.0 | 0.030 / 0.055 | | 0.11 | |
|
||||
| 32-lane xor-mask shuffle (butterfly over the 1 KB window), per lane-op | 181,580 | 1.24 | 0.87 | 0.63 | 0.45 | 55.8 / 29.4 | 0.011 / 0.021 | | 0.042 | routed with SPEF; 15:1x UK |
|
||||
| 32-lane general crossbar, per lane-op | ROW_XBAR |
|
||||
| 8 KB scratch, one random read (flop array: the pessimistic form) | ROW_SCRATCH |
|
||||
| 8 KB scratch, one random 32-bit read of a 2,048 x 32 flop array (the pessimistic form of a chip's L1; an SRAM macro reads lower) | 868,159 | 207 | 145 | 104 | 75 | 2,400 / 1,400 per L2 hit | 0.043 / 0.074 | | 0.15 | routed with SPEF, 19:4x UK; the card's shared-memory read is unmeasured (owed) |
|
||||
| int8 8x8x8 tile, per MAC | ROW_TILE |
|
||||
|
||||
Reading the floors: a lane's add costs the chip about 2.2 pJ at ASAP7 and 1.1 at N3, against the 5090's 6.2
|
||||
|
|
@ -138,7 +138,7 @@ draws 13.9 mW, the run phase 36.9 mW for 8 lanes at 1.5 ns).
|
|||
| core, 8 lanes, 32 registers | synthesis only (no wires, no clock tree) | 186,443 | 6.9 | 4.8 | 3.5 | 2.5 | 11.3 / 6.2 / 6.9 | 0.31 / 0.56 / 0.50 | 0.22 / 0.40 / 0.36 | 1.1 | synthesised; 14:0x UK |
|
||||
| of which the sequential term (register file, imem and IR clock pins, no clock gating) | | | 2.4 | 1.7 | 1.2 | 0.9 | | | | | |
|
||||
| of which the units, the read muxes and the butterfly | | | 4.5 | 3.1 | 2.3 | 1.6 | | | | | |
|
||||
| core, 8 lanes, 32 registers | placed and routed, SPEF | ROW_CORE8_PLACED |
|
||||
| core, 8 lanes, 32 registers, ungated | placed and routed, SPEF, clock tree (one run length of 300 cycles, the load phase subtracted, about plus or minus 10 percent) | 444,478 | 11.3 | 7.9 | 5.6 | 4.1 | 11.3 / 6.2 / 6.9 | 0.50 / 0.91 / 0.82 | 0.36 / 0.66 / 0.59 | 1.8 | placed 16:0x UK on a rented pod; +64 percent over synthesis (wires, and a clock tree of 2.5 pJ per lane-op that gating removes) |
|
||||
| core, 32 lanes, 32 registers | synthesis only (steady state from 150 and 400 run cycles) | 600,381 | 5.55 | 3.9 | 2.8 | 2.0 | 11.3 / 6.2 / 6.9 | 0.25 / 0.45 / 0.41 | 0.18 / 0.32 / 0.29 | 0.90 | synthesised; 15:2x UK |
|
||||
| core, 32 lanes, 16 registers | synthesis only | 443,258 | 4.2 | 2.9 | 2.1 | 1.5 | 11.3 / 6.2 / 6.9 | 0.18 / 0.34 / 0.30 | 0.13 / 0.24 / 0.22 | 0.68 | synthesised; one run length, about plus or minus 10 percent; 15:0x UK |
|
||||
| the bare ARX lane (section 3, the floor) | routed | 11,631 | 2.2 | 1.5 | 1.1 | 0.8 | 11.3 / 6.2 / 6.9 | 0.10 / 0.18 / 0.16 | 0.07 / 0.13 / 0.11 | 0.35 | the lower bound |
|
||||
|
|
@ -203,6 +203,128 @@ takes back on the card's own node (3.6x to 2.4x), which is the design. Node-for-
|
|||
k 0.78 and the 64-register core at 1.09, so "near 0.9" is reached node-for-node by the window alone; what it does
|
||||
not survive is the node step a chip project would buy (an N3 core gives back 0.4x, an N2 core 0.8x).
|
||||
|
||||
### 4c. The adversary's 64-register core: is the window a defence? (the coordinator's order, 15:3x UK)
|
||||
|
||||
Every row in this section is a MODEL of a chip core, never a lower bound on what a chip maker can build; the
|
||||
synthesis gives the cost of the circuit as drawn, and a better circuit is always possible.
|
||||
|
||||
**The live-state analysis** (`tools/chip-model/rtl/flow/livestate.py`, run on build-3: programs drawn as the
|
||||
core testbench draws them, the class v4 op weights, a load on one instruction in 16 as the dependent memory wait,
|
||||
dst and src uniform over the window, the result fold reading every register at the end of the block; 64 drawn
|
||||
programs, 1,024 waits per row):
|
||||
|
||||
| Window R | Live values at a wait (mean, min to max) | Of which necessary (reach a later address or the result, transitively) | Dead writes per block |
|
||||
|---|---|---|---|
|
||||
| 8 (the class ISA) | 7.0 of 8 (6 to 7) | 6.9 | 2.9 percent |
|
||||
| 32 | 30.5 of 32 (29 to 31) | 30.0 | 2.9 percent |
|
||||
| 64 | 61.7 of 64 (59 to 63) | 61.0 | 2.8 percent |
|
||||
| 64 at a 1,024-instruction block | 61.5 of 64 (58 to 63) | 60.6 | 3.1 percent |
|
||||
|
||||
So under a fold that reads every register, 95 percent of the window is live AND necessary across every memory
|
||||
wait: the adversary cannot shrink the state it keeps by liveness, and recomputing a value instead of keeping it
|
||||
costs the dependent chain that produced it (every value feeds the result transitively). The window is a
|
||||
defence ONLY because of the fold rule; a fold that read 8 of the 64 registers would let the chip drop the rest
|
||||
(the dead fraction would rise toward the fraction never read before the fold), so the fold-reads-all rule is the
|
||||
design rule that goes with the window.
|
||||
|
||||
**What the adversary can do with the state it must keep** is make it cheaper per access, not smaller. The
|
||||
GPU-shaped row (4a, 64 registers in flops, every flop clocked every cycle, three 64:1 read muxes) is 9.7 pJ per
|
||||
lane-op at ASAP7. The forms a chip maker would use:
|
||||
|
||||
| Form of the 64-register state (per lane, 256 bytes) | pJ per lane-op ASAP7 | N3 | k at the lock (N3) | Label |
|
||||
|---|---|---|---|---|
|
||||
| flops, no clock gating, 64:1 read muxes (the 4a row) | 9.7 | 4.9 | 0.78 | synthesised; a model |
|
||||
| flops with the register-file clock gated (one of 64 registers written per cycle; the ICG cells allowed back in and inferred by Yosys, 320 gates) | 6.2 (225,441 cells; sequential 0.2) | 3.1 | 0.50 (0.70 node-for-node) | synthesised 16:0x UK; a model |
|
||||
| the same gating on the 32-register base, for the penalty | 4.5 (156,833 cells; sequential 0.15) | 2.3 | 0.37 (0.51 node-for-node) | synthesised 16:0x UK; a model |
|
||||
| the gated 32-register base PLACED AND ROUTED (SPEF, clock tree, 379,633 cells; steady state solved from 150 and 600 run cycles) | 6.7 (+49 percent over synthesis) | 3.4 | 0.54 (0.76 node-for-node) | placed 19:5x UK; the GDDR7 board at the lock 2.4x node-for-node, 2.8x a node ahead: the morning's headline figures to the digit |
|
||||
| the gated 64-register window core PLACED AND ROUTED (563,339 cells; parasitics estimated from global routing, the SPEF lost to a full disk; steady state solved from 150 and 600 run cycles; plus or minus 15 percent) | 9.45 (+52 percent over synthesis; N5 6.6, N2 3.4) | 4.8 | 0.77 (1.07 node-for-node, 0.55 at N2) | placed 21:3x UK; the GDDR7 board at the lock 2.0x node-for-node, 2.45x a node ahead, 2.9x two ahead; the window's residual against the placed base +2.75 pJ per lane-op, +0.23 of k at the lock |
|
||||
| latch-based register file (the clocked element halved; about 30 percent under the gated flop file, approximate) | about 0.7 x the gated row | | | modelled |
|
||||
| SRAM-banked state shared across time-multiplexed lanes (one execution port serving many lanes' streams in turn, each lane's 256 bytes in a bank): modelled 8.5 to 10.5 from the access energies; BUILT as `core_tm` (8 lanes x 64 registers in banks, one port, round-robin, ungated): 8.8 pJ per lane-op synthesised (173,426 cells), against the SIMD ungated 9.7 | 8.8 (built) | 4.4 | 0.71 | synthesised 16:2x UK: port sharing saves 0.9 pJ of units and the bank-select muxes take most of it back; NOT the lever; the multi-family adversary lane's macro-window core reads 5.9 (its FakeRAM term modelled 2.0 to 7.0 pJ per access), within 5 percent of the gated flop row, so the file's form is not the lever either |
|
||||
| values recomputed instead of kept | not available: 95 percent of the window is necessary (above) | | | measured on drawn programs |
|
||||
|
||||
The defence, then, is the penalty that remains after the adversary's best form: the gated 64-register file
|
||||
against the gated 32-register file (the two synthesised rows above when they land; measured: 6.2 against 4.5 pJ per lane-op at ASAP7 synthesised, a penalty of 1.7 pJ; PLACED 9.45 against 6.7, a
|
||||
penalty of 2.75 pJ, 1.4 at N3, +0.23 of k at the lock, 0.54 to 0.77 at N3 and 0.76 to 1.07 node-for-node; the
|
||||
analytic estimate had been 1.5 pJ). So on placed rows the window takes the GDDR7 board at the lock from 2.4x to
|
||||
2.0x node-for-node and from 2.8x to 2.45x a node ahead, for at most 5 percent per load on the card. Two corrections this
|
||||
forces: the honest adversary's BASE core is the gated one (k 0.37 at N3, 0.51 node-for-node), under the ungated
|
||||
0.56 and 0.78 of section 4, which are the GPU-shaped core a maker would not build; and placement costs more than
|
||||
the +20 to +40 percent estimated (the ungated placed base reads 11.3 against 6.9 pJ: wires plus a 2.5 pJ clock tree
|
||||
that gating removes), so the placed gated rows (on a rented pod, 17:30 UK) are the figures to serve. On the 32-lane core the same
|
||||
penalty applies per lane (the register file does not amortise), so the window moves the 32-lane core from k 0.45
|
||||
to about 0.57 at the lock at N3 (0.63 to about 0.80 node-for-node).
|
||||
|
||||
**The GPU side** (the hash lane's hand): the compiled allocation of the 64-register measurement pack (ptxas
|
||||
registers per thread, local-memory spill bytes, occupancy) and the rate beside the 8-register base, clock
|
||||
18:00 UK; until then the modelled reading stands: about 110 of 255 registers per thread, occupancy about half,
|
||||
the rate expected to hold under the latency-bound chain (the 5090 hides about 330,000 ops per hash before compute
|
||||
binds) and the energy to move little, the per-lane register traffic the unmeasured term.
|
||||
|
||||
The measured GPU side (the hash lane, 16:1x to 16:4x UK, RunPod secure pods, driver 580, the kit worker, 250 x
|
||||
2^24, nvidia-smi 1 Hz; ptxas from nvcc 12.8 -Xptxas -v on the pack's kernel; pods destroyed, USD 1.22):
|
||||
|
||||
| Card, pack | MH/s | W | microjoules per hash | registers per thread (ptxas) | spill | blocks per SM (occupancy) | per load | Label |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| 5090, the base (mx8-devnet-epoch0) | 141.74 | 303.1 | 2.139 | 30 | 0 B | 24 (4,080 warps) | 16.7 nJ | measured |
|
||||
| 5090, the window, arithmetic-only (hl-reg64, twice the base's work by construction) | 80.38 | 308.6 | 3.839 | 96 | 0 B | 20 (83 percent) | 15.0 nJ | measured: level per unit of work |
|
||||
| 5090, the window, full chain (hl-reg64c: every load's address mixes all 64 registers) | 70.96 | 320.3 | 4.513 | 88 | 0 B | 20 (83 percent) | 17.6 nJ (+5 percent) | measured |
|
||||
| 4090, the base | 62.67 | 208.9 | 3.333 | 29 | 0 B | 24 | 26.0 nJ | measured |
|
||||
| 4090, the window, arithmetic-only | 31.57 | 210.3 | 6.663 | 104 | 0 B | 16 (67 percent) | 26.0 nJ | measured: level |
|
||||
| 4090, the window, full chain | 31.38 | 216.5 | 6.898 | 87 | 0 B | 20 (83 percent) | 27.0 nJ (+4 percent) | measured |
|
||||
|
||||
So the card's side of the window defence is at most 5 percent per load: no spill on either card in either form, 88
|
||||
to 104 registers per thread, occupancy 67 to 83 percent, and the rate per unit of work held within 5 percent under
|
||||
the latency-bound chain. The sound class form is the full chain (the arithmetic-only fold fails the liveness rule;
|
||||
class string `+reg64c`, pack hl-v6-win with `check_window_liveness` in its suite). The chip's side (this section's
|
||||
gated rows) therefore carries the whole defence.
|
||||
|
||||
### 4d. The connected-state variant (cs64s27x16, the connected-state lane's structure) on the adversary's core
|
||||
|
||||
The connected-state lane's program (a 64-register window; per step a load whose address register is the previous
|
||||
block's last dst, the word landing in m_j, then a 27-instruction block whose first instruction reads m_j and every
|
||||
later one draws its src from the block's last four dsts, the last instruction injecting; 16 steps of text, 448
|
||||
instructions, 16 passes per block; its liveness tool: 63 of 64 live at every address, about 11 registers in the
|
||||
per-step dependent chain, about 20 touched per block) priced on the gated 64-register core with a 512-entry imem,
|
||||
the program drawn by those rules in the testbench (`CS` mode of `tb_core_common.vh`), synthesis-only:
|
||||
|
||||
| Row | Cells | pJ per lane-op ASAP7 | N5 | N3 | N2 | k at the lock N5 / N3 / N2 | k at stock N5 / N3 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| cs64s27x16 on the gated 64-register core, 512 imem | 253,059 | 6.3 | 4.4 | 3.2 | 2.3 | 0.71 / 0.51 / 0.37 | 0.39 / 0.28 |
|
||||
| the class v4 draw on the gated 64-register core, 256 imem (4c) | 225,441 | 6.2 | 4.3 | 3.1 | 2.2 | 0.70 / 0.50 / 0.36 | 0.38 / 0.27 |
|
||||
|
||||
The chip's shadow for the program is 55,296 x 3.2 pJ = 0.18 microjoules per hash at N3 (0.24 node-for-node)
|
||||
against 0.13 for the genesis window on the same core; the card pays +0.6 percent for the window on the 5090 (the
|
||||
connected-state lane's measurement). The structure's other knobs do not reach the chip: the 16-pass loop and the
|
||||
448 text cost the shared imem about 0.1 pJ per lane-op, hot-set banking is not needed (the gated file charges only
|
||||
the written register), and the chain's width sets lane count, which is free. On the GDDR7 board at the lock the
|
||||
window moves the chip's edge by about 1.1x (3.6x to 3.3x node-for-node), under the 1.25x gate that lane set.
|
||||
|
||||
### 4e. The mixed-resource lane's FP32 units (class-v6-mixedfp) on the adversary's lane
|
||||
|
||||
The mixed-resource lane's candidate adds four FP32 families (fadd, fmul, ffma, fcvt) to the shadow's draw, every
|
||||
result injected by xor, with inputs masked to a 7-bit exponent range (never zero, denormal, NaN or Inf). The
|
||||
adversary's simplified units (`rtl/fp32_units.v`): an FMA with the 24 x 24 mantissa multiplier, a 100-bit
|
||||
alignment window, a full normaliser and RNE; a separate adder and multiplier; the int32 to float converter; the
|
||||
exponent path narrowed to the range; no NaN, Inf, denormal or flag logic. The lane: an 8 x 32-bit window, the
|
||||
four units, `d ^= bits(result)`. Routed with SPEF, random-input VCD, 42,936 cells, a 2 ns clock.
|
||||
|
||||
| Op (every unit evaluating each cycle: an UPPER bound per op, no operand isolation) | pJ per op ASAP7 | N5 | N3 | N2 | 5090 fp32_fma stock / lock | k at N3 vs stock / lock |
|
||||
|---|---|---|---|---|---|---|
|
||||
| fadd | 6.6 | 4.6 | 3.3 | 2.4 | 9.2 / 5.2 | 0.36 / 0.64 |
|
||||
| fmul | 7.0 | 4.9 | 3.5 | 2.5 | 9.2 / 5.2 | 0.38 / 0.68 |
|
||||
| ffma | 6.7 | 4.7 | 3.4 | 2.4 | 9.2 / 5.2 | 0.37 / 0.65 |
|
||||
| fcvt | 6.9 | 4.8 | 3.5 | 2.5 | 9.2 / 5.2 (cvt unmeasured on the card) | 0.38 / 0.67 |
|
||||
| random mix | 7.1 | 4.9 | 3.6 | 2.6 | | 0.39 / 0.68 |
|
||||
|
||||
Reading: the four read alike because all four units switch every cycle on the same operands, so each row is the
|
||||
upper bound for its op (a chip isolates the idle units; by cell share about ffma 3.5 to 4, fmul 2.5, fadd 2, fcvt
|
||||
1 pJ at ASAP7, approximate). Even on the upper bound the FP family is the chip's dearest per op relative to the
|
||||
card: k 0.65 at N3 at the lock against 0.18 for the integer ARX lane floor, because the card does an FMA for 5.2
|
||||
pJ (under its own int add at 6.2) while the chip's multiply, alignment and normaliser cost about three int ops.
|
||||
On the units' floors the shadow's k_eff rises from 0.097 (class v4) to about 0.14 at the fp12 mix and 0.17 at
|
||||
fp24 (0.20 and 0.24 with isolation taken as half), the core's per-op overhead on top. The GPU-cost budget (10
|
||||
percent of energy per hash) is the binding side, and the vendor-rounding question is the class's, not the chip's.
|
||||
|
||||
## 5. The chip edge at the measured k
|
||||
|
||||
`E_chip = E_mem + N_ops x e_chip` (absolute: the chip's shadow cost is 102,100 x 3.5 pJ = 0.36 microjoules per hash
|
||||
|
|
@ -270,7 +392,7 @@ to 2.3x and the strongest chips at 2.6x to 4.2x.
|
|||
| 6 | prmt, lop3 | 0.64, 0.72 | 11.5, 13.0 | 0.056, 0.055 | not drawn (RTL rows only) |
|
||||
| 7 | mulhi | 0.68 | 21.0 | 0.032 | yes (6) |
|
||||
| 8 | 32-lane shuffle (butterfly) | 0.63 | 29.4 | 0.021 | yes (8) |
|
||||
| 9 | L1 scratch read (8 KB flop array) against the card's L2 hit | ROW_SCRATCH_K | 1,400 | pending | not drawn |
|
||||
| 9 | L1 scratch read (8 KB flop array) against the card's L2 hit | 104 (pJ per read) | 1,400 | 0.074 | not drawn |
|
||||
| 10 | int8 8x8x8 tile, per MAC | ROW_TILE_K | 2.2 | pending | not drawn (the tensor lever is dead on other grounds) |
|
||||
|
||||
The order is set by the card's price, not the chip's: the chip pays 0.6 to 1.7 pJ for everything, and the card
|
||||
|
|
|
|||
|
|
@ -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,8 +336,8 @@ 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
|
||||
|
|
@ -367,7 +367,7 @@ Rule: a case maps to a cell only where the cell's tests visibly answer it; cover
|
|||
|
||||
### pc:install-update
|
||||
|
||||
- Command: `signed jobs and relay runs on PC 1 (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)`
|
||||
- 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:
|
||||
|
|
@ -375,16 +375,48 @@ Rule: a case maps to a cell only where the cell's tests visibly answer it; cover
|
|||
- 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
|
||||
|
||||
### review:k-lane-shadow-k
|
||||
|
||||
- Command: `floor lane 2's placed and routed cores on ASAP7 in tools/chip-model/rtl (the rows in docs/analysis/class-v6/floor/shadow-k.md); the register landing 50ff1611f recorded ADV-06 against it before the recorder existed`
|
||||
- Box class: none
|
||||
- Fixtures: none
|
||||
- Cases:
|
||||
- ADV-06 Separate process advantage from specialisation: partial: the placed gated cores and the adversary's forms are modelled in shadow-k.md; the independent review remains
|
||||
|
||||
## 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-06 Separate process advantage from specialisation: process-advantage separation is the adversary lanes' chip study
|
||||
- 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
|
||||
|
|
@ -471,7 +503,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
|
||||
|
|
@ -490,9 +521,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
|
||||
|
|
@ -530,4 +558,4 @@ Rule: a case maps to a cell only where the cell's tests visibly answer it; cover
|
|||
|
||||
## Count
|
||||
|
||||
171 automated cases: 78 mapped to a cell, 150 NOT RUN with a reason.
|
||||
171 automated cases: 83 mapped to a cell, 145 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
|
||||
|
|
|
|||
|
|
@ -23,8 +23,14 @@ CPUSET = $(if $(LEASE_ON),--cpuset-cpus {cpuset},)
|
|||
DOCKER := $(LEASEPFX) docker run --rm -u $(UID_GID) -e HOME=/tmp -e NUM_CORES=$(THREADS) $(CPUSET) -v $(WORK):/work
|
||||
ORFS := $(DOCKER) -w /OpenROAD-flow-scripts/flow $(ORFS_IMG)
|
||||
SIM := $(DOCKER) -w /work $(SIM_IMG)
|
||||
# NO_DOCKER=1: the box IS the ORFS image (a rented pod started from openroad/orfs:latest with iverilog installed
|
||||
# and /work a symlink to this directory); the same targets run natively.
|
||||
ifeq ($(NO_DOCKER),1)
|
||||
ORFS := env NUM_CORES=$(THREADS) bash -c 'cd /OpenROAD-flow-scripts/flow && exec "$$@"' --
|
||||
SIM := env
|
||||
endif
|
||||
|
||||
DESIGNS := arx mul prmt lop3 fold shfl xbar scratch tile core8 core32 core32r16 core8r64 core8i1k core8sel core32all
|
||||
DESIGNS := arx mul prmt lop3 fold shfl xbar scratch tile core8 core32 core32r16 core8r64 core8i1k core8sel core32all core8g core8r64g coretm cs64 fp32
|
||||
top = $(shell sed -n 's/^$(1) \([^ ]*\) .*/\1/p' flow/designs.txt)
|
||||
|
||||
# per-family simulation tags (the op field fixed per row where the family has several ops)
|
||||
|
|
@ -37,13 +43,18 @@ SIMS_shfl := mix
|
|||
SIMS_xbar := mix
|
||||
SIMS_scratch := mix
|
||||
SIMS_tile := mix
|
||||
SIMS_core8 := mix mixld:+loads=1
|
||||
SIMS_core32 := mix mixld:+loads=1
|
||||
SIMS_core32r16 := mix mixld:+loads=1
|
||||
SIMS_core8r64 := mix
|
||||
SIMS_core8 := s150:+cycles=150 s600:+cycles=600
|
||||
SIMS_core32 := s150:+cycles=150 s600:+cycles=600
|
||||
SIMS_core32r16 := s150:+cycles=150 s600:+cycles=600
|
||||
SIMS_core8r64 := s150:+cycles=150 s600:+cycles=600
|
||||
SIMS_core8i1k := mix
|
||||
SIMS_core8sel := mix
|
||||
SIMS_core32all := mix
|
||||
SIMS_core8g := s150:+cycles=150 s600:+cycles=600
|
||||
SIMS_core8r64g := s150:+cycles=150 s600:+cycles=600
|
||||
SIMS_coretm := mix
|
||||
SIMS_cs64 := s150:+cycles=150 s600:+cycles=600
|
||||
SIMS_fp32 := mix fadd:+op=0 fmul:+op=1 ffma:+op=2 fcvt:+op=3
|
||||
|
||||
.PHONY: rows table clean
|
||||
|
||||
|
|
@ -65,6 +76,7 @@ sim-%: flow-%
|
|||
|
||||
power-%: sim-%
|
||||
$(ORFS) make DESIGN_CONFIG=/work/flow/$*.mk RUN_SCRIPT=/work/flow/power.tcl RUN_LOG_NAME_STEM=power run 2>&1 | tee logs/power-$*.log
|
||||
rm -f sim/$*/*.vcd # the traces run to gigabytes each; the power log keeps every number (a full disk killed four runs on 8 October 2026)
|
||||
|
||||
# synthesis-only row (no placement, no parasitics): the 14:45 fallback
|
||||
synth-%:
|
||||
|
|
|
|||
12
tools/chip-model/rtl/flow/asap7_icg_model.v
Normal file
12
tools/chip-model/rtl/flow/asap7_icg_model.v
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// Behavioural model of the ASAP7 integrated clock gate (latch_posedge_precontrol: the enable is latched while the
|
||||
// clock is low, the gated clock is CLK AND the latched enable OR test). The liberty carries no function for it.
|
||||
module ICGx1_ASAP7_75t_R(input CLK, input ENA, input SE, output GCLK);
|
||||
reg en = 0;
|
||||
always @(CLK or ENA or SE) if (!CLK) en = ENA | SE;
|
||||
assign GCLK = CLK & en;
|
||||
endmodule
|
||||
module ICGx2_ASAP7_75t_R(input CLK, input ENA, input SE, output GCLK);
|
||||
reg en = 0;
|
||||
always @(CLK or ENA or SE) if (!CLK) en = ENA | SE;
|
||||
assign GCLK = CLK & en;
|
||||
endmodule
|
||||
|
|
@ -6,7 +6,7 @@ import re, sys, os, csv
|
|||
|
||||
work = sys.argv[1] if len(sys.argv) > 1 else '.'
|
||||
# ops per cycle per design (the per-op divisor) and the GPU row each family is read against
|
||||
OPS = {'arx': 1, 'mul': 1, 'prmt': 1, 'lop3': 1, 'fold': 1, 'shfl': 32, 'xbar': 32, 'scratch': 1, 'tile': 512, 'core8': 8, 'core32': 32, 'core32r16': 32, 'core8r64': 8, 'core8i1k': 8, 'core8sel': 8, 'core32all': 32}
|
||||
OPS = {'arx': 1, 'mul': 1, 'prmt': 1, 'lop3': 1, 'fold': 1, 'shfl': 32, 'xbar': 32, 'scratch': 1, 'tile': 512, 'core8': 8, 'core32': 32, 'core32r16': 32, 'core8r64': 8, 'core8i1k': 8, 'core8sel': 8, 'core32all': 32, 'core8g': 8, 'core8r64g': 8, 'coretm': 1, 'cs64': 8, 'fp32': 1}
|
||||
# 5090 measured pJ per counted op: (unlocked, at the 1,300 MHz lock); 15.1a
|
||||
GPU = {
|
||||
'arx:mix': (11.3, 6.2), 'arx:add': (11.3, 6.2), 'arx:sub': (11.3, 6.2), 'arx:xor': (11.3, 6.2), 'arx:or': (11.3, 6.2),
|
||||
|
|
@ -17,7 +17,7 @@ GPU = {
|
|||
'shfl:mix': (55.8, 29.4), 'xbar:mix': (55.8, 29.4),
|
||||
'scratch:mix': (2400.0, 1400.0), # the card's L2 hit (no shared-memory probe measured: owed)
|
||||
'tile:mix': (4.1, 2.2),
|
||||
'core8:mix': (11.3, 6.2), 'core8:mixld': (11.3, 6.2), 'core32:mix': (11.3, 6.2), 'core32:mixld': (11.3, 6.2), 'core32r16:mix': (11.3, 6.2), 'core8r64:mix': (11.3, 6.2), 'core8i1k:mix': (11.3, 6.2), 'core8sel:mix': (11.3, 6.2), 'core32all:mix': (11.3, 6.2), # the class v4 draw: read against int_arx (the packs job read the whole mix at 10.8 / 6.4) # dependent u8 m8n8k16 per MAC; the wide s8 tile reads 1.36 / 0.83
|
||||
'core8:mix': (11.3, 6.2), 'core8:mixld': (11.3, 6.2), 'core32:mix': (11.3, 6.2), 'core32:mixld': (11.3, 6.2), 'core32r16:mix': (11.3, 6.2), 'core8r64:mix': (11.3, 6.2), 'core8i1k:mix': (11.3, 6.2), 'core8sel:mix': (11.3, 6.2), 'core32all:mix': (11.3, 6.2), 'core8g:mix': (11.3, 6.2), 'core8r64g:mix': (11.3, 6.2), 'coretm:mix': (11.3, 6.2), 'cs64:mix': (11.3, 6.2), 'fp32:mix': (9.2, 5.2), 'fp32:fadd': (9.2, 5.2), 'fp32:fmul': (9.2, 5.2), 'fp32:ffma': (9.2, 5.2), 'fp32:fcvt': (9.2, 5.2), # the class v4 draw: read against int_arx (the packs job read the whole mix at 10.8 / 6.4) # dependent u8 m8n8k16 per MAC; the wide s8 tile reads 1.36 / 0.83
|
||||
}
|
||||
# per-node energy scaling from ASAP7 (a 7 nm-class predictive PDK at 0.70 V), approximate and claimed:
|
||||
# N7 -> N5 x0.70 (TSMC: "30 percent lower power at the same speed"), N5 -> N3E x0.72 (TSMC: 25 to 30 percent),
|
||||
|
|
@ -67,10 +67,10 @@ for d in OPS:
|
|||
pairs = [('vcd:s150', 'vcd:s600', 150, 600), ('vcd:short', 'vcd:synth', 150, 800), ('vcd:s150', 'vcd:s400', 150, 400)]
|
||||
for sh, lg, cs, cl in pairs:
|
||||
if sh in rows and lg in rows:
|
||||
rows['vcd:steady'] = steady(rows, period, sh, lg, cs, cl, 1028 if d in ('core8i1k', 'core32all') else LOAD_CYCLES)
|
||||
rows['vcd:steady'] = steady(rows, period, sh, lg, cs, cl, 1028 if d in ('core8i1k', 'core32all') else (452 if d == 'cs64' else LOAD_CYCLES))
|
||||
for tag, r in rows.items():
|
||||
sub = tag.split(':')[1] if ':' in tag else 'prop'
|
||||
key = f'{d}:{sub}' if sub in ('add','sub','xor','or','rotl','rotr','mul','mulhi','mad','mixld') else f'{d}:mix'
|
||||
key = f'{d}:{sub}' if sub in ('add','sub','xor','or','rotl','rotr','mul','mulhi','mad','mixld','fadd','fmul','ffma','fcvt') else f'{d}:mix'
|
||||
gpu = GPU.get(key, (None, None))
|
||||
pj = r['total'] * period * 1e-12 / OPS[d] * 1e12 # W * s / ops -> pJ
|
||||
pj_dyn = (r['internal'] + r['switching']) * period / OPS[d]
|
||||
|
|
|
|||
18
tools/chip-model/rtl/flow/core8g.mk
Normal file
18
tools/chip-model/rtl/flow/core8g.mk
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# ORFS design config for the programmable shadow core (core8: core_v6_8), ASAP7.
|
||||
export PLATFORM = asap7
|
||||
export DESIGN_NAME = core_v6_8
|
||||
export DESIGN_NICKNAME = core8g
|
||||
export VERILOG_FILES = /work/rtl/core_v6_8.v
|
||||
export VERILOG_INCLUDE_DIRS = /work/rtl
|
||||
export SDC_FILE = /work/flow/core8g.sdc
|
||||
export CORE_UTILIZATION = 40
|
||||
export CORE_ASPECT_RATIO = 1
|
||||
export CORE_MARGIN = 0.5
|
||||
export PLACE_DENSITY = 0.55
|
||||
export CORNER = TC
|
||||
export SKIP_LAST_GASP = 1
|
||||
export WORK_HOME = /work/out/core8g
|
||||
export SYNTH_MEMORY_MAX_BITS = 2000000
|
||||
# the adversary's register file: clock gating inferred (the ICG cells allowed back in)
|
||||
export INFER_CLKGATES = 1
|
||||
export DONT_USE_CELLS = *x1p*_ASAP7* *xp*_ASAP7* SDF*
|
||||
10
tools/chip-model/rtl/flow/core8g.sdc
Normal file
10
tools/chip-model/rtl/flow/core8g.sdc
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
current_design core_v6_8
|
||||
set clk_name core_clock
|
||||
set clk_port_name clk
|
||||
set clk_period 1500
|
||||
set clk_io_pct 0.2
|
||||
set clk_port [get_ports $clk_port_name]
|
||||
create_clock -name $clk_name -period $clk_period $clk_port
|
||||
set non_clock_inputs [all_inputs -no_clocks]
|
||||
set_input_delay [expr $clk_period * $clk_io_pct] -clock $clk_name $non_clock_inputs
|
||||
set_output_delay [expr $clk_period * $clk_io_pct] -clock $clk_name [all_outputs]
|
||||
18
tools/chip-model/rtl/flow/core8r64g.mk
Normal file
18
tools/chip-model/rtl/flow/core8r64g.mk
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# ORFS design config for the programmable shadow core (core8: core_v6_8r64), ASAP7.
|
||||
export PLATFORM = asap7
|
||||
export DESIGN_NAME = core_v6_8r64
|
||||
export DESIGN_NICKNAME = core8r64g
|
||||
export VERILOG_FILES = /work/rtl/core_v6_8r64.v
|
||||
export VERILOG_INCLUDE_DIRS = /work/rtl
|
||||
export SDC_FILE = /work/flow/core8r64g.sdc
|
||||
export CORE_UTILIZATION = 40
|
||||
export CORE_ASPECT_RATIO = 1
|
||||
export CORE_MARGIN = 0.5
|
||||
export PLACE_DENSITY = 0.55
|
||||
export CORNER = TC
|
||||
export SKIP_LAST_GASP = 1
|
||||
export WORK_HOME = /work/out/core8r64g
|
||||
export SYNTH_MEMORY_MAX_BITS = 2000000
|
||||
# the adversary's register file: clock gating inferred (the ICG cells allowed back in)
|
||||
export INFER_CLKGATES = 1
|
||||
export DONT_USE_CELLS = *x1p*_ASAP7* *xp*_ASAP7* SDF*
|
||||
10
tools/chip-model/rtl/flow/core8r64g.sdc
Normal file
10
tools/chip-model/rtl/flow/core8r64g.sdc
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
current_design core_v6_8r64
|
||||
set clk_name core_clock
|
||||
set clk_port_name clk
|
||||
set clk_period 1500
|
||||
set clk_io_pct 0.2
|
||||
set clk_port [get_ports $clk_port_name]
|
||||
create_clock -name $clk_name -period $clk_period $clk_port
|
||||
set non_clock_inputs [all_inputs -no_clocks]
|
||||
set_input_delay [expr $clk_period * $clk_io_pct] -clock $clk_name $non_clock_inputs
|
||||
set_output_delay [expr $clk_period * $clk_io_pct] -clock $clk_name [all_outputs]
|
||||
18
tools/chip-model/rtl/flow/coretm.mk
Normal file
18
tools/chip-model/rtl/flow/coretm.mk
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# ORFS design config for the programmable shadow core (core8: core_tm_8r64), ASAP7.
|
||||
export PLATFORM = asap7
|
||||
export DESIGN_NAME = core_tm_8r64
|
||||
export DESIGN_NICKNAME = coretm
|
||||
export VERILOG_FILES = /work/rtl/core_tm_8r64.v
|
||||
export VERILOG_INCLUDE_DIRS = /work/rtl
|
||||
export SDC_FILE = /work/flow/coretm.sdc
|
||||
export CORE_UTILIZATION = 40
|
||||
export CORE_ASPECT_RATIO = 1
|
||||
export CORE_MARGIN = 0.5
|
||||
export PLACE_DENSITY = 0.55
|
||||
export CORNER = TC
|
||||
export SKIP_LAST_GASP = 1
|
||||
export WORK_HOME = /work/out/coretm
|
||||
export SYNTH_MEMORY_MAX_BITS = 2000000
|
||||
# the adversary's register file: clock gating inferred (the ICG cells allowed back in)
|
||||
export INFER_CLKGATES = 1
|
||||
export DONT_USE_CELLS = *x1p*_ASAP7* *xp*_ASAP7* SDF*
|
||||
10
tools/chip-model/rtl/flow/coretm.sdc
Normal file
10
tools/chip-model/rtl/flow/coretm.sdc
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
current_design core_tm_8r64
|
||||
set clk_name core_clock
|
||||
set clk_port_name clk
|
||||
set clk_period 1500
|
||||
set clk_io_pct 0.2
|
||||
set clk_port [get_ports $clk_port_name]
|
||||
create_clock -name $clk_name -period $clk_period $clk_port
|
||||
set non_clock_inputs [all_inputs -no_clocks]
|
||||
set_input_delay [expr $clk_period * $clk_io_pct] -clock $clk_name $non_clock_inputs
|
||||
set_output_delay [expr $clk_period * $clk_io_pct] -clock $clk_name [all_outputs]
|
||||
18
tools/chip-model/rtl/flow/cs64.mk
Normal file
18
tools/chip-model/rtl/flow/cs64.mk
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# ORFS design config for the programmable shadow core (core8: core_v6_8cs64), ASAP7.
|
||||
export PLATFORM = asap7
|
||||
export DESIGN_NAME = core_v6_8cs64
|
||||
export DESIGN_NICKNAME = cs64
|
||||
export VERILOG_FILES = /work/rtl/core_v6_8cs64.v
|
||||
export VERILOG_INCLUDE_DIRS = /work/rtl
|
||||
export SDC_FILE = /work/flow/cs64.sdc
|
||||
export CORE_UTILIZATION = 40
|
||||
export CORE_ASPECT_RATIO = 1
|
||||
export CORE_MARGIN = 0.5
|
||||
export PLACE_DENSITY = 0.55
|
||||
export CORNER = TC
|
||||
export SKIP_LAST_GASP = 1
|
||||
export WORK_HOME = /work/out/cs64
|
||||
export SYNTH_MEMORY_MAX_BITS = 2000000
|
||||
# the adversary's register file: clock gating inferred (the ICG cells allowed back in)
|
||||
export INFER_CLKGATES = 1
|
||||
export DONT_USE_CELLS = *x1p*_ASAP7* *xp*_ASAP7* SDF*
|
||||
10
tools/chip-model/rtl/flow/cs64.sdc
Normal file
10
tools/chip-model/rtl/flow/cs64.sdc
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
current_design core_v6_8cs64
|
||||
set clk_name core_clock
|
||||
set clk_port_name clk
|
||||
set clk_period 1500
|
||||
set clk_io_pct 0.2
|
||||
set clk_port [get_ports $clk_port_name]
|
||||
create_clock -name $clk_name -period $clk_period $clk_port
|
||||
set non_clock_inputs [all_inputs -no_clocks]
|
||||
set_input_delay [expr $clk_period * $clk_io_pct] -clock $clk_name $non_clock_inputs
|
||||
set_output_delay [expr $clk_period * $clk_io_pct] -clock $clk_name [all_outputs]
|
||||
|
|
@ -14,3 +14,8 @@ core8r64 core_v6_8r64 1500
|
|||
core8i1k core_v6_8i1k 1500
|
||||
core8sel core_v6_8sel 1500
|
||||
core32all core_v6_32all 1500
|
||||
core8g core_v6_8 1500
|
||||
core8r64g core_v6_8r64 1500
|
||||
coretm core_tm_8r64 1500
|
||||
cs64 core_v6_8cs64 1500
|
||||
fp32 lane_fp32 2000
|
||||
|
|
|
|||
15
tools/chip-model/rtl/flow/fp32.mk
Normal file
15
tools/chip-model/rtl/flow/fp32.mk
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# ORFS design config for the mul shadow-core family (top lane_fp32), ASAP7.
|
||||
export PLATFORM = asap7
|
||||
export DESIGN_NAME = lane_fp32
|
||||
export DESIGN_NICKNAME = fp32
|
||||
export VERILOG_FILES = /work/rtl/fp32_units.v
|
||||
export VERILOG_INCLUDE_DIRS = /work/rtl
|
||||
export SDC_FILE = /work/flow/fp32.sdc
|
||||
export CORE_UTILIZATION = 40
|
||||
export CORE_ASPECT_RATIO = 1
|
||||
export CORE_MARGIN = 0.5
|
||||
export PLACE_DENSITY = 0.55
|
||||
export CORNER = TC
|
||||
export SKIP_LAST_GASP = 1
|
||||
export WORK_HOME = /work/out/fp32
|
||||
|
||||
10
tools/chip-model/rtl/flow/fp32.sdc
Normal file
10
tools/chip-model/rtl/flow/fp32.sdc
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
current_design lane_fp32
|
||||
set clk_name core_clock
|
||||
set clk_port_name clk
|
||||
set clk_period 2000
|
||||
set clk_io_pct 0.2
|
||||
set clk_port [get_ports $clk_port_name]
|
||||
create_clock -name $clk_name -period $clk_period $clk_port
|
||||
set non_clock_inputs [all_inputs -no_clocks]
|
||||
set_input_delay [expr $clk_period * $clk_io_pct] -clock $clk_name $non_clock_inputs
|
||||
set_output_delay [expr $clk_period * $clk_io_pct] -clock $clk_name [all_outputs]
|
||||
79
tools/chip-model/rtl/flow/livestate.py
Normal file
79
tools/chip-model/rtl/flow/livestate.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Live-state analysis of a drawn shadow program on an R-register window (the coordinator's order, 15:3x UK).
|
||||
The program is drawn as the core testbench draws it: NPROG instructions with the class v4 op weights, a load on
|
||||
one instruction in 16 (the dependent memory wait), dst/src/src2 uniform over the R registers, and the result
|
||||
fold reading every register at the end of the block. For every load (wait) the script reports the live set:
|
||||
registers whose current value is read later (by an instruction, a later address, or the fold) before being
|
||||
overwritten, split into those that feed a later ADDRESS or the RESULT and those that die inside an arithmetic
|
||||
block. Dead writes (overwritten before any read) are counted too. Usage: livestate.py R [NPROG] [seeds]"""
|
||||
import random, sys
|
||||
R = int(sys.argv[1]) if len(sys.argv) > 1 else 64
|
||||
N = int(sys.argv[2]) if len(sys.argv) > 2 else 256
|
||||
SEEDS = int(sys.argv[3]) if len(sys.argv) > 3 else 64
|
||||
W = [('add',12),('xor',10),('mul',8),('mad',8),('shfl',8),('rotl',7),('sub',6),('mulhi',6),('rotr',6),('or',4)]
|
||||
ops = [o for o,w in W for _ in range(w)]
|
||||
def draw(rng):
|
||||
prog = []
|
||||
for k in range(N):
|
||||
op = 'load' if k % 16 == 15 else rng.choice(ops)
|
||||
d, s, s2 = rng.randrange(R), rng.randrange(R), rng.randrange(R)
|
||||
reads = [s] if op not in ('load',) else [s] # the load's address comes from src
|
||||
if op in ('add','xor','mul','mad','sub','or','rotr','shfl','mulhi','rotl'): reads.append(d) # dst is read too (r[d] op= ...)
|
||||
if op == 'mad': reads.append(s2)
|
||||
if op == 'rotl': reads = [d]
|
||||
prog.append((op, d, reads))
|
||||
return prog
|
||||
tot_live = tot_addr = tot_dead = tot_waits = 0
|
||||
live_min, live_max = R, 0
|
||||
for seed in range(SEEDS):
|
||||
rng = random.Random(seed)
|
||||
prog = draw(rng)
|
||||
# a value's "version" = (reg, write index); the fold at the end reads every register
|
||||
# forward pass: for each instruction i and register r, next read of r's current value before its next write
|
||||
n = len(prog)
|
||||
# necessity: a version is NECESSARY if it reaches an address (a load's src) or the fold, transitively
|
||||
# compute transitively by backward dataflow over versions
|
||||
writes_at = {} # (i) -> reg written
|
||||
# build version ids: version of reg r valid after instruction i
|
||||
cur = {r: ('init', r) for r in range(R)}
|
||||
uses = {} # version -> list of (consumer index, consumer version or 'addr'/'fold')
|
||||
versions = set(cur.values())
|
||||
deps = {} # version -> set of versions it reads
|
||||
for i, (op, d, reads) in enumerate(prog):
|
||||
srcs = [cur[r] for r in reads]
|
||||
if op == 'load':
|
||||
v = ('load', i); deps[v] = set() # the returned word: its ADDRESS depends on srcs
|
||||
for s in srcs: uses.setdefault(s, []).append(('addr', i))
|
||||
else:
|
||||
v = (op, i); deps[v] = set(srcs)
|
||||
for s in srcs: uses.setdefault(s, []).append(('op', i))
|
||||
cur[d] = v; versions.add(v)
|
||||
fold = set(cur.values())
|
||||
# necessary = reaches an address or the fold
|
||||
necessary = set(fold)
|
||||
for v, us in uses.items():
|
||||
if any(k == 'addr' for k, _ in us): necessary.add(v)
|
||||
changed = True
|
||||
while changed:
|
||||
changed = False
|
||||
for v in list(versions):
|
||||
if v in necessary:
|
||||
for s in deps.get(v, ()):
|
||||
if s not in necessary: necessary.add(s); changed = True
|
||||
# per wait: the versions live at the load (written before it, read after it)
|
||||
last_read = {}
|
||||
for v, us in uses.items():
|
||||
last_read[v] = max(i for _, i in us)
|
||||
for v in fold: last_read[v] = n
|
||||
written_at = {v: (v[1] if v[0] != 'init' else -1) for v in versions}
|
||||
for i, (op, d, reads) in enumerate(prog):
|
||||
if op != 'load': continue
|
||||
live = [v for v in versions if written_at[v] < i and last_read.get(v, -1) > i]
|
||||
nec = [v for v in live if v in necessary]
|
||||
tot_live += len(live); tot_addr += len(nec); tot_waits += 1
|
||||
live_min = min(live_min, len(live)); live_max = max(live_max, len(live))
|
||||
dead = sum(1 for v in versions if v[0] not in ('init',) and v not in uses and v not in fold)
|
||||
tot_dead += dead
|
||||
print(f'R = {R}, NPROG = {N}, {SEEDS} drawn programs, {tot_waits} waits')
|
||||
print(f'live values at a wait: mean {tot_live/tot_waits:.1f} of {R} (min {live_min}, max {live_max}); of which necessary (reach a later address or the result): {tot_addr/tot_waits:.1f}')
|
||||
print(f'dead writes (overwritten before any read): {tot_dead/SEEDS:.1f} per {N}-instruction block ({100*tot_dead/SEEDS/N:.1f} percent)')
|
||||
9
tools/chip-model/rtl/flow/pod.sh
Executable file
9
tools/chip-model/rtl/flow/pod.sh
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
#!/usr/bin/env bash
|
||||
# pod.sh: bootstrap a rented pod started from openroad/orfs:latest (RunPod, root): iverilog, /work -> this dir.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
command -v iverilog >/dev/null || { apt-get update -qq >/dev/null 2>&1; apt-get install -y -qq iverilog rsync python3 >/dev/null 2>&1; }
|
||||
[ -e /work ] || ln -s "$(pwd)" /work
|
||||
export PATH=/OpenROAD-flow-scripts/tools/install/OpenROAD/bin:/OpenROAD-flow-scripts/tools/install/yosys/bin:$PATH
|
||||
echo "pod ready: $(nproc) cores, $(free -g | awk '/Mem/{print $2}') GB, yosys $(yosys -V | cut -d' ' -f2), $(which iverilog)"
|
||||
|
|
@ -4,6 +4,7 @@ set -euo pipefail
|
|||
name=$1; tag=$2; shift 2
|
||||
out=/work/sim/$name
|
||||
simcells=$(yosys-config --datdir)/simcells.v
|
||||
iverilog -g2005 -I /work/tb -o $out/sim_$tag $out/sim_net.v /work/tb/tb_$(sed -n "s/^$name \([^ ]*\) .*/\1/p" /work/flow/designs.txt).v $simcells
|
||||
tbfile=${TB:-/work/tb/tb_$(sed -n "s/^$name \([^ ]*\) .*/\1/p" /work/flow/designs.txt).v}
|
||||
iverilog -g2005 -I /work/tb -o $out/sim_$tag $out/sim_net.v /work/flow/asap7_icg_model.v $tbfile $simcells
|
||||
( cd $out && vvp -n sim_$tag "$@" | tee sim_$tag.log && mv dump.vcd $tag.vcd )
|
||||
ls -la $out/$tag.vcd
|
||||
|
|
|
|||
82
tools/chip-model/rtl/rtl/core_tm.v
Normal file
82
tools/chip-model/rtl/rtl/core_tm.v
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
// The adversary's time-multiplexed core: ONE execution port (every class unit, once) serving LANES lanes' instruction
|
||||
// streams round-robin, each lane's state (REGS x 32-bit) kept in its own bank; the imem and sequencer shared.
|
||||
// One lane-op per cycle. Compared with core_v6 at the same LANES x REGS this removes LANES-1 copies of the units and
|
||||
// keeps the register state and the imem: the energy per lane-op is the state's cost plus one unit set's.
|
||||
// The butterfly shuffle across lanes needs every lane's source register in the same cycle, so the shuffle reads the
|
||||
// bank-wide source column (as the SIMD core does) and the lane in turn takes its word. Loads return on ld_val.
|
||||
`include "lane_common.vh"
|
||||
module core_tm #(parameter LANES = 8, parameter LOG_LANES = 3, parameter REGS = 64, parameter LOG_REGS = 6,
|
||||
parameter IW = 40, parameter IMEM_LOG = 8) (
|
||||
input clk, input rst, input run,
|
||||
input prog_we, input [9:0] prog_addr, input [IW-1:0] prog_data,
|
||||
input cfg_en, input [9:0] cfg_n, input [31:0] cfg_m, input [4:0] cfg_r, input [31:0] cfg_wm, input [31:0] cfg_off, input [31:0] cfg_mask, input [63:0] cfg_sel,
|
||||
input [31:0] ld_val,
|
||||
output [31:0] addr, output [31:0] out);
|
||||
localparam IMEM = 1 << IMEM_LOG;
|
||||
reg [IW-1:0] imem [0:IMEM-1];
|
||||
reg [IMEM_LOG-1:0] pc; reg [IMEM_LOG-1:0] n_q; reg [IW-1:0] ir; reg [LOG_LANES-1:0] lane;
|
||||
reg [31:0] m_q, wm_q, off_q, mask_q; reg [4:0] r_q;
|
||||
integer i;
|
||||
// the sequencer: the same instruction is issued to each lane in turn (LANES cycles per instruction)
|
||||
always @(posedge clk) begin
|
||||
if (prog_we) imem[prog_addr[IMEM_LOG-1:0]] <= prog_data;
|
||||
if (rst) begin pc <= 0; ir <= 0; lane <= 0; n_q <= {IMEM_LOG{1'b1}}; m_q <= 32'h9e3779b1; r_q <= 5'd13; wm_q <= 32'h0fffffc0; off_q <= 3; mask_q <= 32'h0fffffff; end
|
||||
else begin
|
||||
if (cfg_en) begin n_q <= cfg_n[IMEM_LOG-1:0]; m_q <= cfg_m | 1; r_q <= cfg_r; wm_q <= cfg_wm; off_q <= cfg_off; mask_q <= cfg_mask; end
|
||||
if (run) begin
|
||||
if (lane == {LOG_LANES{1'b1}}) begin ir <= imem[pc]; pc <= (pc == n_q) ? {IMEM_LOG{1'b0}} : pc + 1'b1; end
|
||||
lane <= lane + 1'b1;
|
||||
end
|
||||
end
|
||||
end
|
||||
wire [3:0] op = ir[3:0];
|
||||
wire [LOG_REGS-1:0] dst = ir[4 +: LOG_REGS]; wire [LOG_REGS-1:0] src = ir[4+LOG_REGS +: LOG_REGS]; wire [LOG_REGS-1:0] src2 = ir[4+2*LOG_REGS +: LOG_REGS];
|
||||
wire [4:0] imm = ir[4+3*LOG_REGS +: 5]; wire [7:0] aux = ir[9+3*LOG_REGS +: 8];
|
||||
wire is_load = (op == 4'd12);
|
||||
wire [4:0] rn = (imm == 0) ? 5'd1 : imm;
|
||||
wire [LOG_LANES-1:0] smask = imm[LOG_LANES-1:0];
|
||||
// the banked state: one bank per lane, read through the lane select (a chip's SRAM bank select)
|
||||
reg [31:0] rf [0:LANES*REGS-1];
|
||||
wire [31:0] d = rf[lane*REGS + dst];
|
||||
wire [31:0] s = rf[lane*REGS + src];
|
||||
wire [31:0] s2 = rf[lane*REGS + src2];
|
||||
wire [31:0] sx = rf[(lane ^ smask)*REGS + src]; // the shuffle partner's source word
|
||||
// the one execution port
|
||||
function [7:0] pick; input [63:0] b; input [3:0] k; reg [7:0] v;
|
||||
begin v = b[8*k[2:0] +: 8]; pick = k[3] ? {8{v[7]}} : v; end
|
||||
endfunction
|
||||
wire [4:0] sn = (s[4:0] == 0) ? 5'd1 : s[4:0];
|
||||
wire mad = (op == 4'd8);
|
||||
wire [63:0] p = (mad ? s : d) * (mad ? s2 : s);
|
||||
wire [63:0] bytes = {s, d}; wire [15:0] sel = {aux, aux};
|
||||
wire [31:0] prm = {pick(bytes, sel[15:12]), pick(bytes, sel[11:8]), pick(bytes, sel[7:4]), pick(bytes, sel[3:0])};
|
||||
reg [31:0] lp; integer b;
|
||||
always @* for (b = 0; b < 32; b = b + 1) lp[b] = aux[{d[b], s[b], s2[b]}];
|
||||
reg [31:0] r;
|
||||
always @* begin
|
||||
case (op)
|
||||
4'd0, 4'd13: r = d + s;
|
||||
4'd1, 4'd15: r = d - s;
|
||||
4'd2, 4'd14: r = d ^ s;
|
||||
4'd3: r = d | s;
|
||||
4'd4: r = `ROTL32(d, rn);
|
||||
4'd5: r = `ROTR32(d, sn);
|
||||
4'd6: r = p[31:0];
|
||||
4'd7: r = p[63:32];
|
||||
4'd8: r = p[31:0] + d;
|
||||
4'd9: r = d ^ sx;
|
||||
4'd10: r = prm;
|
||||
4'd11: r = lp;
|
||||
default: r = ld_val ^ (32'h9e3779b9 * (lane + 1));
|
||||
endcase
|
||||
end
|
||||
wire [31:0] fx = s * m_q;
|
||||
wire [4:0] frn = (r_q == 0) ? 5'd1 : r_q;
|
||||
wire [31:0] fy = `ROTL32(fx, frn);
|
||||
assign addr = is_load ? (((fy & wm_q) | off_q) & mask_q) : 32'd0;
|
||||
always @(posedge clk) begin
|
||||
if (rst) begin for (i = 0; i < LANES*REGS; i = i + 1) rf[i] <= 32'h9e3779b9 * (i + 1); end
|
||||
else if (run) rf[lane*REGS + dst] <= r;
|
||||
end
|
||||
assign out = r;
|
||||
endmodule
|
||||
7
tools/chip-model/rtl/rtl/core_tm_8r64.v
Normal file
7
tools/chip-model/rtl/rtl/core_tm_8r64.v
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
`include "core_tm.v"
|
||||
module core_tm_8r64(input clk, input rst, input run, input prog_we, input [9:0] prog_addr, input [39:0] prog_data,
|
||||
input cfg_en, input [9:0] cfg_n, input [31:0] cfg_m, input [4:0] cfg_r, input [31:0] cfg_wm, input [31:0] cfg_off, input [31:0] cfg_mask, input [63:0] cfg_sel,
|
||||
input [31:0] ld_val, output [31:0] addr, output [31:0] out);
|
||||
core_tm #(.LANES(8), .LOG_LANES(3), .REGS(64), .LOG_REGS(6), .IW(40)) c(.clk(clk), .rst(rst), .run(run), .prog_we(prog_we), .prog_addr(prog_addr), .prog_data(prog_data),
|
||||
.cfg_en(cfg_en), .cfg_n(cfg_n), .cfg_m(cfg_m), .cfg_r(cfg_r), .cfg_wm(cfg_wm), .cfg_off(cfg_off), .cfg_mask(cfg_mask), .cfg_sel(cfg_sel), .ld_val(ld_val), .addr(addr), .out(out));
|
||||
endmodule
|
||||
7
tools/chip-model/rtl/rtl/core_v6_8cs64.v
Normal file
7
tools/chip-model/rtl/rtl/core_v6_8cs64.v
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
`include "core_v6.v"
|
||||
module core_v6_8cs64(input clk, input rst, input run, input prog_we, input [9:0] prog_addr, input [40-1:0] prog_data,
|
||||
input cfg_en, input [9:0] cfg_n, input [63:0] cfg_sel, input [31:0] cfg_m, input [4:0] cfg_r, input [31:0] cfg_wm, input [31:0] cfg_off, input [31:0] cfg_mask,
|
||||
input [31:0] ld_val, output [31:0] addr, output [31:0] out);
|
||||
core_v6 #(.LANES(8), .LOG_LANES(3), .REGS(64), .LOG_REGS(6), .IW(40), .IMEM_LOG(9)) c(.clk(clk), .rst(rst), .run(run), .prog_we(prog_we), .prog_addr(prog_addr), .prog_data(prog_data),
|
||||
.cfg_en(cfg_en), .cfg_n(cfg_n), .cfg_sel(cfg_sel), .cfg_m(cfg_m), .cfg_r(cfg_r), .cfg_wm(cfg_wm), .cfg_off(cfg_off), .cfg_mask(cfg_mask), .ld_val(ld_val), .addr(addr), .out(out));
|
||||
endmodule
|
||||
123
tools/chip-model/rtl/rtl/fp32_units.v
Normal file
123
tools/chip-model/rtl/rtl/fp32_units.v
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
// The adversary's simplified FP32 units for the mixed-resource lane's candidate (class-v6-mixedfp): inputs are
|
||||
// f(x) = as_float((x & 0x807FFFFF) | ((96 + ((x >> 23) & 63)) << 23)): never zero, denormal, NaN or Inf; exponents
|
||||
// in [96, 159]; results normal or +0 (exact cancellation). The units drop NaN/Inf/denormal handling and the flags,
|
||||
// keep the full 24-bit mantissa path, a full alignment and a full normaliser (the mantissas are uniform), RNE.
|
||||
// Each lane reads two or three registers of an 8 x 32-bit window, applies f(), computes, xors the bits into dst.
|
||||
`include "lane_common.vh"
|
||||
// ---- the shared pieces ----
|
||||
module fp_unpack(input [31:0] x, output s, output [8:0] e, output [23:0] m);
|
||||
assign s = x[31];
|
||||
assign e = 9'd96 + {3'b0, x[28:23]}; // the masked exponent, 96..159
|
||||
assign m = {1'b1, x[22:0]};
|
||||
endmodule
|
||||
module lzc48(input [47:0] v, output reg [5:0] n); // leading-zero count (v != 0)
|
||||
integer i; always @* begin n = 6'd47; for (i = 47; i >= 0; i = i - 1) if (v[i]) begin n = 6'd47 - i; i = -1; end end
|
||||
endmodule
|
||||
module lzc32(input [31:0] v, output reg [5:0] n);
|
||||
integer i; always @* begin n = 6'd31; for (i = 31; i >= 0; i = i - 1) if (v[i]) begin n = 6'd31 - i; i = -1; end end
|
||||
endmodule
|
||||
// ---- the FMA: fma(a, b, c) = a*b + c, one rounding (RNE), exponents in the lane's ranges ----
|
||||
module fp_fma(input [31:0] a, input [31:0] b, input [31:0] c, output [31:0] y);
|
||||
wire sa, sb, sc; wire [8:0] ea, eb, ec; wire [23:0] ma, mb, mc;
|
||||
fp_unpack ua(a, sa, ea, ma); fp_unpack ub(b, sb, eb, mb); fp_unpack uc(c, sc, ec, mc);
|
||||
wire [47:0] prod = ma * mb; // 48-bit product, binary point after bit 46
|
||||
wire sp = sa ^ sb;
|
||||
wire [9:0] ep = {1'b0, ea} + {1'b0, eb} - 10'd127; // product exponent (bias kept), 65..192
|
||||
// align the addend to the product: a 100-bit window keeps full precision for exponent gaps up to about 96 (the lane's bound)
|
||||
wire [9:0] diff = (ep >= {1'b0, ec}) ? ep - {1'b0, ec} : {1'b0, ec} - ep;
|
||||
wire prod_big = (ep >= {1'b0, ec});
|
||||
wire [99:0] pw = {2'b0, prod, 50'b0};
|
||||
wire [99:0] cw = {2'b0, mc, 24'b0, 50'b0}; // the addend at the product's scale when exponents equal
|
||||
wire [6:0] sh = (diff > 10'd99) ? 7'd99 : diff[6:0];
|
||||
wire [99:0] smw = prod_big ? (cw >> sh) : (pw >> sh);
|
||||
wire [99:0] bgw = prod_big ? pw : cw;
|
||||
wire sbig = prod_big ? sp : sc; wire ssmall = prod_big ? sc : sp;
|
||||
wire [9:0] ebig = prod_big ? ep : {1'b0, ec};
|
||||
wire [100:0] sum = (sbig == ssmall) ? ({1'b0, bgw} + {1'b0, smw}) : ({1'b0, bgw} - {1'b0, smw});
|
||||
wire [100:0] mag = sum[100] ? (~sum + 1'b1) : sum; // two's complement when the subtraction went negative
|
||||
wire ssum = sum[100] ? ssmall : sbig;
|
||||
// normalise: find the leading one in the 101-bit magnitude
|
||||
reg [6:0] lz; integer i;
|
||||
always @* begin lz = 7'd100; for (i = 100; i >= 0; i = i - 1) if (mag[i]) begin lz = 7'd100 - i; i = -1; end end
|
||||
wire [100:0] norm = mag << lz; // leading one at bit 100
|
||||
wire [23:0] mant = norm[100:77];
|
||||
wire guard = norm[76]; wire sticky = |norm[75:0];
|
||||
wire round_up = guard & (sticky | mant[0]);
|
||||
wire [24:0] mr = {1'b0, mant} + round_up;
|
||||
wire carry = mr[24];
|
||||
wire [9:0] eres = ebig + 10'd2 - lz + carry; // the leading one of bgw sat at bit 98 (two headroom bits)
|
||||
wire zero = (mag == 0);
|
||||
wire [7:0] eout = eres[7:0];
|
||||
assign y = zero ? 32'h0 : {ssum, eout, carry ? mr[23:1] : mr[22:0]};
|
||||
endmodule
|
||||
// ---- the adder and the multiplier as their own units ----
|
||||
module fp_add(input [31:0] a, input [31:0] b, output [31:0] y);
|
||||
wire sa, sb; wire [8:0] ea, eb; wire [23:0] ma, mb;
|
||||
fp_unpack ua(a, sa, ea, ma); fp_unpack ub(b, sb, eb, mb);
|
||||
wire abig = (ea > eb) || (ea == eb && ma >= mb);
|
||||
wire [8:0] ebig = abig ? ea : eb; wire [8:0] esm = abig ? eb : ea;
|
||||
wire [23:0] mbig = abig ? ma : mb; wire [23:0] msm = abig ? mb : ma;
|
||||
wire sbig = abig ? sa : sb; wire ssm = abig ? sb : sa;
|
||||
wire [8:0] diff = ebig - esm; wire [6:0] sh = (diff > 9'd70) ? 7'd70 : diff[6:0];
|
||||
wire [73:0] bw = {1'b0, mbig, 49'b0}; wire [73:0] sw = {1'b0, msm, 49'b0} >> sh;
|
||||
wire [74:0] sum = (sbig == ssm) ? ({1'b0, bw} + {1'b0, sw}) : ({1'b0, bw} - {1'b0, sw});
|
||||
reg [6:0] lz; integer i;
|
||||
always @* begin lz = 7'd74; for (i = 74; i >= 0; i = i - 1) if (sum[i]) begin lz = 7'd74 - i; i = -1; end end
|
||||
wire [74:0] norm = sum << lz;
|
||||
wire [23:0] mant = norm[74:51]; wire guard = norm[50]; wire sticky = |norm[49:0];
|
||||
wire round_up = guard & (sticky | mant[0]);
|
||||
wire [24:0] mr = {1'b0, mant} + round_up; wire carry = mr[24];
|
||||
wire [9:0] eres = {1'b0, ebig} + 10'd1 - lz + carry;
|
||||
wire zero = (sum == 0);
|
||||
assign y = zero ? 32'h0 : {sbig, eres[7:0], carry ? mr[23:1] : mr[22:0]};
|
||||
endmodule
|
||||
module fp_mul(input [31:0] a, input [31:0] b, output [31:0] y);
|
||||
wire sa, sb; wire [8:0] ea, eb; wire [23:0] ma, mb;
|
||||
fp_unpack ua(a, sa, ea, ma); fp_unpack ub(b, sb, eb, mb);
|
||||
wire [47:0] prod = ma * mb;
|
||||
wire top = prod[47];
|
||||
wire [23:0] mant = top ? prod[47:24] : prod[46:23];
|
||||
wire guard = top ? prod[23] : prod[22]; wire sticky = top ? |prod[22:0] : |prod[21:0];
|
||||
wire round_up = guard & (sticky | mant[0]);
|
||||
wire [24:0] mr = {1'b0, mant} + round_up; wire carry = mr[24];
|
||||
wire [9:0] eres = {1'b0, ea} + {1'b0, eb} - 10'd127 + top + carry;
|
||||
assign y = {sa ^ sb, eres[7:0], carry ? mr[23:1] : mr[22:0]};
|
||||
endmodule
|
||||
// ---- int32 to float, RNE ----
|
||||
module fp_cvt(input [31:0] a, output [31:0] y);
|
||||
wire s = a[31]; wire [31:0] mag = s ? (~a + 1'b1) : a;
|
||||
wire [5:0] lz; lzc32 l(mag, lz);
|
||||
wire [31:0] norm = mag << lz; // leading one at bit 31
|
||||
wire [23:0] mant = norm[31:8]; wire guard = norm[7]; wire sticky = |norm[6:0];
|
||||
wire round_up = guard & (sticky | mant[0]);
|
||||
wire [24:0] mr = {1'b0, mant} + round_up; wire carry = mr[24];
|
||||
wire [7:0] e = 8'd127 + 8'd31 - lz + carry;
|
||||
assign y = (mag == 0) ? 32'h0 : {s, e, carry ? mr[23:1] : mr[22:0]};
|
||||
endmodule
|
||||
// ---- the lane: op 0 fadd, 1 fmul, 2 ffma, 3 fcvt; d ^= bits(result) ----
|
||||
module lane_fp32(
|
||||
input clk, input rst,
|
||||
input [1:0] op, input [2:0] dst, input [2:0] src, input [2:0] src2,
|
||||
input ld_en, input [31:0] ld_val,
|
||||
output [31:0] out);
|
||||
reg [31:0] rf [0:7];
|
||||
reg [1:0] op_q; reg [2:0] dst_q, src_q, src2_q; reg ld_q; reg [31:0] ld_val_q;
|
||||
integer i;
|
||||
always @(posedge clk) begin
|
||||
if (rst) begin op_q <= 0; dst_q <= 0; src_q <= 0; src2_q <= 0; ld_q <= 0; ld_val_q <= 0; end
|
||||
else begin op_q <= op; dst_q <= dst; src_q <= src; src2_q <= src2; ld_q <= ld_en; ld_val_q <= ld_val; end
|
||||
end
|
||||
wire [31:0] d = rf[dst_q]; wire [31:0] s = rf[src_q]; wire [31:0] s2 = rf[src2_q];
|
||||
wire [31:0] ya, ym, yf, yc;
|
||||
fp_add A(d, s, ya);
|
||||
fp_mul M(d, s, ym);
|
||||
fp_fma F(s, s2, d, yf);
|
||||
fp_cvt C(s, yc);
|
||||
reg [31:0] res;
|
||||
always @* case (op_q) 2'd0: res = d ^ ya; 2'd1: res = d ^ ym; 2'd2: res = d ^ yf; default: res = d ^ yc; endcase
|
||||
always @(posedge clk) begin
|
||||
if (rst) begin for (i = 0; i < 8; i = i + 1) rf[i] <= 32'h9e3779b9 * (i + 1) + 9; end
|
||||
else rf[dst_q] <= ld_q ? ld_val_q : res;
|
||||
end
|
||||
assign out = res;
|
||||
endmodule
|
||||
|
|
@ -21,10 +21,32 @@ module tb;
|
|||
@(negedge clk); cfg_en = 1; cfg_n = `NPROG - 1; cfg_sel = `SEL; cfg_m = 32'h9e3779b1; cfg_r = 5'd13; cfg_wm = 32'h0fffffc0; cfg_off = 3; cfg_mask = 32'h0fffffff;
|
||||
@(negedge clk); cfg_en = 0;
|
||||
// the program: `NPROG instructions drawn with the class v4 weights
|
||||
`ifdef CS
|
||||
// the connected-state draw: step = load + 27-instruction spine block; `NPROG = 16 x 28 = 448
|
||||
begin : cs
|
||||
integer st, q, last0, last1, last2, last3, mreg, areg, dreg, sreg, s2reg;
|
||||
areg = 0; mreg = 1;
|
||||
for (st = 0; st < `NPROG / 28; st = st + 1) begin
|
||||
@(negedge clk); w = {$random, $random}; mreg = $random & 63;
|
||||
prog_we = 1; prog_addr = st*28; prog_data = {w[`IW-1:4], 4'd12}; prog_data[4 +: 6] = mreg; prog_data[10 +: 6] = areg; // load: dst m_j, src a_j
|
||||
last0 = mreg; last1 = mreg; last2 = mreg; last3 = mreg;
|
||||
for (q = 0; q < 27; q = q + 1) begin
|
||||
@(negedge clk); w = {$random, $random}; opc = draw_op($random); dreg = $random & 63;
|
||||
case ($random & 3) 0: sreg = last0; 1: sreg = last1; 2: sreg = last2; default: sreg = last3; endcase
|
||||
s2reg = last0;
|
||||
if (q == 26 && !(opc == 4'd0 || opc == 4'd1 || opc == 4'd2 || opc == 4'd8 || opc == 4'd9)) opc = 4'd0; // the last instruction injects
|
||||
prog_we = 1; prog_addr = st*28 + 1 + q; prog_data = {w[`IW-1:4], opc}; prog_data[4 +: 6] = dreg; prog_data[10 +: 6] = sreg; prog_data[16 +: 6] = s2reg;
|
||||
last3 = last2; last2 = last1; last1 = last0; last0 = dreg;
|
||||
end
|
||||
areg = last0;
|
||||
end
|
||||
end
|
||||
`else
|
||||
for (k = 0; k < `NPROG; k = k + 1) begin
|
||||
@(negedge clk); w = {$random, $random}; opc = (loads && (k % 16 == 15)) ? 4'd12 : draw_op($random);
|
||||
prog_we = 1; prog_addr = k; prog_data = {w[`IW-1:4], opc};
|
||||
end
|
||||
`endif
|
||||
@(negedge clk); prog_we = 0; run = 1;
|
||||
for (n = 0; n < cycles; n = n + 1) begin
|
||||
@(negedge clk); ld_val = $random; acc = acc ^ out ^ addr;
|
||||
|
|
|
|||
6
tools/chip-model/rtl/tb/tb_core_tm_8r64.v
Normal file
6
tools/chip-model/rtl/tb/tb_core_tm_8r64.v
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
`define TOP core_tm_8r64
|
||||
`define HALF 750
|
||||
`define IW 40
|
||||
`define NPROG 256
|
||||
`define SEL 64'hfedcba9876543210
|
||||
`include "tb_core_common.vh"
|
||||
3
tools/chip-model/rtl/tb/tb_core_v6_8_legacy.v
Normal file
3
tools/chip-model/rtl/tb/tb_core_v6_8_legacy.v
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
`define TOP core_v6_8
|
||||
`define HALF 750
|
||||
`include "tb_core_legacy.vh"
|
||||
7
tools/chip-model/rtl/tb/tb_core_v6_8cs64.v
Normal file
7
tools/chip-model/rtl/tb/tb_core_v6_8cs64.v
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
`define TOP core_v6_8cs64
|
||||
`define HALF 750
|
||||
`define IW 40
|
||||
`define NPROG 448
|
||||
`define CS 1
|
||||
`define SEL 64'hfedcba9876543210
|
||||
`include "tb_core_common.vh"
|
||||
22
tools/chip-model/rtl/tb/tb_lane_fp32.v
Normal file
22
tools/chip-model/rtl/tb/tb_lane_fp32.v
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
`timescale 1ps/1ps
|
||||
module tb;
|
||||
reg clk = 0, rst = 1; reg [1:0] op = 0; reg [2:0] dst = 0, src = 0, src2 = 0; reg ld_en = 0; reg [31:0] ld_val = 0;
|
||||
wire [31:0] out;
|
||||
lane_fp32 dut(.clk(clk), .rst(rst), .op(op), .dst(dst), .src(src), .src2(src2), .ld_en(ld_en), .ld_val(ld_val), .out(out));
|
||||
integer n, fixed_op, cycles; reg [31:0] acc = 0;
|
||||
always #1000 clk = ~clk;
|
||||
// a reference check of the units against the host's float arithmetic is the mixed lane's own (the ranges are its);
|
||||
// this bench drives random registers and reports the checksum
|
||||
initial begin
|
||||
if (!$value$plusargs("op=%d", fixed_op)) fixed_op = -1;
|
||||
if (!$value$plusargs("cycles=%d", cycles)) cycles = 3000;
|
||||
$dumpfile("dump.vcd"); $dumpvars(0, tb.dut);
|
||||
repeat (4) @(negedge clk); rst = 0;
|
||||
for (n = 0; n < cycles; n = n + 1) begin
|
||||
@(negedge clk);
|
||||
op = (fixed_op < 0) ? $random : fixed_op; dst = $random; src = $random; src2 = $random;
|
||||
ld_en = (($random & 7) == 0); ld_val = $random; acc = acc ^ out;
|
||||
end
|
||||
$display("CHECKSUM %08x", acc); $finish;
|
||||
end
|
||||
endmodule
|
||||
|
|
@ -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 |
|
||||
|
|
|
|||
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
26
tools/ci/batches/floor-k-20261008-rows-repeat.json
Normal file
26
tools/ci/batches/floor-k-20261008-rows-repeat.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"run_id": "floor-k-20261008-rows-repeat",
|
||||
"manifest_sha": "86e5b0fb",
|
||||
"cut_tip": "class-v6-floor-k 86e5b0fb (the amendment carries shadow-k.md; floor lane 2's rows ADV-05 and ADV-06 move with it on its word, method model, no decision changed; the steward's rows no longer cite the directory since d2e1c192)",
|
||||
"evidence_dir": "docs/analysis/class-v6/floor/shadow-k.md",
|
||||
"boxes": [],
|
||||
"cells": [
|
||||
{
|
||||
"cell": "adversary:mf-placed",
|
||||
"cases": [
|
||||
"ADV-05"
|
||||
],
|
||||
"status": "RUNNING",
|
||||
"method": "model",
|
||||
"evidence": "docs/analysis/class-v6/floor/shadow-k.md",
|
||||
"note": "repeat for ADV-05 at this manifest on floor lane 2's word (placed and routed RTL on ASAP7, a model); the rtl directory tools/chip-model/rtl/ is the flow, cited by its document since the recorder takes files only"
|
||||
},
|
||||
{
|
||||
"cell": "review:k-lane-shadow-k",
|
||||
"status": "NOT RUN",
|
||||
"method": "model",
|
||||
"evidence": "docs/analysis/class-v6/floor/shadow-k.md",
|
||||
"note": "repeat of team-2026-10-08 for ADV-06 at this manifest on floor lane 2's word; no decision changed"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
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');
|
||||
|
|
|
|||
|
|
@ -36,8 +36,9 @@ BS_ROUTE_STATE_1="free=1 slots=2 load1=80" BS_ROUTE_STATE_2="free=3 slots=3 load
|
|||
BS_ROUTE_STATE_1="free=2 slots=2 load1=70" BS_ROUTE_STATE_2="free=3 slots=3 load1=5" expect "load 70 stays on build-1 (the line is 80)" build 1 0
|
||||
# the ssh path itself under the hook's shell (/bin/bash is 3.2 on the Mac; the pool lane found `BS_SSH_OPTS[@]: unbound variable`
|
||||
# at the first unpinned route, 7 Oct 2026): a host file pointing at a port nothing answers on must read "down" in a few seconds,
|
||||
# not die on an unset array
|
||||
printf 'build@127.0.0.1\n' > "$t/hosts"
|
||||
# not die on an unset array. Port 1 in the ssh URI form (8 Oct 2026, the workers-page lane): on a build box sshd answers on
|
||||
# 127.0.0.1:22 and a forwarded agent logs in, so the bare address read "free=..." there and the gate was red only off the Mac
|
||||
printf 'ssh://build@127.0.0.1:1\n' > "$t/hosts"
|
||||
out=$(IGNEUM_BUILD_KEY="$t/no-such-key" /bin/bash -c '. infra/build-server/lib.sh; bs_box_state 1' 2>&1 || true)
|
||||
if [ "$out" = down ]; then echo "route-spill: the ssh probe under /bin/bash $(/bin/bash -c 'echo $BASH_VERSION') reads an unreachable box as down"
|
||||
else echo "route-spill: the ssh probe under /bin/bash failed: '$out' (expected 'down')" >&2; fail=1; fi
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -631,7 +631,7 @@
|
|||
}
|
||||
},
|
||||
"pc:install-update": {
|
||||
"command": "signed jobs and relay runs on PC 1 (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)",
|
||||
"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"
|
||||
|
|
@ -646,17 +646,68 @@
|
|||
"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"
|
||||
}
|
||||
},
|
||||
"review:k-lane-shadow-k": {
|
||||
"command": "floor lane 2's placed and routed cores on ASAP7 in tools/chip-model/rtl (the rows in docs/analysis/class-v6/floor/shadow-k.md); the register landing 50ff1611f recorded ADV-06 against it before the recorder existed",
|
||||
"box_class": "none",
|
||||
"fixtures": [],
|
||||
"cases": [
|
||||
"ADV-06"
|
||||
],
|
||||
"coverage": {
|
||||
"ADV-06": "partial: the placed gated cores and the adversary's forms are modelled in shadow-k.md; the independent review remains"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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-06": "process-advantage separation is the adversary lanes' chip study",
|
||||
"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",
|
||||
|
|
@ -743,7 +794,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",
|
||||
|
|
@ -762,9 +812,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",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"><meta name="robots" content="noindex">
|
||||
<title>Igneum Workers</title>
|
||||
<!-- the main site's icon set (site/index.html's head, the ember mark); the files sit next to this page in /srv/workers -->
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Crect width='1024' height='1024' fill='%230C0C0E'/%3E%3Cg transform='translate(166.95 166.95) scale(6.901)'%3E%3Cpolygon points='50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34' fill='%23F2541B'/%3E%3Cpolygon points='50,42 59,58 50,82 41,58' fill='%230C0C0E'/%3E%3C/g%3E%3C/svg%3E" type="image/svg+xml">
|
||||
<link rel="icon" href="/favicon.ico" sizes="48x48">
|
||||
<link rel="icon" href="/favicon-32.png" type="image/png" sizes="32x32">
|
||||
<link rel="icon" href="/icon-192.png" type="image/png" sizes="192x192">
|
||||
<link rel="icon" href="/icon-512.png" type="image/png" sizes="512x512">
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Unbounded:wght@500;700&family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600&display=swap">
|
||||
<style>
|
||||
:root {
|
||||
|
|
@ -195,17 +202,31 @@ function arc(pct, cls) {
|
|||
}
|
||||
const tone = (pct, warn = 70, bad = 90) => pct >= bad ? 'bad' : pct >= warn ? 'warn' : '';
|
||||
|
||||
const FEEDS = { 'igneum-build-1': 'https://build.igneum.network/workers.json', 'igneum-build-2': 'https://build-2.igneum.network/workers.json', 'igneum-build-3': 'https://build-3.igneum.network/workers.json' };
|
||||
// one card per entry of boxes[] in the merged workers.json (merge-workers.mjs on build-1, every 20 s); no per-box feed map (8 October 2026:
|
||||
// the old three-entry map drew build-1 to build-3 only while the merge carried nine boxes)
|
||||
const STALE_S = 120;
|
||||
const BOX_NOTES = { 'igneum-build-3': 'down since 16:40 UK, Hetzner switch fault' };
|
||||
const PROVISIONING = 'provisioning, first build logged when the sources land';
|
||||
function boxState(b) { // live | down | provisioning, with the line the cards print
|
||||
const age = b && b.collected_at ? (Date.now() - Date.parse(b.collected_at)) / 1000 : null;
|
||||
const stale = !b || b.down || !b.cores || (b.source && b.source.ok === false) || age === null || age > STALE_S;
|
||||
if (b && stale && BOX_NOTES[b.name]) return { kind: 'down', note: BOX_NOTES[b.name] };
|
||||
if (b && b.down) return { kind: 'down', note: String(b.reason || 'no answer from this box') };
|
||||
if (!b || !b.cores) return { kind: 'provisioning', note: PROVISIONING };
|
||||
if (!b.log || !b.log.present) return { kind: 'provisioning', note: PROVISIONING, live: true };
|
||||
return { kind: 'live', stale: age !== null && age > STALE_S };
|
||||
}
|
||||
const buildHeld = b => (b.slots && b.slots.held || []).filter(h => /^build-\d+$/.test(h.slot)).length;
|
||||
const boxesOf = d => (Array.isArray(d.boxes) && d.boxes.length ? d.boxes : d.box ? [{ ...d.box, source: d.sources && d.sources.box, headline: d.headline }] : []);
|
||||
function serverSection(b, i) {
|
||||
const id = `srv${i}`;
|
||||
if (!b || !b.cores) return `<section class="server" aria-label="${esc(b && b.name || 'build server')}"><div class="head"><div><div class="name">${esc(b && b.name || 'build server')}<small>build server</small></div><div class="facts"><span>${esc(b && b.source && b.source.error || 'provisioning, first build logged when the sources land')}</span></div></div></div><div class="cores">${Array.from({ length: 96 }, () => '<i style="--h:2%"></i>').join('')}</div><div class="corelabel"><span>no sample</span><span></span></div></section>`;
|
||||
const st = boxState(b);
|
||||
if (st.kind === 'down' || !b || !b.cores) return `<section class="server" aria-label="${esc(b && b.name || 'build server')}"><div class="head"><div><div class="name">${esc(b && b.name || 'build server')}<small>build server</small></div><div class="facts"><span>${esc(st.note || PROVISIONING)}</span></div></div></div><div class="cores">${Array.from({ length: 96 }, () => '<i style="--h:2%"></i>').join('')}</div><div class="corelabel"><span>no sample</span><span></span></div></section>`;
|
||||
const per = (b.cpu && b.cpu.per_core) || []; const n = Math.max(per.length, b.cores || 96);
|
||||
const temps = (b.temps || []).map(x => `${esc(x.name)} <b>${x.c.toFixed(0)}°</b>`);
|
||||
const m = b.mem || {}, d = b.disk || {}, sc = b.sccache;
|
||||
const hit = sc && sc.hit_rate_pct !== null && sc.hit_rate_pct !== undefined ? sc.hit_rate_pct : null;
|
||||
const stale = b.source && !b.source.ok;
|
||||
const stale = (b.source && !b.source.ok) || st.stale;
|
||||
return `<section class="server ${b.running && b.running.length ? 'hot' : ''}" aria-label="${esc(b.name)}">
|
||||
<div class="head"><div><div class="name">${esc(b.name)}<small>${esc(b.os || 'build server')}</small></div><div class="facts">${[`<b>${b.cores}</b> threads`, `<b>${esc(fmtUptime(b.uptime_s))}</b> up`, `load <b>${(b.load || []).map(x => Number(x).toFixed(1)).join(' / ')}</b>`, b.kernel ? `kernel <b>${esc(b.kernel)}</b>` : ''].filter(Boolean).map(x => `<span>${x}</span>`).join('')}</div></div>
|
||||
<div class="facts">${[...temps, b.net ? `net ↓<b>${esc(fmtBps(b.net.rx_bps))}</b> ↑<b>${esc(fmtBps(b.net.tx_bps))}</b>` : '', `<span title="${esc(b.collected_at)}" ${stale ? 'style="color:var(--warn)"' : ''}>${stale ? 'STALE, ' : ''}read ${esc(ago(b.collected_at))}</span>`].filter(Boolean).map(x => `<span>${x}</span>`).join('')}</div></div>
|
||||
|
|
@ -219,25 +240,39 @@ function serverSection(b, i) {
|
|||
<div class="g">${arc(b.slots ? (buildHeld(b) / Math.max(1, b.slots.count)) * 100 : 0, 'good')}<div><div class="l">Build slots</div><div class="v">${b.slots ? `${buildHeld(b)}/${b.slots.count}` : '–'}</div><div class="s">${b.queue && b.queue.length ? `${b.queue.length} waiting${b.queue.some(q => q.priority === 'gate') ? ', a gate first' : ''}` : 'nobody waiting'}</div></div></div>
|
||||
</div></section>`;
|
||||
}
|
||||
function renderServers() { const bx = boxesOf(D); $('servers').innerHTML = bx.length ? bx.map(serverSection).join('') : serverSection(null, 0); }
|
||||
// the Mac mini (igneum-mini, the Mac build box, M6): its own collector pushes mini.json to build-1; until then its card reads no report yet
|
||||
const MINI = { name: 'igneum-mini', label: 'the Mac build box, M6' };
|
||||
const miniLive = () => { const m = D && D.mini; return m && m.source && m.source.ok && m.cores ? m : null; };
|
||||
function miniSection() {
|
||||
const m = miniLive();
|
||||
if (!m) return `<section class="server" aria-label="${MINI.name}"><div class="head"><div><div class="name">${MINI.name}<small>${MINI.label}</small></div><div class="facts"><span>no report yet</span></div></div></div></section>`;
|
||||
return serverSection({ ...m, name: MINI.name, os: MINI.label, log: m.log || { present: true } }, 99);
|
||||
}
|
||||
function renderServers() { const bx = boxesOf(D); $('servers').innerHTML = (bx.length ? bx.map(serverSection).join('') : serverSection(null, 0)) + miniSection(); }
|
||||
|
||||
function slotBar(slots, count) {
|
||||
const held = new Map((slots && slots.held || []).map(h => [h.slot, h]));
|
||||
const names = Array.from({ length: count }, (_, i) => `build-${i}`);
|
||||
return `<div class="slots">${names.map(nm => `<i class="${held.has(nm) ? 'held' : ''}" title="${esc(nm)}${held.has(nm) ? ': ' + esc(held.get(nm).label) : ': free'}"></i>`).join('')}</div>`;
|
||||
}
|
||||
const MAC_COPY = 'nothing builds here except the one DMG; the mini carries macOS binaries from tonight';
|
||||
function renderCrew() {
|
||||
const mac = D.mac, pcs = (D.pcs && D.pcs.machines) || [];
|
||||
const cards = [];
|
||||
for (const bx of boxesOf(D)) {
|
||||
if (bx && bx.cores) cards.push(`<div class="w"><div class="top"><div><div class="card">${esc(bx.name)}</div><div class="meta">Hetzner, ${bx.cores} threads, ${esc(fmtBytes((bx.mem && bx.mem.total_kb || 0) * 1024))}, RAID 1 NVMe</div></div>${pill(bx.running && bx.running.length ? 'building' : 'idle', bx.running && bx.running.length ? 'running' : '')}</div><div class="doing">${bx.running && bx.running.length ? bx.running.map(r => esc(`${r.worktree || '?'}: ${kindLabel(r.kind)}`)).join('<br>') : 'Slot free. The next build-remote.sh or cross-remote.sh routed here takes it.'}</div>${slotBar(bx.slots, bx.slots ? bx.slots.count : 1)}<div class="foot"><span>${bx.recent ? bx.recent.length : 0} builds logged</span><span>${esc(ago(bx.collected_at))}</span></div></div>`);
|
||||
const st = boxState(bx);
|
||||
if (st.kind === 'down') { cards.push(`<div class="w"><div class="top"><div><div class="card">${esc(bx.name)}</div><div class="meta">build server</div></div>${pill('down', 'error')}</div><div class="doing">${esc(st.note)}</div></div>`); continue; }
|
||||
if (st.kind === 'provisioning' && !st.live) { cards.push(`<div class="w"><div class="top"><div><div class="card">${esc(bx && bx.name || 'build server')}</div><div class="meta">build server</div></div>${pill('provisioning', 'queued')}</div><div class="doing">${esc(st.note)}</div></div>`); continue; }
|
||||
if (bx && bx.cores) cards.push(`<div class="w"><div class="top"><div><div class="card">${esc(bx.name)}</div><div class="meta">Hetzner, ${bx.cores} threads, ${esc(fmtBytes((bx.mem && bx.mem.total_kb || 0) * 1024))}, RAID 1 NVMe</div></div>${pill(bx.running && bx.running.length ? 'building' : 'idle', bx.running && bx.running.length ? 'running' : '')}</div><div class="doing">${bx.running && bx.running.length ? bx.running.map(r => esc(`${r.worktree || '?'}: ${kindLabel(r.kind)}`)).join('<br>') : (st.kind === 'provisioning' ? esc(st.note) : 'Slot free. The next build-remote.sh or cross-remote.sh routed here takes it.')}</div>${slotBar(bx.slots, bx.slots ? bx.slots.count : 1)}<div class="foot"><span>${bx.recent ? bx.recent.length : 0} builds logged</span><span>${esc(ago(bx.collected_at))}</span></div></div>`);
|
||||
else cards.push(`<div class="w"><div class="top"><div><div class="card">${esc(bx && bx.name || 'build server')}</div><div class="meta">build server</div></div>${pill('unreachable', 'error')}</div><div class="doing">${esc(bx && bx.source && bx.source.error || 'no facts from this box')}</div></div>`);
|
||||
}
|
||||
if (mac) {
|
||||
const held = mac.slots.held || [], runs = held.filter(h => /^run-/.test(h.slot)), builds = held.filter(h => /^build-/.test(h.slot)), meas = held.filter(h => h.slot === 'measure');
|
||||
const doing = held.length ? held.map(h => `${esc(h.slot)}: ${esc((h.worktree ? h.worktree + ' ' : '') + (h.command || h.label || ''))}`.slice(0, 140)).join('<br>') : 'Nothing under with-lock.sh. macOS binaries and the DMG build here; everything else goes to the box or the PCs.';
|
||||
cards.push(`<div class="w"><div class="top"><div><div class="card">${esc(mac.name)}</div><div class="meta">this MacBook: the one DMG only, nothing else builds here (the rule since 7 October 2026); ${mac.build_slots} build slot${mac.build_slots === 1 ? '' : 's'}, 3 run slots</div></div>${pill(meas.length ? 'measuring' : builds.length ? 'building' : runs.length ? 'running' : 'idle', held.length ? 'running' : '')}</div><div class="doing">${doing}</div><div class="slots">${['build-0', 'build-1', 'build-2', 'run-0', 'run-1', 'run-2', 'measure'].map(nm => { const h = held.find(x => x.slot === nm); return `<i class="${h ? (nm === 'measure' ? 'measure' : nm.startsWith('run') ? 'run' : 'held') : ''}" title="${esc(nm)}${h ? ': ' + esc(h.label) : ': free'}"></i>`; }).join('')}</div><div class="foot"><span>${mac.queue.length ? mac.queue.length + ' waiting for a slot' : 'nobody waiting'}</span><span>${esc(ago(mac.collected_at))}</span></div></div>`);
|
||||
} else cards.push(`<div class="w"><div class="top"><div><div class="card">MacBook-Pro</div><div class="meta">this Mac</div></div>${pill('no push', 'error')}</div><div class="doing">${esc(D.sources && D.sources.mac && D.sources.mac.error || 'the Mac pusher has not written yet (launchd com.igneum.workers-push, every 60 s)')}</div></div>`);
|
||||
const doing = held.length ? held.map(h => `${esc(h.slot)}: ${esc((h.worktree ? h.worktree + ' ' : '') + (h.command || h.label || ''))}`.slice(0, 140)).join('<br>') : MAC_COPY;
|
||||
cards.push(`<div class="w"><div class="top"><div><div class="card">${esc(mac.name)}</div><div class="meta">this MacBook, ${mac.build_slots} build slot${mac.build_slots === 1 ? '' : 's'}, 3 run slots</div></div>${pill(meas.length ? 'measuring' : builds.length ? 'building' : runs.length ? 'running' : 'idle', held.length ? 'running' : '')}</div><div class="doing">${doing}</div><div class="slots">${['build-0', 'build-1', 'build-2', 'run-0', 'run-1', 'run-2', 'measure'].map(nm => { const h = held.find(x => x.slot === nm); return `<i class="${h ? (nm === 'measure' ? 'measure' : nm.startsWith('run') ? 'run' : 'held') : ''}" title="${esc(nm)}${h ? ': ' + esc(h.label) : ': free'}"></i>`; }).join('')}</div><div class="foot"><span>${mac.queue.length ? mac.queue.length + ' waiting for a slot' : 'nobody waiting'}</span><span>${esc(ago(mac.collected_at))}</span></div></div>`);
|
||||
} else cards.push(`<div class="w"><div class="top"><div><div class="card">MacBook-Pro</div><div class="meta">this MacBook</div></div>${pill('no push', 'error')}</div><div class="doing">${MAC_COPY}</div></div>`);
|
||||
{ const m = miniLive(); const held = m && m.slots && m.slots.held || [];
|
||||
cards.push(`<div class="w"><div class="top"><div><div class="card">${MINI.name}</div><div class="meta">${MINI.label}</div></div>${pill(!m ? 'no report' : held.length ? 'building' : 'idle', !m ? 'queued' : held.length ? 'running' : '')}</div><div class="doing">${!m ? 'no report yet' : held.length ? held.map(h => esc(`${h.slot}: ${h.worktree || h.label || ''}`)).join('<br>') : 'macOS binaries build here from tonight; slot free.'}</div>${m && m.slots ? slotBar(m.slots, m.slots.count || 1) : ''}<div class="foot"><span>${m ? `${(m.recent || []).length} builds logged` : 'its collector pushes to build-1'}</span><span>${m ? esc(ago(m.collected_at)) : ''}</span></div></div>`); }
|
||||
for (const pc of pcs) {
|
||||
const r = pc.running;
|
||||
cards.push(`<div class="w"><div class="top"><div><div class="card">${esc(pc.name)}</div><div class="meta">${esc(pc.machine || pc.id)} · ${esc(pc.role)}</div></div>${pill(r ? 'running' : pc.queue.length ? 'queued' : 'idle', r ? 'running' : pc.queue.length ? 'queued' : '')}</div><div class="doing">${r ? `${esc(r.kind)} ${esc(r.job)}${r.title ? '<br>' + esc(r.title) : ''}${r.stage ? `<br><span class="pill">stage ${esc(r.stage)}</span>` : ''}` : pc.queue.length ? `Next: ${esc(pc.queue[0].kind)} ${esc(pc.queue[0].job)}${pc.queue[0].title ? ', ' + esc(pc.queue[0].title) : ''}` : esc(pc.note || 'idle on jobs; the relay polls every 10 min')}</div><div class="foot"><span>${pc.recent.length} reports kept</span><span>${pc.last_report_at ? 'last report ' + esc(ago(pc.last_report_at)) : 'no report in 7 days'}</span></div></div>`);
|
||||
|
|
@ -363,7 +398,7 @@ function renderAnalytics() {
|
|||
function renderSources() {
|
||||
const s = D.sources || {};
|
||||
const one = (name, v, what) => `<span>${esc(name)} <b class="${v && v.ok ? '' : v && v.at ? 'warn' : 'no'}">${v && v.ok ? 'live' : v && v.at ? 'stale' : 'missing'}</b>${v && v.at ? ' ' + esc(ago(v.at)) : ''}${v && v.error ? ': ' + esc(v.error) : ''}${!v || v.ok ? '' : ' · ' + esc(what)}</span>`;
|
||||
$('sources').innerHTML = [...boxesOf(D).map(b => one(b.name || 'box', b.source || s.box, 'collect.mjs on the box, systemd timer every 30 s')), one('mac', s.mac, 'push.mjs on the Mac, launchd every 60 s'), one('pcs', s.pcs, 'relay intake read by push.mjs'), `<span>${esc(D._feed || '')}: fetched ${esc(ago(D._fetched))}, written ${t(D.generated_at)} by ${esc(D.generated_by || '?')}</span>`, ...boxesOf(D).filter(b => b.log).map(b => `<span>${esc(b.name)} log ${b.log.present ? `${b.log.total} lines${b.log.bad_lines ? `, ${b.log.bad_lines} unreadable` : ''}` : 'not written yet'}</span>`)].join('');
|
||||
$('sources').innerHTML = [...boxesOf(D).map(b => one(b.name || 'box', b.source || s.box, 'collect.mjs on the box, systemd timer every 30 s')), one('mac', s.mac, 'push.mjs on the Mac, launchd every 60 s'), one('mini', D.mini && D.mini.source, 'its collector on the mini, pushed to build-1'), one('pcs', s.pcs, 'relay intake read by push.mjs'), `<span>${esc(D._feed || '')}: fetched ${esc(ago(D._fetched))}, written ${t(D.generated_at)} by ${esc(D.generated_by || '?')}</span>`, ...boxesOf(D).filter(b => b.log).map(b => `<span>${esc(b.name)} log ${b.log.present ? `${b.log.total} lines${b.log.bad_lines ? `, ${b.log.bad_lines} unreadable` : ''}` : 'not written yet'}</span>`)].join('');
|
||||
}
|
||||
|
||||
function apply(d) {
|
||||
|
|
@ -375,25 +410,14 @@ function apply(d) {
|
|||
const el = $('servers'); if (el && !el.innerHTML) el.innerHTML = '';
|
||||
renderServers(); renderCrew(); renderNow(); renderQueue(); renderBackground(); renderDone(); renderHeadline(); renderAnalytics(); renderSources();
|
||||
}
|
||||
let skipLive = 0, feed = '';
|
||||
let feed = '';
|
||||
async function fetchJson(url, ms) { const r = await fetch(`${url}?t=${Date.now()}`, { cache: 'no-store', signal: AbortSignal.timeout(ms) }); if (!r.ok) throw new Error(`http ${r.status}`); return r.json(); }
|
||||
async function load() {
|
||||
let edge = null, d = null;
|
||||
try { const file = demo ? 'workers.demo.json' : 'workers.json'; edge = await fetchJson(`${base}/${file}`, 8000); }
|
||||
catch (e) { if (!D && (demo || skipLive > 0)) { $('live').className = 'live down'; $('stamp').textContent = `workers.json not reachable (${e && e.message || e})`; } }
|
||||
catch (e) { if (!D) { $('live').className = 'live down'; $('stamp').textContent = `workers.json not reachable (${e && e.message || e})`; } }
|
||||
if (demo) { d = edge; feed = 'demo document'; }
|
||||
else if (skipLive <= 0) {
|
||||
// the boxes the edge copy knows (or build-1 alone before the first push), each from its own Caddy feed, 4 s each, in parallel
|
||||
const names = edge ? boxesOf(edge).map(b => b.name).filter(Boolean) : ['igneum-build-1'];
|
||||
const results = await Promise.all(names.map(n => FEEDS[n] ? fetchJson(FEEDS[n], 4000).then(j => ({ n, j })).catch(() => ({ n, j: null })) : Promise.resolve({ n, j: null })));
|
||||
const liveOnes = results.filter(r => r.j);
|
||||
if (liveOnes.length) {
|
||||
const first = liveOnes[0].j;
|
||||
const boxes = results.map(r => r.j ? { ...r.j.box, headline: r.j.headline, source: { ok: true, at: r.j.generated_at } } : (edge && boxesOf(edge).find(b => b.name === r.n)) || { name: r.n, source: { ok: false, error: 'no live feed and no edge copy for this box' } });
|
||||
d = { ...first, boxes, mac: first.mac || (edge && edge.mac), pcs: first.pcs || (edge && edge.pcs), baselines: first.baselines || (edge && edge.baselines), sources: { ...(first.sources || {}), box: boxes[0].source } };
|
||||
feed = liveOnes.length === results.length ? `live from ${liveOnes.length === 1 ? 'the box' : liveOnes.length + ' boxes'}` : `live from ${liveOnes.map(r => r.n).join(', ')}, edge copy for the rest`;
|
||||
} else skipLive = 4; // nothing answered: use the edge copy for the next minute before trying again
|
||||
} else skipLive--;
|
||||
else { d = edge; feed = edge ? 'the merged copy from build-1' : feed; }
|
||||
if (!d) { d = edge; feed = 'edge copy, up to 5 min behind the box'; }
|
||||
if (!d) { if (!D) { $('now').innerHTML = $('queue').innerHTML = `<div class="empty">No workers.json next to this page yet and no box answers. The Mac pusher writes the copy every 60 s and the fleet deploy carries it within 5 min.</div>`; } return; }
|
||||
d._feed = feed; apply(d);
|
||||
|
|
|
|||
Loading…
Reference in a new issue