Repository navigation
op-acceptance-tests: cover proofs history across an unclean shutdown - #23421
Draft
einar-oplabs wants to merge 3 commits into
Draft
einar-oplabs wants to merge 3 commits into
einar-oplabs wants to merge 3 commits into
Conversation
…o the mixed runtime SubProcess.Kill sends SIGKILL without an interrupt and waits a bounded time for the exit, so a test can end an op-reth process the way a crash or a power loss would, losing whatever it had not written to its datadir. OpReth.StopUngracefully exposes it, stack.UngracefulLifecycle lets the preset frontend forward it, and L2ELNode.StopUngracefully is the DSL method. Stop stays graceful. MixedSingleChainNodeSpec gains L2CLOpts, per-node op-node options passed to startL2CLNode, and L2CLVerifierSyncMode is the option a follower needs to EL-sync; the mixed runtime passed no CL options before, so every follower ran CL sync. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two tests over threshold {7, 50} x proofs path {index, execute} x restart sync mode {CL, EL}:
a proofs-history follower is killed once its store has flushed blocks reth has not persisted,
restarted, and must catch up and serve proofs matching the sequencer's across the gap; in the
second test the sequencer's op-reth is killed too, so the lost heights are rebuilt with different
contents and the follower must serve the rebuilt chain's proofs rather than its store's. The
second test fails on develop with the ParentHashMismatch crash of
ethereum-optimism/protocol-team#464 in every cell and is skipped naming it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…wn tests Kill reaps a process that died on its own with a warning instead of reporting a kill, so a crash before the kill stays visible in the logs. The rebuild test kills both op-reths back to back before stopping the CLs, so the sequencer's datadir tip cannot catch up with the follower's store tip while the follower's CL shuts down, re-reads the store tip right before the kill, and requires the workload's funding block to survive the sequencer's crash. The NUMBER/TIMESTAMP recorder contract moves into the DSL as EOA.DeployBlockRecorder, shared by both proofs-history packages. Comments no longer claim SIGKILL models a power loss. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This was referenced Oct 14, 2026
Draft
Draft
op-acceptance-tests,op-devstack: cover forkchoice updates with a stale safe or finalized hash
#23440
Draft
This branch has not been deployed
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.
Why
After a SIGKILL, OOM or power loss, an op-reth proofs-history store usually holds blocks reth's datadir has not persisted (reth persists every
--engine.persistence-thresholdblocks, the store every 5). Nothing reconciles the two at startup, so a node that comes back onto a chain the sequencer rebuilt serves staleeth_getProofdata and then crash-loops onParentHashMismatch(ethereum-optimism/protocol-team#464, fixed by #23358). Nothing tested the unclean-shutdown path at all; these are the acceptance tests for it.What
op-acceptance-tests/tests/proofs_history_restart(sequencer without proofs history as theeth_getProofreference, follower with proofs history under the cell's flags, 1s L2 blocks, both op-reths with--engine.persistence-thresholdand--engine.memory-block-buffer-target=5passed explicitly):TestProofsHistoryFollowerRecoversFromUncleanShutdown: traffic (transfers back and forth plus a contract that writesNUMBER/TIMESTAMPto two slots) runs until the follower's proofs store has flushed the cell's target block, the follower's op-reth is SIGKILLed at the cell's kill point and its CL stopped. On restart the test reads reth's datadir tip before the CL comes back and requires it below the store's tip (otherwise the cell is not exercising the crash gap), re-peers EL and CL, waits for the follower to be in sync with the sequencer, drives ten more blocks and sweepsVerifyHistoricalProofsMatchfrom the datadir tip to the last traffic block for the message passer, both EOAs and the contract's two slots.TestProofsHistoryFollowerServesRebuiltBlocksAfterUncleanShutdown: same, but the batcher is stopped up front and the sequencer's op-reth is SIGKILLed right after the follower's, while its own unsafe blocks above the follower's store tip are still in memory; the CLs are stopped once both processes are gone. The restarted sequencer must have its datadir tip below the store tip and at or above the block that completed the workload's funding, and re-sequences the lost heights with different contents (ReorgTriggeredon the store-tip block); only then is the follower restarted. Skipped naming protocol-team#464, see below.Matrix, as table-driven subtests: threshold {7, 50} x proofs path {index =
verification-interval=0, execute ==1} x restart sync mode {CL sync, EL sync}. The kill point is paired with the threshold instead of crossing the matrix again: threshold 7 kills mid-buffer (store tip 25, head 27, so the store loses blocks of its own on top of reth's), threshold 50 kills right after the store flushed block 60 (store tip = head, the store's lead over reth is largest). Observed datadir tips at the kill: 21 (once 19 or 20) at threshold 7, 46 at threshold 50, so the store is 4 to 6 and 14 blocks ahead of reth. The sequencer's datadir tip equalled the follower's in every cell.op-devstackseams (for #23395, #23396 and later PRs):sysgo.SubProcess.Kill(): SIGKILL with no interrupt, bounded wait for the exit (controlledKillWait), same pipe cleanup asStop. A process that had already died on its own is reaped with aSub-process exited before it could be killedwarning rather than reported as killed, so a crash before the kill stays visible.OpReth.StopUngracefully()exposes it;Stop()stays graceful.stack.UngracefulLifecycle(StopUngracefully()), forwarded by the presetl2ELFrontend, anddsl.L2ELNode.StopUngracefully()which fails the test for a node that cannot be killed (in-process op-geth).sysgo.MixedSingleChainNodeSpec.L2CLOpts: per-node op-node options passed tostartL2CLNode(the mixed runtime passed none before, so every follower ran CL sync).sysgo.L2CLVerifierSyncMode(mode)is the option the EL-sync cells use. Ignored for kona-node likeOpRethOptsis for op-geth.dsl.EOA.DeployBlockRecorder(): the NUMBER/TIMESTAMP recorder contract both proofs-history packages deploy, moved out ofproofs_history_matchso it is not copied a second time.Unit tests:
TestSubProcessKillEndsProcessThatIgnoresInterrupts(ashthat traps INT and TERM exits at once, no interrupt is sent, the sub-process restarts afterwards),TestSubProcessKillOfExitedProcess(warns instead of claiming a kill, second kill is a no-op),TestL2CLVerifierSyncMode.Local verification
Debug op-reth built from this branch,
DEVSTACK_ORCHESTRATOR=sysgo RUST_BINARY_PATH_OP_RETH=<debug op-reth> go test -count=1 -timeout 40m -parallel 2 -v -run <Test> ./op-acceptance-tests/tests/proofs_history_restart/:The second test, run once with the skip removed, fails in all eight cells the same way: the sequencer rebuilds the store-tip block with a different hash, the restarted follower imports the chain up to the store's tip, and its op-reth exits on the next block:
"expected" is the hash of the store's tip block on the chain that was killed, "got" the rebuilt block at the same height; the ExEx had skipped the rebuilt blocks below the tip by height. The test's own failure is the sync wait losing the follower's RPC, with the panic in the log right above it.
After the review changes (both op-reths killed back to back before the CLs stop, store tip re-read right before the kill, workload setup-block precondition, recorder contract in the DSL), one cell of each was re-run:
threshold_7-index-el_sync-kill_mid_bufferof the first test passed (52s, datadir tip 20, store tip 25, head 27),threshold_50-index-cl_sync-kill_after_flushof the second, un-skipped, failed the same way (Parent hash mismatch at block 61; workload setup block 16, sequencer datadir tip 46), andproofs_history_match/threshold_7-masking_0-indexpassed with the moved recorder helper.CI budget: with
-parallel 1the eight cells of the first test take about 10 minutes back to back on this machine, and the second test the same once #23358 un-skips it, so the package stays under the 30-minute package timeout with about 10 minutes to spare. If CI boxes turn out slower, the rebuild test can move to its own package.Not run: the kona-node CL variant (
DEVSTACK_L2CL_KIND=kona-node; no kona-node binary built locally). Under kona-node the CL-sync cells skip in code, since kona-node has no CL sync mode, and the EL-sync cells run;KonaNodehas noStopUngracefullyyet, the tests only kill op-reth.Skipped cells
TestProofsHistoryFollowerServesRebuiltBlocksAfterUncleanShutdown, all cells: ethereum-optimism/protocol-team#464; op-reth: reconcile the proofs store with reth's chain at startup #23358 un-skips it.DEVSTACK_L2CL_KIND=kona-node: kona-node has no CL sync mode.Findings and deviations from the issue
develop. Implement CI release pipeline #464 says the same-blocks case is harmless and the runs confirm it: the ExEx skips the re-imported blocks by height and the store's copies match. The issue expected both tests to fail; only the rebuilt-blocks one does. The first test stays unskipped as the regression test for the recovery path itself and should keep passing with op-reth: reconcile the proofs store with reth's chain at startup #23358, which re-indexes those blocks instead of skipping them.TestResyncinginrust/op-reth/tests/proofs/corestays as the graceful variant.--engine.memory-block-buffer-targetis passed explicitly next to the threshold so the gap does not depend on reth's defaults (develop's reth 2.5.2 pin: threshold 7, target 5).Stacked on #23418.
Closes #23380
🤖 Generated with Claude Code