Stage 6 Release B: delete the BFT payout vote and the GHOST-03 ledger sweep - #750
Open
defenwycke wants to merge 13 commits into
Open
Stage 6 Release B: delete the BFT payout vote and the GHOST-03 ledger sweep#750defenwycke wants to merge 13 commits into
defenwycke wants to merge 13 commits into
Conversation
defenwycke
marked this pull request as draft
August 23, 2026 07:59
defenwycke
force-pushed
the
feat/stage6-release-b
branch
from
August 23, 2026 08:41
4a7bb66 to
aadbc65
Compare
defenwycke
force-pushed
the
feat/stage6-release-b
branch
from
August 24, 2026 12:37
6cd34d4 to
a7d8a3d
Compare
…etion can reach it
Stage 6 Release B deletes `settlement.rs` — the legacy tip-observing settlement. But
the SHARD's maturity settlement calls into that file: `shard.rs:1855`,
`crate::settlement::fetch_coinbase_parts(rpc, &hash)`.
Deleting it naively would have broken the shard's settlement — the money path the
deletion exists to preserve. Its own doc comment says why it is a free function:
BOTH ledgers' settlements read coinbases the same way ... it carries the
internal-vs-display hash-order fix below, and a fresh implementation would
re-hit that trap.
That trap is real and documented: `BlockEvent` hashes arrive in internal order
despite the parser claiming otherwise (observed on vm5, 2026-08-01 — every
settlement probe got "Block not found" for a block that existed). Rewriting rather
than moving would have re-earned it.
Moved to `coinbase_verifier.rs`, next to the `CoinbaseOutput` it returns, rather than
into `shard.rs`: what it encodes is how to READ a coinbase, not how to settle one.
After the deletion the shard is its only caller.
⚠ This is the rehome the build plan's rule predicted — "find what the shard inherited
BEFORE deleting, or the delete takes working machinery with it" — and it is NOT
`apply_settlement`, which an earlier survey wrongly listed as a survivor.
`apply_settlement`'s only non-test callers are `settle_paid_block` and
`settlement.rs`, both of which the deletion removes, so it goes with them.
Also removes the legacy `select_ledger_miner_work` call from the payout checkpoint
(`main.rs`). Its gate `CHECKPOINT_FROM_SHARD_HEIGHT` (963,388) passed on 2026-08-21,
so that arm has been unreachable in production since; it now abstains loudly instead
of computing a root from a source that is being removed. The gate itself stays —
collapsing gates is a later release.
No behaviour change: `shard::` 33 tests and `coinbase_verifier::` 11 tests green.
… its own Removes `payout_ledger_e2e.rs` (931 lines) and `fleet_rehearsal.rs` (511), both of which exercise the mesh-ratified payout path that Stage 6 Release B deletes. ⛔ Deleted only because the replacement exists on this branch: `regtest_shard_coinbase_e2e.rs` proves a real block, real proof-of-work, real `submitblock`, coinbase built from the shard's `owed` with NO vote — verified by the chain advancing rather than by a green tick — and `regtest_shard_coinbase_control.rs` proves the fixture cannot arm a coinbase below the gate, so the positive test is not passing on its own permissiveness. Deleting these before that existed would have removed the only end-to-end proof that a mined block pays the miners it should. That was Release B's stated prerequisite. ⚠ `fleet_rehearsal` was FAILING at the point of deletion — it reconciles all 8 nodes and then ratifies 0 of 8 where it asserts 8. It is not being deleted to hide that: it had been failing undetected because the file skipped (wrong credentials read as "no regtest node"), and what it tests — BFT ratification of a mesh payout — is the machinery this release removes. Fixing it would have been work on something being deleted. Recorded here so the failure is not mistaken later for something this release caused. Both files also held the last non-`main.rs` consumers of `make_proposal_validator`, `settle_paid_block` and `select_ledger_miner_work`, so the remaining surface is now `main.rs` alone.
Scoped under #411 (2026-08-23). The gate reads like leftover caution and is not: aggregated mode already works — the translator mints real per-downstream extranonces locally with no upstream round trip. What is missing is that the per-share TLV carries only the worker NAME, not the payout address, so payout derivation has nothing per-miner to key on and every miner in the aggregate collapses onto one channel address. Anyone who finds this gate, assumes it is stale and removes it causes a money bug that pays the wrong people. The reason now lives next to the check. Making aggregation safe means moving payout derivation to the per-share TLV for every SV1 miner — a money-path change needing a height gate and a fleet roll — and it is deferred deliberately: the pool runs ~4 miners against a cap of 1,000.
…se B, part 1) `SHARE_SHARD.md` §8: "No consensus on the ledger. Each node pays from its own view." This is that, in code. `handle_block_found` now commits the payout from this node's own shard view unconditionally. Gone with the vote: - the `PAYOUT_FROM_SHARD_HEIGHT` conditional. It HAD to go — you cannot delete a branch and keep the conditional that selects it. The gate did its job: it let the no-vote path run on the live fleet and be observed before the machinery was removed. The constant is now inert and is retired by the tip-keyed gate collapse, a later release. - the single-operator bypass (`is_single_operator`, `is_single_operator_mode`) and its two tests. That bypass existed so a pool with one operator did not need a quorum of its own nodes agreeing with themselves. With no vote at all, mode no longer changes what happens to a payout. - the GHOST-02 proposal validator wiring in `main.rs`. - `PayoutHandler`'s `vote_handler` and `mining_mode` fields, and both constructor parameters. This is the structural result worth noting: **`payout.rs` no longer imports `VoteHandler`.** The dependency is deleted, not merely unused. ⚠ **The vote handler itself STAYS.** Elder revocation proposes BFT revocation votes (`main.rs`), a different proposal type the payout validator never inspected. Deleting the voting layer was considered and rejected on 2026-08-19. What the removed vote actually did, since the name oversells it: `validate_proposal_split` recomputed the miner split from the VOTER's own table and demanded an exact match. It never inspected a share. Its whole guarantee was "your arithmetic matches mine" — only ever as strong as the two tables already agreeing, and when they did not it converted divergence into a total payment HALT (nothing finalised 18-21 Aug 2026). Share validity is untouched and lives where it always did: per-share at receive time, plus §6 sampling. Also deletes `regtest_shard_coinbase_control.rs`. It asserted the fixture could NOT arm a coinbase below the gate — a premise that existed only while the branch did. It failed honestly the moment the branch went, which is the correct behaviour for a control, and its companion e2e now states the argument for a world with no gate. Verified: `ghost-pool` lib 512 passed / 0 failed; clippy and rustdoc clean under `-D warnings`; and `regtest_shard_coinbase_e2e` still mines a real block accepted by a real ghostd — chain 793 -> 794 — on code where the BFT path no longer exists.
…HOST-02 validator Two changes that belong together, because the second is only safe once the first removes its last caller. **The shard is now mandatory.** `create_proposal`'s `None` arm — the legacy fallback that recomputed the split from `data.miner_work`, i.e. the unpaid share ledger — is replaced by a hard error. A node without a running, genesis-installed shard can no longer construct a payout and must say so, rather than silently paying from a source that is being deleted. ⚠ This makes `pool.share_shard` plus installed genesis a HARD REQUIREMENT for taking part in payouts. It is the same position `CHECKPOINT_FROM_SHARD_HEIGHT` already took for the checkpoint, which abstains loudly instead of falling back — a silent fall back to a second source is exactly the mixed-source divergence that gate exists to end. **`make_proposal_validator` and the six GHOST-02 vote tests are deleted.** The validator recomputed a peer's miner split from the voter's own table and demanded an exact match. With no payout proposal broadcast, nothing reaches it. ⚠ GHOST-02's protection is NOT lost. `payout_checkpoint.rs` has its own `payouts_agree` (13 references) doing the equivalent comparison on the checkpoint path, which is the surviving consensus mechanism. Only the vote's copy goes. `ghost02_creator` is reinstated as `regtest_creator` — it never had anything to do with GHOST-02, and the #726 shard address-validation tests still need it. The compiler caught that; the name had disguised it. `create_proposal_fee_split_anchors_to_cutoff_not_block_timestamp` moves onto the shard source. What it tests — that the fee split anchors to the CUTOFF rather than the block timestamp — is unaffected by where the balances came from. Verified: `ghost-pool` lib 506 passed / 0 failed; clippy and rustdoc clean under `-D warnings`; `regtest_shard_coinbase_e2e` still mines a block accepted by a real ghostd, chain 794 -> 795.
Three documents disagreed: #608 said the deletion happens after the release, SHARE_SHARD_BUILD.md scheduled it for "first September release", and the work has been running before v1 since step 3 shipped. Ratifying what is actually happening, because we are committed to it: the gate is armed at 964,100 and Release B is five commits deep. Two reasons the ordering is right, not merely convenient: - A public release should not carry a dead payout path plus a GHOST-03 sweep doing 3,392-6,507 writes per node per day into a 5.3 GB database for a ledger nothing reads any more. - The release exists so a stranger can run the ninth node. That person should never meet the legacy machinery, and every piece of it left in is surface they can hit and we would have to explain. #608 updated to match in a separate comment.
The sweep repaired the LEGACY unpaid ledger. Migration v56 gave the shard
ownership of `shares`, which left nothing for it to repair — so it was
switched off at runtime on 2026-08-18 and has been a no-op on every node
since. This deletes the machinery it was already skipping.
`main.rs` keeps the round-in-flight repair, which is a different thing: one
request per tick, scoped to the current round, and not what flooded. What
goes is the bucket rotation, the sweep cursor, the span anchoring and the
fan-out cap — all unreachable behind `if shard_owns_evidence { continue }`.
`convergence.rs` loses `build_ledger_request`, `ledger_request_bytes`,
`handle_ledger_request`, `apply_ledger_response` and `accept_proof`, plus
the five tests that exercised only those.
The `LedgerRequest`/`LedgerResponse` variants are KEPT, inert, for one
release. `ConvergencePayload` is serialised with `serde_json` as an
externally-tagged enum, so removing a variant makes a message from a
pre-Stage-6 peer fail to decode as a whole envelope. No node emits them any
more, but an operator on an older binary still could.
Also drops `count_unpaid_shares_in` and `unpaid_share_hashes_in`, which had
no other caller, and corrects the Component A sweep's comment — it mirrored
the deleted sweep by design and is a separate subsystem.
Net -870 lines.
`blocks_found` (public API, `/api/v1/pool`) counts `won_blocks`, and the only writer was `settle_paid_block` — which runs on the node that SUBMITTED the block. Seven of eight nodes therefore reported 0 for a block the pool genuinely won, and which node reported 1 was an accident of timing. The shard's maturity settlement is chain-derived: every node reaches it from the same coinbase at the same depth. Recording the win there makes the number agree fleet-wide, and needs no reorg reversal — a block COINBASE_MATURITY deep is not coming back, which is the reversal machinery the shard design exists to not need. `INSERT OR IGNORE` keeps this and the submitter's own writer idempotent against each other. The cost is a ~16-hour lag before a win appears. That is the honest number: it counts blocks whose coins exist and are spendable. This matters now because deleting `settlement.rs` (the chain-OBSERVED legacy settler) removes the only other path by which a non-submitting node learned of a win. Landing it first keeps that deletion semantically neutral.
Removes `SettlementObserver` (911 lines), its regtest rehearsal, the block-submitted `settle_paid_block` call site, and the four payout functions only it reached. Net -1,813. The observer settled the LEGACY unpaid ledger from an observed coinbase at tip. It existed for a specific reason, recorded on `OBSERVED_SETTLEMENT_HEIGHT`: the seven non-winning nodes would otherwise still owe work the pool had paid and, being the majority, would carry that view into the next BFT proposal and pay it twice. Release B removes the vote, so there is no majority view to poison, and the shard is the only ledger a payout is built from. `ShareShard::settle_matured` reaches the same conclusion from the same evidence — the coinbase — on every node. The difference is depth: at COINBASE_MATURITY rather than at tip. That depth is why this is a deletion and not a port. The tip-time settler needed `reverse_settlement` and a deferred-settlement table because a reorg could undo what it had booked. A block 100 deep cannot be undone, so the replacement carries none of that machinery. Deleting the tip-settler and keeping `settle_paid_block` was not an option: the reverser lived in the file being deleted, so the pair would have left settlements a reorg could never undo, plus a `won_blocks` row inflating the public `blocks_found`. They go together or not at all. The ~100-block window in which `owed` still counts work already paid is deliberate, not a gap. `owed` is signed and never clamped, so a second payment inside the window leaves a residual the next block corrects (SHARE_SHARD.md §4.4, §4.6). KEPT on purpose: - `proposal_sync` — it answers peers as well as asking, and a node on a pre-Stage-6 binary needs those answers. Retire it when none can ask. - `settle_block_atomic`, `reverse_settlement`, the deferred-settlement accessors and their tests. `settled_blocks` and `deferred_settlements` must stay replayable, same reasoning as the retained `sbc_*` tables. - `OBSERVED_SETTLEMENT_HEIGHT`, as a tombstone. Old logs and the ordering assertion on #606 are only interpretable with it. Nothing reads it.
The soak signal for the cutover: once per epoch it scanned the legacy unpaid ledger and compared it to the shard's balances, naming the worst-drifting addresses. It did its job — it is how convergence was validated, and how the -355e12 growth was attributed. It cannot survive Release B, for two reasons. It would lie. Nothing settles the legacy ledger any more, so the first block the pool wins makes the shard's `settled` rise while the legacy unpaid set stays where it is. The drift jumps by the paid amount and the warning fires for ever, describing a divergence that is the intended behaviour rather than a fault. And it costs. Each call runs `get_top_unpaid_addresses` — the 2.76M-row, ~1.6 s scan its own doc comment calls "the very load this design exists to delete". Removing a recurring full scan of the unpaid ledger is a small direct win against the working set behind #690. Also removes an orphaned doc comment left where the function had been. It had silently reattached itself to the next function, which is legal, so rustdoc stayed green and only reading caught it.
#608 §B asks for all three mining modes proven end-to-end on a chain and records that none had been. PublicPool has since been covered by `regtest_shard_coinbase_e2e`; this adds the two that had no chain-level proof at all. The mode logic already has good unit coverage, but a unit test asserts what WE think the coinbase says. A coinbase can be internally consistent, hash correctly, and still be rejected — wrong subsidy for the height, a malformed witness commitment, an output script the chain will not take. Only a real `submitblock` settles it. Every payment claim here is read back from `getblock`, not from the `PayoutProposal` we built. Both gates are armed via env so the modes are proven on the SHIPPING fee model: regtest heights are ~500 and `COINBASE_FEE_SPLIT_HEIGHT` is 959,290, so without the override this would have tested the superseded pre-gate split. The regtest chain is one shared mutable resource and both tests mine on it. Run in parallel — cargo's default — one advances the tip while the other is still building on the height it read, and `submitblock` answers "inconclusive": well-formed but unattachable. That reads exactly like a bad coinbase and is not one, so a mutex covers fetch-template THROUGH submit rather than a note asking for --test-threads=1.
defenwycke
force-pushed
the
feat/stage6-release-b
branch
from
August 24, 2026 22:38
a7d8a3d to
a833be2
Compare
defenwycke
added a commit
that referenced
this pull request
Aug 25, 2026
…770) Seven e2e tests have been reporting green without ever executing. Nothing in CI started a regtest `ghostd`, and each target opens with a skip-and-return, so they were compiled, run, printed SKIP and passed by doing nothing. `GHOST_REGTEST_REQUIRED=1` makes an unreachable node a FAILURE rather than a skip; this makes that requirement satisfiable. `ghostd` is not built from this workspace — it lives in the Bitcoin Core port — so it comes from the published release tarball, which ships it beside the pool binaries. Pinned to a version rather than tracking `latest`: CI should not change behaviour because someone cut a release. ⚠ The targets run ONE AT A TIME, in their own step, separate from the bulk `cargo test`. cargo runs test binaries in parallel and these share a single chain: `empty_template_e2e` parks a fee-paying transaction in the mempool on purpose, while `regtest_shard_coinbase_e2e` requires an empty one and drains it. Concurrently they sabotage each other. The sequencing is load-bearing. Two traps found by proving the recipe on a BLANK chain before writing any of it — neither would have shown up against the long-lived docker node, which has state the tests were leaning on without saying so: - **Priming with a second wallet breaks `empty_template_e2e`.** It resolves wallet RPCs on the base url, which only works while exactly one wallet is loaded. The chain is primed with the same wallet the tests use. - **The `ghost-cli` in the release tarball is the POOL ADMIN cli**, not a Core-style RPC client. It shares a name with the Core-style ghost-cli deployed at `/opt/ghost/bin/ghost-cli` on the fleet and takes entirely different arguments, so RPC is driven with curl. Two known-red tests are marked `#[ignore]` with their reasons rather than fixed, because #750 deletes both files outright: `fleet_rehearsal`'s fixture backdates its proposal past `PRE_GATE_FRESHNESS_SECS` so 0 of 8 nodes can ever ratify, and `payout_ledger_e2e`'s block-acceptance test builds a coinbase-only block against a populated mempool. Ignored rather than left silently skipping, because a test that fails in CI is worse than one visibly not running. Block-acceptance coverage does not depend on the latter — `regtest_shard_coinbase_e2e` submits a real block on the shard path and drains the mempool first. Verified end to end against a fresh chain from the v1.11.28 tarball: `payout_ledger_e2e` 4 passed + 1 ignored, `empty_template_e2e` passed, `regtest_shard_coinbase_e2e` passed with a real `submitblock` accepted by ghostd. `check-workflow-scalars` and `check-inlined-copies` clean.
# Conflicts: # bins/ghost-pool/tests/fleet_rehearsal.rs # bins/ghost-pool/tests/payout_ledger_e2e.rs
…se B deletes `payout_ledger_e2e` and `fleet_rehearsal` are deleted by this release, but `payout_ledger_e2e` was still named in the regtest sequence, so CI would have tried to run a target that no longer exists. `regtest_mining_modes_e2e` arrived with this release and was in no sequence at all — `common::require_regtest()` returns `None` when `GHOST_REGTEST_REQUIRED` is unset, so it would have SKIPPED and passed without a node ever starting. That is the defect #770 closed for the other targets, reintroduced by a new file. It runs first, on a clean mempool, because `empty_template_e2e` deliberately parks a transaction.
defenwycke
marked this pull request as ready for review
August 26, 2026 17:55
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stage 6, Release B: the deletion half. Removes the BFT vote from the payout path, makes the
share shard the only ledger a payout can be built from, and deletes the GHOST-03 unpaid-ledger
sweep. +730 / −5,042 across 18 files (measured against
mainafter merging it in).✅ The gate has fired and the precondition is met
PAYOUT_FROM_SHARD_HEIGHT = 964_100fired at block 964,100, 2026-08-26 06:01:30 UTC, and thefleet was observed paying from the shard for ~11 h before this was marked ready. Release A is
gate-reversible without a redeploy; this is not, because it deletes the machinery the gate falls
back to — so the three signals were measured before merging, on all 8 nodes.
Counts come from splitting
journalctl -u ghost-poolat the gate timestamp — an 11 hpre-window against the 10.8 h post-window. A 24 h count straddles the gate and averages the
transition into invisibility.
Payout consensus approvedPaying from this node's own shard view (no votepayout ledger checkpoint FINALISEDThe last
Payout consensus approvedon vm1 was 05:51:59, 9.5 min before the gate; the firstpost-gate
FINALISEDwas 06:02:02, 32 s after it. Checkpoints carryapprovals=6 needed=6throughout, so the 18–21 Aug standoff has not recurred. Zero panics fleet-wide; all 8 on
ghost-pool 1.11.28.⚠ The
no voterate is ~40–44/day, not the ~22/day this PR originally predicted. Present andhealthy on every node — the estimate was low, the behaviour is not wrong.
One unrelated defect this exposed, tracked separately
Splitting the fee-drift logs at the gate surfaced that
payout_fee_estimate()is node-local andhas diverged 125x across the fleet (
original_fees25,030 on vm1 … 3,137,573 on vm6, whileavailable_feesis ~2.1–2.7M everywhere). Nodes that under-estimate route ~2–2.8M sats/block offees to the treasury and node pool instead of to miners; vm6 over-estimates and falls back to a
non-shard coinbase.
This is not caused by this PR and not fixed by it — it lives in
template.rs, the gate merelymade the estimate node-local by removing the vote that used to collapse the fleet onto one value.
Worth stating here because a fix cannot rely on resurrecting the vote this PR deletes. Practical
impact is nil while the pool cannot win a block, but it must not ship public unfixed.
Why the vote goes
validate_proposal_splitrecomputed the miner split from the voter's ownlocal_miner_workand demanded exact equality. It never inspected a share. Its whole guarantee was "your arithmetic
matches mine", which is only as strong as the tables already agreeing — and when they did not,
that exactness converted divergence into a total payment halt.
Without it, divergence is benign:
owedis signed and never clamped, so an over- orunder-payment leaves a residual the next block corrects (
SHARE_SHARD.md§4.4, §4.6).§8 settles the design question: "No consensus on the ledger. Each node pays from its own view."
Share validity is untouched. It is per-share and at receive time — GHOST-09 signature with
receiver and address binding, the PoW preimage check, the difficulty-tier commitment. Delete the
gate, keep the rule.
What is in it
379ad92c5fetch_coinbase_partsintocoinbase_verifier— the shard's maturity settlement needs itca09d1cd94634457656bfc3bbd53f2bc98913c334ad88aadbc6537The sweep was already dead
shard_owns_evidenceis true on all eight nodes, so the sweep branch hascontinued on everytick since migration v56 landed on 18 Aug. This deletes machinery that was already being skipped.
The round-in-flight repair is kept — one request per tick, scoped to the current round, and
not what flooded.
⚠ The
LedgerRequest/LedgerResponseenum variants are kept, inert, for one release.ConvergencePayloadisserde_jsonexternally-tagged, so removing a variant makes a messagefrom a pre-Stage-6 peer fail to decode as a whole envelope, not just that field. No node
emits them any more, but an operator on an older binary still could. Drop them when no fleet
binary can.
blocks_foundhad to be fixed herewon_blockshad one writer —settle_paid_block, which runs on the node that submitted theblock. Seven of eight nodes reported
0for a block the pool genuinely won, and which nodereported
1was an accident of timing.The shard's maturity settlement now records it too. That path is chain-derived, so every node
reaches it from the same coinbase at the same depth, and it needs no reorg reversal — a block
COINBASE_MATURITYdeep is not coming back.INSERT OR IGNOREkeeps both writers idempotentagainst each other. Cost is a ~16-hour lag, which is the honest number: it counts blocks whose
coins exist and are spendable.
This is a precondition for deleting
settlement.rs, not a tidy-up — that file is thechain-observed settler, the only path by which a non-submitting node learned of a win.
Tip-time settlement goes with it
SettlementObserversettled the legacy ledger from an observed coinbase at tip. It existedbecause the seven non-winning nodes would otherwise still owe work the pool had paid and, being
the majority, would carry that into the next BFT proposal and pay it twice. No vote, no majority
view to poison — and
ShareShard::settle_maturedreaches the same conclusion from the sameevidence, on every node, at
COINBASE_MATURITYinstead of at tip.That depth is why this is a deletion rather than a port: the tip-time settler needed
reverse_settlementand a deferred-settlement table because a reorg could undo what it booked.A block 100 deep cannot be.
Deleting the observer while keeping
settle_paid_blockwas not an option — the reverserlived in the deleted file, so the pair would have left settlements a reorg could never undo plus
a
won_blocksrow inflating the publicblocks_found. They go together or not at all.Kept on purpose:
proposal_sync(it answers peers, and a pre-Stage-6 node needs thoseanswers);
settle_block_atomic/reverse_settlement/ the deferred-settlement accessors andtheir tests, because
settled_blocksanddeferred_settlementsmust stay replayable — the samereasoning that retained the
sbc_*tables; andOBSERVED_SETTLEMENT_HEIGHTas a tombstone, sinceold logs and the #606 ordering are only interpretable with it.
⚠
crates/ghost-reconciliation/src/settlement.rsis an unrelated module (Ghost Pay L2) and isnot in scope for any of this.
Verification
No live block can validate this — the pool wins one roughly every 161 years. So:
485unit tests pass; the whole suite is green after rebasing onto0abf2ea5apath produced from the same
BlockFoundData— this is the load-bearing test, because it provesthe change is a route change, not an amount change
set_local_payout_caches_the_proposal_it_publishespins the settlement precondition:approved_payoutis set and the proposal is retrievable, which is what makespayout_snapshot→settle_paid_blockfireblocks_foundtest was mutation-checked — it reads0with the fix removedghostdRollback
Once merged and deployed there is no gate to fall back to. The rollback is the
.bakbinary,which is why this must not ride the same release as a cutover.