feat(health): expose settlement lag so a paying-nobody chain cannot look healthy - #8225
Open
Scottcjn wants to merge 1 commit into
Open
feat(health): expose settlement lag so a paying-nobody chain cannot look healthy#8225Scottcjn wants to merge 1 commit into
Scottcjn wants to merge 1 commit into
Conversation
…ook healthy RustChain never settled epochs 91-174: 2026-03-03 to 2026-05-27, 84 consecutive days. epoch_state has no row for any of them. Not settled = 0, absent. 183 miners enrolled across those epochs and were paid nothing, 126 RTC was never emitted, and 83% of those miners never attested again. Nothing noticed for 84 days, because nothing was watching settlement: * the epoch number is derived from wall-clock, so it kept counting up * blocks kept being produced, so tip_age_slots stayed 0 * /health therefore reported ok: true throughout * node_health_monitor read only epoch and miners, both of which looked fine finalize_epoch returned early on an empty miner set without writing a row or logging anything, so the outage left no trace to find later. Two halves: Node. _settlement_lag_epochs() reports epochs since the last settled epoch, surfaced on /epoch (reusing its existing connection) and /health. The query is bounded by the current epoch on purpose: epoch_state still carries rows from the pre-2025-12 numbering (a stray 424, plus 20000-series values), and an unbounded MAX() returns one of those, computes a negative lag, and reports perfect health during exactly the stall it exists to catch. _record_unsettled_epoch() makes both no-payout paths write settled = 0 and log the reason. Safe: the authoritative replay guard inserts the same row then atomically claims 0 -> 1, so an existing unsettled row cannot block a real settlement, and nothing was credited to be credited twice. Monitor. NodeStatus carries settlement_lag_epochs, NetworkHealth gains settlement_stalled, threshold 3 (steady state is 1). A stalled node stays "online" deliberately: it is answering, and miners must keep enrolling so the stalled epochs stay reconstructible from epoch_enroll. /health does not flip ok on lag. A 503 pulls the node from rotation and stops miners enrolling, which is the opposite of what a settlement stall needs. 19 tests, including the negative-lag trap pinned against the real helper rather than a copy of its SQL. Both halves mutation-tested: unbinding the query and removing the monitor alert each fail the suite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Scott <scottbphone12@gmail.com>
Contributor
|
| Metric | Value |
|---|---|
| Trust Score | 49/100 |
| Certificate ID | BCOS-df46b432 |
| Tier | L1 (not met) |
What does this mean?
The BCOS (Beacon Certified Open Source) engine scans for:
- SPDX license header compliance
- Known CVE vulnerabilities (OSV database)
- Static analysis findings (Semgrep)
- SBOM completeness
- Dependency freshness
- Test infrastructure evidence
- Review attestation tier
BCOS v2 Engine - Free & Open Source (MIT) - Elyan Labs
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.
The outage
RustChain never settled epochs 91-174. That is 2026-03-03 to 2026-05-27, 84 consecutive days.
epoch_statehas no row for any of them, notsettled = 0, absent.Settlement resumed forward from the present and never backfilled. Of the 183 miners, 31 ever attested again and 13 are active today: 83% never came back. (Correlation, not proven causation - no operator has said why they left.)
Why it ran 84 days unnoticed
Nothing was watching settlement. Everything that was being watched looked perfect:
tip_age_slotsstayed0/healththerefore reportedok: truefor the entire 84 daysnode_health_monitorread onlyepochandminers, both healthyAnd
finalize_epochreturned early on an empty miner set without writing a row or logging anything, so the outage left no trace to find afterwards.This is the same class as the
/statusbug fixed in #8220: a green light that does not depend on the thing that matters. There, a 200 was not evidence of a node. Here, an advancing epoch is not evidence that anyone is being paid.The fix
Node.
_settlement_lag_epochs()reports epochs since the last settled epoch, on/epoch(reusing its existing connection) and/health.The query is bounded by the current epoch on purpose.
epoch_statestill carries rows from the pre-2025-12 numbering (a stray424settled in Dec 2025, plus 20000-series values). An unboundedMAX()returns one of those, computes a negative lag, and reports flawless health during exactly the stall it exists to detect. I nearly shipped that version; it has its own test._record_unsettled_epoch()makes both no-payout paths writesettled = 0and log the reason. Safe: the authoritative replay guard inserts the same row then atomically claims0 -> 1, so an existing unsettled row cannot block a real settlement, and nothing was credited here to be credited twice.Monitor.
NodeStatus.settlement_lag_epochs,NetworkHealth.settlement_stalled, threshold 3 (steady state is 1, so this catches a stall on day two rather than day eighty-four).Two deliberate non-changes
online. It is answering, and miners must keep enrolling so the stalled epochs stay reconstructible fromepoch_enroll./healthdoes not flipokon lag. A 503 pulls the node from load-balancer rotation and stops miners enrolling, which is the opposite of what a settlement stall needs.Testing
19 tests. The trickiest case is pinned against the real helper rather than a copy of its SQL, since a test that reimplements the logic guards nothing.
Both halves mutation-tested:
AssertionError: 20424 != 90Reverting both restores green. Existing health suites unaffected:
test_health_monitor21,test_node_liveness_requires_node_state14,test_rustchain_health*13, all pass.Not in this PR
The 84 stalled epochs are recoverable -
epoch_enrollsurvived intact, 84/84 epochs, 1,733 rows, every one with positive weight. Distributing them is a monetary act (126 RTC to 186 wallets), not a bug fix, so it is deliberately left as a separate decision.