V6-08 design: proving task protection (leases, backpressure gate, shard-sized tasks, bounded transport, preflight, flow conservation), docs/design/proving-task-protection.md

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-08 19:39:22 +00:00
parent c0e43ec600
commit b51ce846af

View file

@ -0,0 +1,273 @@
# Proving task protection (master review R1, residual V6-08)
8 October 2026, 20:4x UK, the enforced-proving lane's V6-08 sub-lane. The order, from the master review's closure of V6-08
(`docs/plans/igneum-2.0-master/evidence/04_full_system/IGNEUM_V6_Full_System_Review.md`, pages 210 to 212): "smaller verifiable paid
tasks for constrained hardware where economical; a chain-health/backpressure gate; bounded binary/chunked proof transport; preflight
package integrity and dependency checks. Any protected reservation must resist claim-and-abandon griefing and restore open recovery
when the lease expires", and the measurement correction: "queue size alone cannot certify throughput. Use flow conservation: closing
backlog = opening backlog + arrivals - verified completions - expiries - cancellations. Report each departure category". The outcome
ledger that already landed (b7507dd79 on master: `tools/fleet/prover-outcomes.py`, `tools/fleet/box-prover.py` point 6,
`docs/analysis/proving-outcome-ledger.md`) is the instrument this document's six items read; it is not repeated here.
The facts this document stands on are read from the code at the 2.0.2 line's tip (`successor-2.0.1` at aea5b465, the node fork) and
from box master (cef5234b5): `vendor/igneum-node/igneum/exec/src/proving.rs` (the pool, the claims, the segment status), `rpc.rs`
(the prover's RPC), `protocol/flows/src/v10/proving.rs` (the relay bound), `tools/fleet/box-prover.py` (the fleet prover),
`proving/igneum-prove/host/src/main.rs` and `pinned.rs` (the host's modes and its pinned-guest check), and the day's record
`docs/analysis/proving-pipeline-2026-10-08.md` (3,421 claims, 93 paid, 137 steals, 313 claims and no paid segment on the 12 GB tier,
629 chain failures from a dangling host link, 264 from the host's proof string, 1,681 records submitted into a chain that never paid).
## 0. Activation, in one place
| Part | Where it lives | Default on the live object | On in tests | Consensus |
|---|---|---|---|---|
| Leases, abandon deprioritising, lease counters, the `leased` fields of `igneum_getAssignedShards` | `ProvingConfig.leases: Option<LeaseRules>` in the node fork; the daemon sets it from `IGNEUM_PROVING_LEASES=1` (the pattern of `IGNEUM_PROOF_VERIFY`, `proving.rs` `VerifyMode::from_env`), else `None` | off (`None`): the claim path behaves exactly as 0.3.20's | `Some(LeaseRules::test())` in the pool and RPC tests | none: claims are local to the node they are posted to; no Params field, the digest does not move |
| Bounded transport at the pool and the RPC | `ProofPool::submit`, `submit_segment`, `igneum_submitProofRecord`, `igneum_submitSegmentRecord` | always on, because the bound is the relay's existing `MAX_PROOF_BYTES` (8 MiB): a proof over it is already undeliverable to any peer, so no valid proof's path changes | the same | none: the body rule and the consensus-side cap are the node lane's (V6-09) |
| Backpressure gate, task sizing, preflight, the `abandoned` outcome and the conservation line | `tools/fleet/box-prover.py`, `tools/fleet/prover-outcomes.py` | prover-side; the gate and preflight are on (a prover that cannot pass preflight must not claim); `TASK` defaults to `auto` | the python self-tests | none |
| A lease honoured in the payment rule (a record from a non-holder inside the lease pays nothing) | NOT BUILT. Named: `Params::proving_lease_rule_daa`, `u64::MAX` on every network, entering the digest only once set, a later cut with its own fast-time crossing | never | | consensus: the shipper's word only |
| Lease gossip between nodes (`IgneumSegmentClaim` p2p message) | NOT BUILT. Named as phase 2 of item 1; a protocol message, not a consensus rule | | | protocol, not consensus; still not built without the node lane's word |
| A per-tier segment length | NOT BUILT. `proving_v1_segment_blocks` is a consensus param; "smaller" is answered below without touching it | | | consensus |
Nothing here moves the live object's digest (be5f4068 on 2.0.1 and 2.0.2, the node lane's word of 20:3x UK).
## 1. The protected reservation: a claim is a lease
**What exists today.** `ProofPool::claim_segment(first, last, key_hash, tip_daa)` (`proving.rs` 1151) stores any claim; a second claim
for the same `(first, last, key)` refreshes it; a claim by another key for the same range is stored beside it, never refused;
the expiry is `tip_daa + unproven_daa`, the whole 600 DAA window. `open_claims(tip_daa)` (1166) drops a claim whose expiry passed or
whose key's segment record is in the pool. `igneum_claimSegment` (`rpc.rs` 1521) always answers `accepted: true`; `igneum_getProofClaims`
lists the open ones; `igneum_getAssignedShards` (1278) lists the claims covering a block only through `igneum_getSegment`. The claims
are consulted by nothing: `submit_segment` (1430) and the payment rule never read them. And the fleet prover never posts one:
`box-prover.py` "claims" in its own state only (`state["claimed"]`, `RESULT claim`) and spreads by FNV-1a of (first, key). So on
8 October two boxes could prove the same segment and the faster one was paid: 137 steals, 4.0 percent of claims, the slow box's
work wasted (the pipeline record, "Steals").
**The change.** `LeaseRules { lease_daa, abandon_window_daa, abandon_threshold, penalty_daa }` on `ProvingConfig` (`None` = today's
behaviour). With it set:
1. A claim is a lease: `expires_daa = tip_daa + lease_daa` (default 300 DAA: the measured claim-to-submitted p95 is 230 s and the
prover's own margin floor is 240 DAA; `unproven_daa` is the cap), never the whole window.
2. A second claimant with a different key inside the lease is refused: `claim_segment` returns `ClaimOutcome { accepted: false,
holder: Some(claim), reason: "leased by 0x.. until DAA N" }`; `igneum_claimSegment` answers `accepted: false` with the holder and
the expiry so the prover skips the segment instead of racing. The same key inside its own lease is answered `accepted: true,
new: false` and the expiry does NOT move (a refresh cannot hold a segment for ever).
3. On expiry the segment reopens: `open_claims` drops the lease, any key may claim, and the first claimant's record is still accepted
if it arrives before the segment's deadline, because `submit_segment` does not consult the claims (unchanged; the test pins it).
That is "open recovery when the lease expires".
4. Claim-and-abandon: when a lease expires and the pool holds no segment record of its key for that range, `open_claims` records an
abandon for the key (a per-key ring of expiry DAA scores, bounded to 64 entries). A key with `abandon_threshold` (default 3) or
more abandons inside `abandon_window_daa` (default 3,600 DAA, one hour) is deprioritised, never banned: its claim on a segment is
refused with `reason: "deprioritised: N abandoned leases in the window; claim again from DAA D"` until `tip_daa >= last_daa +
penalty_daa` (default 120 DAA past the segment's last block), so prompt provers have first pick and the abandoner takes the
leftovers; the count decays out of the window on its own. `igneum_getAssignedShards` reports, for the caller's keys,
`claimPriority: { abandons, deprioritised, claimFromDaa }` on every shard row, and each row gains `leased`, `leaseHolder`,
`leaseExpiresDaa` so the prover's candidate grouping (every shard "open, unpaid, not in the pool") also requires "not leased by
another key".
5. The prover posts the claim: `box-prover.py` calls `igneum_claimSegment` before the export and treats `accepted: false` as a
skip (a `RESULT lease_refused` line, no ledger row: a refused claim is not an eligible job). The lease is the prover's reservation
for the time it takes to prove; the segment deadline stays the chain's.
6. Counters for the chain-side view, in `igneum_getProvingStatus` under `leases`: `open`, `granted`, `refusedHeld` (a holder's lease
refused a second claimant), `refusedDeprioritised`, `landed` (a lease whose record arrived), `abandoned` (expired with no record).
Flow: `granted = landed + abandoned + open` at any read (the test asserts it).
**Tests, known-failed first** (`proving.rs`, the pool's test module; `rpc.rs`, the node-info test module's `Ctx` pattern):
- `a_second_claimant_is_refused_inside_the_lease_and_the_segment_reopens_at_expiry`: today's pool accepts both claims (the known
failure); with leases, B is refused while A's lease runs, B is accepted one DAA past the expiry, and A's record submitted after the
expiry but before the deadline is still accepted.
- `a_refresh_by_the_holder_does_not_move_the_expiry`.
- `three_abandoned_leases_in_the_window_deprioritise_a_key_and_the_window_clears_it`: the key is refused inside the penalty, accepted
past it, accepted again on a fresh segment once the abandons age out; a key with two abandons is never refused.
- `lease_counters_conserve_flow`: granted = landed + abandoned + open after a mixed sequence.
- `with_leases_off_the_claim_path_is_0_3_20s`: both claims accepted, no counters, the RPC reply shape unchanged.
- RPC: `igneum_claimSegment` answers `accepted: false` with `holder` and `expiresDaa` for the second key; `igneum_getAssignedShards`
carries `leased` and `claimPriority`.
**Cost.** A block: none (nothing in consensus). A prover: one `igneum_claimSegment` per task (one round trip, under a millisecond on
the node) and the chance of a refusal, which costs one pass (15 s) and saves the whole export, cut and chain of a segment another
box is already proving (median 75 s, p95 230 s of wasted card time per steal on 8 October). The node: the claims vector (bounded at
10,000 today, unchanged) plus a per-key abandon ring of at most 64 entries.
**What a lease does not do tonight.** It binds only the node it is posted to. A fleet whose boxes run their own nodes still race
across nodes; a pool operator's node serving many provers gets the full protection. Cross-node leases are the gossip message named in
section 0 (phase 2); a lease the payment rule honours is the consensus item there.
## 2. The chain-health backpressure gate
**What exists today.** `box-prover.py` waits once at start for `igneum-miner watch` to print `synced=true`, then claims on every pass
whatever the node's state. The node reports its own health: `igneum_getExecStatus` (`rpc.rs` 1398) carries `blocked`, `reexecuting`,
`recordsContinuous`, `synced`, `executedTip`, `executedTipDaa` and `sinkNumber` (the consensus sink's chain block number); `pause_reason`
(`rpc.rs` 156) is the node's own definition of "paused" (blocked, re-executing, vetoing, or the executor more than two chain blocks
behind the sink) but is served only on `igneum_getTransactionStatus`; `igneum_getProvingStatus` carries `execSync.stateFresh`,
`pausedSinceMs` (the finality pause) and `tipDaa`. On 8 October the chain stalled at 11:45Z and partitioned; the fleet kept claiming
and submitted 1,681 segment records into a chain that never paid them, the day's largest waste.
**The change.** `chain_health(ex, st, clock)` in `box-prover.py`, read every pass before `candidates`, returns a hold cause or `None`:
| cause | read | threshold (env, default) |
|---|---|---|
| `blocked` | `igneum_getExecStatus.blocked` set | any |
| `reexecuting` | `.reexecuting` set | any |
| `discontinuous` | `.recordsContinuous` is false | any |
| `lagging` | `.sinkNumber - .executedTip` | over `LAG_BLOCKS` (64, the settled floor's depth) |
| `stalled` | `igneum_getProvingStatus.tipDaa` unchanged across passes for | `STALL_S` (120 s: DAA moves about one a second) |
| `finality_paused` | `.pausedSinceMs` set for longer than | `FINALITY_PAUSE_S` (600 s) |
| `unreachable` | either RPC answers nothing | any |
On a hold the prover does not claim, does not export, and prints `RESULT backpressure <stamp> hold cause=<cause> <reading>` once per
cause change and `RESULT backpressure <stamp> released after <s> s` when the gate clears; every pass under a hold counts in
`state["backpressure"] = {holds, held_s, by_cause}`. Submitted records keep being polled (the paid state of an accepted record is still
worth reading) and held fresh records keep being offered, because both are free. The node side needs no change: every reading
is already served. `pause_reason` is reused by name in the design so the app's pause and the prover's hold agree on what "paused" is;
adding it to `igneum_getProvingStatus` as `paused` is a one-line follow-up for the node lane, not needed tonight.
**Tests, known-failed first** (`box-prover.py --self-test`, pure functions, no node): a healthy reading holds nothing (the known
failure is a stub that holds on everything); each cause fires on its own reading at the threshold and not below it; the stall
detector needs two readings `STALL_S` apart with the same DAA and clears on a move.
**Cost.** A block: none. A prover: one extra RPC per pass (`igneum_getExecStatus`, already called per task for the export start); a
hold costs the prover nothing it would have been paid for, by the record of 8 October (zero paid from the stall on).
## 3. Smaller verifiable paid tasks
**What exists today.** The protocol already pays two sizes. The shard record (`ProofRecord`, spec 7.7, `igneum_submitProofRecord`,
paid by `shard_parts`: 90 percent of a block's pool credit split over its shards) is one shard of one block, bounded by the shard
budget S_p (`shard_proving_gas_budget_at`); the segment record (`SegmentRecord`, spec 7.8, the aggregator's 10 percent) is the chain
over eight blocks' shards. The host makes a single shard proof with `--mode compressed --shard i` (`main.rs` `run_compressed`: the
statement, the proof sha256 and the proof file, everything `sign-record` needs) and aggregates shard proofs it did not make with
`--mode aggregate --proofs <a.bin,...>` from the pool (`igneum_getProofBytes`). The app's shard loop (`app/igneum-app/src/prover.rs`
`choose`) claims one shard at a time. The fleet prover claims only whole segments (`--mode chain`, eight blocks, every shard, then the
aggregation): peak memory 11,948 MiB median and 26,210 MiB max, and the 12 GB tier claimed 313 segments and was paid for none.
**What "smaller" is.** One shard of one block: the smallest unit the chain pays, verifiable on its own (the node verifies each shard
record at submission under the pinned shard program id), with no aggregation step and no chain link to lose. A constrained card proves
shards and leaves the segment chain to a card with the memory for it; the aggregator's share goes to the aggregator, which is the
split the spec already makes. No consensus change: the segment length and the shard budget stay.
**The change, the sizing knob.** `box-prover.py` gains `TASK=segment|shard|auto` (default `auto`: `shard` when the card's total memory
read from `nvidia-smi` is under `SEGMENT_MIN_MIB`, default 16,384, else `segment`) and `MAX_SHARDS` (default 0 = no cap): in segment
mode a segment with more shards than `MAX_SHARDS` is skipped before the claim (`RESULT skip ... shards N over MAX_SHARDS`), in shard
mode a task is one shard. Shard mode's pass: the candidate shards are the worklist rows that are open, unpaid, not in the pool and not
leased by another key, assigned-first then open, newest first and smallest `pgas` among equals (the app's `choose`); one claim
(`igneum_claimSegment` over the one block, the lease of section 1), one export of that block, one cut, one `--mode compressed --shard i
--out`, one `sign-record`, one `igneum_submitProofRecord`; the paid state is polled through `igneum_getAssignedShards` (`paid` on the
row). Each shard task is a ledger row of `kind: shard` with the same four outcomes and causes, so `prover-outcomes.py` reports both
kinds in one table with the kind beside each count. The planner is not a knob: `proving_v1_segment_blocks` is a consensus param
(section 0).
**Tests, known-failed first.** `box-prover.py --self-test`: `task_mode(total_mib, env)` picks shard under the line and segment over
it (the known failure is a stub that always says segment); `shard_candidates(work, attempted, leases)` orders assigned before open,
newest first, smallest pgas among equals, and never offers a leased, paid or pooled shard; a segment over `MAX_SHARDS` is not a
candidate. The live check is the fleet's: one 12 GB box in shard mode for an hour, the ledger's paid count above zero, which is the
figure the review asked for ("where economical": a shard on a 12 GB card is 5.7 s at threshold 2^26 by the floor rows of
`docs/plans/proving-v1.md`, against a chain the card never finished).
**Cost.** A block: none. A prover: a shard task's export and cut are one block, not eight; its paid unit is one shard part (a 90 percent
share split over the block's shards, about 0.17 IGN on 8 October's credits) rather than a segment's aggregator share (1.5 to 1.6 IGN),
so the constrained card earns in small units it can finish instead of large ones it cannot.
## 4. Bounded transport
**What exists today.** The relay bounds a proof message at `MAX_PROOF_BYTES = 8 << 20` (`protocol/flows/src/v10/proving.rs` 18; a
compressed SP1 shard proof is about 1.3 MB); a larger message is a protocol error and the peer is dropped. The RPC has no bound of its
own: `igneum_submitProofRecord` and `igneum_submitSegmentRecord` (`rpc.rs` 1270, 1313) decode whatever hex the 32 MiB axum body limit
lets through (`rpc.rs` 339), hash it (`proof_hash`, sha256 over the bytes) and hold it by hash in `proofs` before the native checks.
`ProofPool::submit` (1173) and `submit_segment` (1430) have no bound either. So a local or relayed proof of up to 32 MiB is hashed and
held, and a proof over 8 MiB that passes the RPC can never reach a peer. The fleet's "invalid string length" class (264 chain failures,
the host's proof string on the 48 GB cards' larger segments) is the host's own JSON, not this path, and stays with the node lane as
recorded.
**The change.** One bound, the relay's: `kaspa_consensus_core::proving::MAX_PROOF_BYTES` moves to consensus-core so the flows crate and
the exec crate share it (the flows constant becomes a re-export; no value change). `ProofPool::submit` and `submit_segment` refuse
`proof.len() > MAX_PROOF_BYTES` before hashing, with the size in the refusal: `"proof is N bytes, over the M-byte bound"`. The two RPC
methods refuse on the hex string's length before decoding (`len > 2 + 2 * MAX_PROOF_BYTES`), so nothing is allocated for an oversize
submission, with the same text. The outcome is `accepted: false, new: false` so the prover's existing refusal path logs it (`RESULT seg
N shard ... refused: proof is ... bytes`) and the ledger closes the task `cancelled, refused`.
**Chunked transport.** Not built, and named: a chunked RPC alone would let a prover deliver a proof the relay cannot carry, so the
chunking has to be the relay's (an `IgneumProofChunk` message with the proof hash, the index and the count, reassembled under the same
bound times a chunk count the object states). That is the node lane's transport item when a proof larger than the bound exists; none
does today (1.3 MB shard, under 1 MB segment).
**Tests, known-failed first** (`proving.rs`): `a_proof_over_the_bound_is_refused_before_it_is_held`: today the pool holds it by hash
(the known failure: `holds_proof` reads true); after, `accepted: false`, the reason names the size and the bound, `holds_proof` reads
false, nothing in `proofs`; a proof exactly at the bound is held. `rpc.rs`: the hex refusal on both methods, with the reason text.
**Cost.** A block: none. A prover: none for any proof it makes today; an oversize one is refused in constant time instead of being
hashed and held.
## 5. Preflight package integrity and dependency checks
**What exists today.** `box-prover.py` prints `host={os.path.exists(HOST)}` in its start line and goes on either way; the floor
tarball's `igneum-prove-host` was a dangling symlink from 08:00 to 10:59Z on 8 October and every chain failed with "No such file or
directory" (629 failures, the day's second class). The host checks its own embedded guests against the pinned manifest at every start
(`pinned.rs` `Pinned::load`: the two ELFs and the two keys by sha256 against `elf/manifest.json`, the ids derived from the keys and
compared with the manifest's) and prints them on `--mode id`, but only when it runs, which is after the claim, the export and the cut.
The node serves the ids it pins on `igneum_getProvingStatus` (`v1.shardProgramId`, `v1.aggregatorId`) and its manifest on
`igneum_getManifest` (`verifier.shardProgramId`, `verifier.aggregatorId`, `verifier.proverVersion`).
**The change.** `preflight()` in `box-prover.py`, before the key read and the first claim, in order, each with a RESULT line:
1. The binaries: `HOST` and `EXPORT` resolve (`os.path.realpath`), exist, are executable; `ldd` reports nothing "not found".
2. Their sha256 against the expected set in `PREFLIGHT_MANIFEST` (default `/root/fleet/in/prover-manifest.json`, shipped beside
`box-prover.py` by `tools/fleet/fleet.py`; the format `{"igneum-prove-host": "<sha256>", "igneum-prove-export": "<sha256>",
"shard_program_id": "0x..", "aggregator_id": "0x.."}`). A mismatch is `RESULT preflight_failed <stamp> file=<path> expected=<sha>
got=<sha>` and exit 4. No manifest file is `RESULT preflight <stamp> manifest=absent` and the sha256 of each binary is recorded in
the line, not enforced: the ids check below still runs.
3. The ELF manifest: `HOST --mode id` must run and print both ids; they must equal the manifest's when given and the node's
`v1.shardProgramId` and `v1.aggregatorId` always (the node's pinned pair is what the chain pays for; a host with other guests
proves for nothing). A mismatch is `RESULT preflight_failed <stamp> file=<host path> ids=<printed> node=<node's>` and exit 4.
4. Dependencies: `nvidia-smi` answers for `IGNEUM_CUDA_DEVICE`; the floor server binary `/opt/igneum-floor/bin/sp1-gpu-server` exists
and is executable; `igneum-miner` answers `key-hash`. Each failure names the file.
`RESULT preflight <stamp> ok host=<sha16> export=<sha16> shard=<id8> aggregator=<id8> device=<n>` on success; the state file carries
`preflight: {host_sha256, export_sha256, shard_program_id, aggregator_id, at}` so the collector can prove what proved.
**Tests, known-failed first** (`box-prover.py --self-test`): `preflight_verdict(files, manifest, printed_ids, node_ids)` as a pure
function over readings: a dangling link fails naming the file (the known failure: the stub passes on `exists` alone, which is what
8 October's prover did); a sha mismatch names the file and both hashes; an id mismatch against the node fails even with no manifest;
the absent-manifest case passes with the shas recorded.
**Cost.** A block: none. A prover: sha256 over about 76 MB of binaries (under a second) and one host run of `--mode id` (the pinned
check, about 0.3 s), once per start.
## 6. Flow conservation, reported per departure category
**What exists today.** The ledger (b7507dd79) gives every claimed segment one of `paid`, `expired` (cause `unpaid`, `held_expired`,
or `never_submitted` by log reconstruction), `cancelled` (nine causes) or `active`, and the report prints the counts, waste by cause
and throughput. A claim whose prover never submitted and never wrote a cause (killed mid-chain, a box restarted, a lease that ran out)
is `active` for ever in the state file, and `never_submitted` only when a log is reconstructed. The review's identity is not printed.
**The change.**
1. The fourth closing outcome `abandoned`: a claim whose lease (section 1) or deadline passed with no record submitted. The live
prover closes it: on every pass, an active task past its lease expiry with no submit is closed `abandoned` with the cause (`lease`,
or `restart` for active rows found in the state file at start, which a restarted prover closes before its first claim); the
log reconstruction's `never_submitted` becomes `abandoned, never_submitted`. The outcome set is then exactly the order's: paid,
expired, cancelled by cause, abandoned.
2. The conservation line, in the state (`RESULT ledger`) and in `prover-outcomes.py`'s report:
`flow: claims N = paid P + expired E + cancelled C + abandoned A + active O` with `ok` when the sum holds and `BROKEN by d` when it
does not (a broken line is a bug in the prover, never silently repaired). Over a span: `opening backlog + arrivals - paid - expired
- cancelled - abandoned = closing backlog`, where the backlog is the active count at the span's ends and the arrivals the claims
inside it; the report prints the identity with the numbers in it, per kind (segment, shard) and in all, and the departure
categories each with its causes.
3. The chain side: the `leases` counters of section 1 give the node's own identity (`granted = landed + abandoned + open`) on
`igneum_getProvingStatus`, so the fleet's view and the chain's view can be compared per departure category, which the ledger
document named as the next step.
**Tests, known-failed first.** `prover-outcomes.py --self-test` (the existing synthetic log gains an abandoned job): the conservation
line is `ok` on the fixture and `BROKEN` on a state whose counters were edited (the known failure is the report without the line);
`box-prover.py --self-test`: `close()` on an active row past its lease yields `abandoned`, a second close is ignored, the outcome
counters sum to the claims after a mixed sequence.
**Cost.** None to a block; to a prover, a few integers.
## 7. What the build on the branches is, in order, with the clocks
| # | Branch | Change | Test | Clock (UK) |
|---|---|---|---|---|
| 1 | `v608-proving-tasks-node` (node fork, off `successor-2.0.1`) | `LeaseRules`, `ClaimOutcome`, `claim_segment` and `open_claims` with leases, the abandon ring, the counters; `igneum_claimSegment`, `igneum_getAssignedShards`, `igneum_getProvingStatus` fields; `MAX_PROOF_BYTES` in consensus-core, the pool and RPC bounds | the five pool tests and the RPC tests above, `cargo test -p igneum-exec` on build-2 or build-3 through `tools/build-remote.sh` | 00:30 |
| 2 | `v608-proving-tasks` (igneum) | `box-prover.py`: the lease call, the backpressure gate, `TASK`/`MAX_SHARDS` and shard mode, `preflight()`, the `abandoned` outcome, the conservation line, `--self-test`; `prover-outcomes.py`: `abandoned`, the conservation line, the kind column; `tools/ci/pre-push.sh` runs `box-prover.py --self-test` beside the ledger's | the python self-tests | 01:30 |
| 3 | both | the node tip merged once more; shas to the node lane and the coordinator | | 02:00 |
Landing: the igneum branch through `tools/ci/merge-to-master.sh --remote box` on the coordinator's word; the node branch's sha to the
node lane, which lands it on `successor-2.0.1` through its own gate (the six suites, the pair, the devnet-4 canary on build-9).