test(hotblocks): cover the multi-source shape production runs#101
Merged
Conversation
Every script in the suite ran exactly one source, so `StandardDataSource`'s multi-endpoint path — the fixed-order poll race, the fork consensus, the per-endpoint `MaybeOnHead` flush — had never been executed by a test. Per-peer deficits make both laggard shapes scriptable, including the minority one production actually runs. Lag alone proves harmless to the head, in either shape and wherever the laggard sits in the poll order: it sits below the shared cursor, so it has nothing to serve and never wins the race. What wedges the service is a source that answers wrongly, and one of three is enough — not through the majority clause but through the 2 s fork-consensus timeout, which `accept_new_block` clears only on a commit, so on a chain with block time >= 2 s every inter-block gap is a firing window. Hence GAP-41 at P1. The simulator had to be corrected first: it answered any parent assertion above its tip with a fork signal, which made a source that is merely behind indistinguishable from one that disagrees. RP-5b confines the signal to `from == tip + 1`; the old behavior survives as an explicit fault. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
mo4islona
force-pushed
the
test/hotblocks-multi-source-ct4
branch
from
July 20, 2026 15:06
ac179e7 to
9b7f421
Compare
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.
Production configures several sources per dataset; every script in the suite ran exactly one. So
StandardDataSource's multi-endpoint path — the fixed-order poll race, the fork consensus, the per-endpointMaybeOnHeadflush trigger — had never been executed by a test, andcrates/data-sourcestill carries 0 unit tests. This adds the missing shape.Prompted by
base-mainnet, where one endpoint has been sitting minutes behind two on the head.What it found
A lagging source is harmless to the head. The hypothesis going in was that it is not — that the tip flush (
DataEvent::MaybeOnHead) fires only when the endpoint whose stream just ended is itself the one that committed the head block (ep.last_committed_block == Some(prev_block), per-endpoint, whilepositionis shared), so an interleaving laggard could hold the head back. It does not reproduce, in the minority shape or the majority one, and not because the healthy source wins the poll order: reversing the endpoint list changes nothing. A laggard sits below the shared cursor, so it has nothing to serve and never wins the race from any slot. The honest endpoints keep advancing the cursor, and a fork claim is dropped as soon as the cursor moves past it (standard.rs:118-124).What wedges the service is a source that answers wrongly, not one that answers late — and one of three is enough.
SimFaults::fork_signal_above_tip: an endpoint answers a parent assertion above its own tip with a fork signal, which RP-5b confines tofrom == tip + 1. At one signalling endpoint of three there is no majority (1 > 3/2is false) and the honest endpoints stay active (1 == 3is false); the door is the third clause, the 2 sfork_consensus_timeout, which fires on any poll where every endpoint returnedPending. Measured, 5/5 runs, by instrumentingpoll_next_event:extract_forkadopts the liar's stale hints,compute_rollbackrejects them as belowfin, and the epoch parks forP-EPOCH-RETRY— head frozen, no alarm, no recovery, two healthy sources still offering the chain.This is why the credulous consensus (GAP-41) is filed P1 and not P2.
accept_new_blockclears the fork-consensus timer only on a commit, so on a chain with block time ≥ 2 s every inter-block gap is a firing window. The 50 ms cadence in the first draft of this script is what hid it: at that speed the same single fault signals repeatedly and the service conforms.The simulator was wrong in exactly the place under test. It answered a parent assertion naming any position above its tip with a fork signal. RP-5b confines that signal to
from == tip + 1, the one position where the assertion is evaluable against a block the source holds; above it the answer is no-data. The old behavior made a source that is merely behind indistinguishable from one that disagrees — which would have silently turned every future lag script into a fork script. Kept as an explicit fault, pinned in both directions by a unit test.Gaps
Nothing is closed — no production code changed. GAP-5 and GAP-30 stay open; GAP-5 gains the deterministic repro it lacked, GAP-30 gains the lag dimension: selection is per block, not per source (first arrival that links onto the cursor wins, ties to config order), an endpoint leaves the rotation only by erroring, so slowness is unnameable and the only lever is removing a source by hand. Lag-aware demotion needs per-source observability first — PR #81 is that substrate and has been in draft since 2026-07-08.
Tests
Both wedge repros are pinned because they reach the same terminal state through different clauses — a fix closing only the majority path would leave the single-source one green while a lone bad source still wedges.
Full
-p sqd-hotblocks -p sqd-hotblocks-harnesssuite green (77 passed),cargo +nightly fmt --checkclean, no new clippy warnings.🤖 Generated with Claude Code