test(e2e): release the stale block once its ingress window has reopened - #272
Merged
Merged
Conversation
The cross-chain reorg case released a signed block the instant it had replaced the L1 suffix. Replacing the suffix re-mines it from the fork point, which leaves the L1 head stamped behind where it was, and the e2e clock follows L1, so protocol time walked backwards with it. The held block is signed at the very start of its build frame, which is the same instant its slot's proposal receive window opens, so the release landed ~12s before the window it needed to be inside. Every peer dropped the proposal at p2p ingress for a slot that had not opened yet, the named validator never compared its Inbox prefix, and the test timed out awaiting a comparison that could no longer happen. The gate could not have caught this: `remainingIngressBudgetMs` measured only the distance to the window's upper bound, so it reported a healthy budget off the already-rewound clock. It becomes `ingressWindow()`, which returns both ends from a single reading of the clock, and the reorg case waits for the window to reopen before releasing. Interval mining stamps the next L1 blocks forward again, so the wait resolves on its own and lands back at the same point in the build frame; its bound is wall-clock, so a chain that stopped advancing fails at the wait rather than at the assertion it silently breaks. The e2e itself was not rerun locally; CI is the verification for it.
|
Review corrections, all documentation or diagnostics; the timing logic is unchanged. The wait's comment claimed the clock only climbs back because interval mining stamps the next L1 blocks forward, and that a chain which stopped advancing would fail at the wait. Both are wrong: the shared provider is an offset on the real clock, so it resumes advancing on its own and the wait would succeed from wall-clock passage alone. The bound is there so a clock that never arrives names the window rather than failing downstream. The comment also stated the rewind as a certainty; it has now been measured at both nothing and a full L1 slot for the same reorg depth, so it says that instead. `ingressWindow`'s contract said a proposal is acceptable *exactly* when `opensInMs <= 0 < closesInMs`. Peers judge at receive time and widen both ends by their clock-disparity tolerance, so that is a sufficient condition, not the exact one. The unit case now pins `closesInMs` at both ends, including the inclusive deadline boundary that makes the difference. The wait also logs the window and the reorg depth before it starts, so a timeout shows how far the clock had actually moved, and `HoldContext` grows a `now()` rather than having callers reach through to the event's schedule for the shared sample.
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.
Context
multi-node/block-production/cross_chain_messages.parallel.test.ts→ builds multiple blocks per slot with L1 to L2 messages has been failing since its reorg phase landed with #243. It surfaced on #221, which is unrelated to it — that PR touches noend-to-endcode and is simply the first one since #243 whose diff invalidated the e2e test cache. #243's own final head merged withcicancelled, so this phase was never validated.It is not a flake, and cannot be handled as one.
src/multi-node/.*\.test\.tsalready matches an owned entry in.test_patterns.yml, soci3/run_test_cmdalready gave it its one automatic retry; it failed both times, with different proposer/validator index draws and the same numbers to within 10ms.flake_error_thresholdis read only by the dashboard, never byrun_test_cmd, so no entry would change the outcome.Root cause
The failure is
TimeoutError: Timeout awaiting validator N compares the stale block's Inbox prefix.reorgWithReplacementre-mines the replaced suffix from the fork point, which leaves the L1 head stamped behind where it was — one 12s L1 slot, in both failing runs. The e2e clock follows L1, so protocol time walks backwards with it.The held block is signed at the very start of its build frame (
secondsIntoBuildFrame: 0.652,indexWithinCheckpoint: 0), and for its slot that instant isgetCheckpointProposalReceiveStart. The release therefore landed ~11.73s before the window it had to be inside.proposal_validator.tsgates ingress on both ends of that window, so every peer dropped the proposal as too early:The named validator never ran the Inbox-prefix comparison at all, so the
retryUntilpolling for it timed out after 144s. The product side behaved correctly throughout — every node detected the rolling-hash divergence, rolled back 5→4 messages, and the proposer pruned its stale block and abandoned the slot withinbox_prefix_reorged.The gate could not have caught this.
remainingIngressBudgetMsmeasured only the distance to the window's upper bound, so it reported a healthy78233 mscomputed off the already-rewound clock, andexpect(ingressBudgetMs).toBeGreaterThan(0)passed while the release was in fact too early.Change
HoldContext.remainingIngressBudgetMsbecomesingressWindow(), returning{ opensInMs, closesInMs }from a single reading of the clock — a proposal released now is acceptable exactly whenopensInMs <= 0 < closesInMs, and the two ends cannot be sampled either side of a moving clock. The schedule already exposedgetProposalReceiveStartSeconds(); the gate just never surfaced it.canStartAnotherBlock()still holds. The bound is wall-clock (retryUntilmeasures with a real timer), so a chain that stopped advancing fails at the wait rather than downstream at the assertion it silently breaks.now.The p2p lower bound is correct product behaviour and is untouched; this is a harness defect only.
Testing
Red/green on the gate: the new
reports how long the ingress window is still to opencase fails withctx.ingressWindow is not a functionbefore the change and passes after — 25/25 incheckpoint_proposal_job_test_gate.test.ts. Fullyarn-projectbootstrap, build, format-check and lint are green.The e2e case itself was not rerun locally; CI is the verification for it.