Repository navigation
op-devstack: add a windowed proof oracle to the L2 EL DSL - #23418
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
Add L2ELNode.VerifyHistoricalProofsMatch, which sweeps the blocks a proofs-history store serves and, at every sampled block, verifies eth_getProof for a configured account set against the header's state root and against an independent reference node, requiring the L2ToL1MessagePasser storage root to equal the header's withdrawalsRoot. VerifyProofsMatchAt is the single-block form; ProofsSyncStatus and WaitForProofsStoreBlock expose debug_proofsSyncStatus. Move NormalizeProofResponse and VerifyProof out of the op-reth proofs e2e utils into the DSL (as VerifyAccountProof) and switch that suite to the DSL methods. Add OpRethConfig.ProofsHistoryWindow with OpRethWithProofsHistoryWindow so a test can shrink a node's proofs window, and OpRethWithEthProofWindow so a node without proofs history can serve historical eth_getProof. Add the proofs_history_match acceptance test, which runs transfers and contract writes and sweeps a proofs follower against a sequencer without proofs history over the persistence-threshold x proofs-path matrix. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
op-reth rejects a null storage key list where op-geth accepts it, so awaitProof always sends a list. Retries are logged at warn level so a failing proof lookup is diagnosable from the test log, and the acceptance test pins the sweep's last block to the last workload block so the whole workload is covered and the catch-up wait is exercised. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Re-read the proofs window at every swept block and skip blocks the pruner removed meanwhile when the caller did not pin FirstBlock, so a small proofs window does not race the sweep. Gate the withdrawalsRoot check on a non-empty header value, since pre-Isthmus headers carry the empty withdrawals hash. Reject a nonzero storage value claimed under an empty storage root. Require debug_proofsSyncStatus to be unknown on the reference rather than merely failing. Drop the storage-key padding the client already enforces. The acceptance test adds a withdrawal per round so the message passer's storage root changes inside the swept range, and waits for reth to pass its persistence threshold before the last round so the threshold-50 cells persist at least once. The proofs e2e suite drops the waits that VerifyProofsMatchAt performs itself. 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
Every proofs-store corruption so far (wipe handling, masked-trie overlays, the withdrawalsRoot bug, the crash gap) shows up as a wrong
eth_getProoffor some historical block. This adds one DSL sweep that catches all of them without knowing the cause, so the proofs-history scenario tests (#23380, #23383, #23389, #23390, #23395, #23396, #23398) can share a single oracle.What
op-devstack/dsl/l2_el_proofs.goL2ELNode.VerifyHistoricalProofsMatch(reference, opts...): reads the node's proofs window fromdebug_proofsSyncStatus, waits (bounded) for the store to reach the sweep's last block, then for every sampled block fetcheseth_getProofby block hash forL2ToL1MessagePasserplus the configured accounts from the node under test and fromreference, Merkle-verifies both against the header's state root, requires them equal after normalization, and requires the message passer's storage root to equal the header'swithdrawalsRoot(post-Isthmus, i.e. whenever the header value is not the empty withdrawals hash). Headers are fetched from both nodes and must agree on the hash. The window is re-read at every block: a block the pruner removes during the sweep is skipped when the caller did not pinFirstBlock, and fails the sweep otherwise. The reference must answerdebug_proofsSyncStatuswith method-not-found, which only a node without a proofs-history store does.HistoricalProofsOpts:Accounts []ProofAccount(address + storage keys),CatchUpTimeout(default 30s),FirstBlock/LastBlock(zero = window edge at sweep start),MaxSampledBlocks(zero = every block; edges always included).VerifyProofsMatchAt(reference, block, accounts...): single-block form (listed accounts only, no independence check);ProofsSyncStatus()andWaitForProofsStoreBlock(target, timeout)expose the sync status RPC.NormalizeProofResponseandVerifyAccountProofmoved here fromrust/op-reth/tests/proofs/utils/proof.go. The normalizer additionally turns an empty storage proof list into nil so geth and reth responses compare equal; the verifier additionally rejects a nonzero storage value claimed under an empty storage root.utils/proof.goandutils.WaitForProofsStoreBlockare deleted;proofs/core,proofs/pruneandproofs/reorgcall the DSL methods instead and drop the waitsVerifyProofsMatchAtperforms itself.op-devstack/sysgoOpRethConfig.ProofsHistoryWindow+OpRethWithProofsHistoryWindow(blocks): the runtime passes--proofs-history.windowitself and op-reth rejects a repeated flag, so the window is a config field (proofsHistoryNodeArgsemits it once; default stays 10000). op-reth: acceptance test for the production proofs init path with backfill #23389 and op-reth: tests for the proofs RPC window edges and old-block witness requests #23390 reuse it.OpRethWithEthProofWindow(blocks): reth's--rpc.eth-proof-windowdefaults to 0, so a node without proofs history rejects every historicaleth_getProofwithExceedsMaxProofWindow. A reference node needs this.op-acceptance-tests/tests/proofs_history_matchTestProofsHistoryFollowerServesMatchingHistoricalProofs: a sequencer started withOpRethWithoutProofsHistory()+OpRethWithEthProofWindowis the independent reference; a follower runs proofs history under the matrix cell's flags. Four rounds of: two EOAs transfer back and forth, one initiates a withdrawal (so the message passer's storage root changes inside the swept range), and one calls a contract that SSTOREsNUMBERandTIMESTAMP. Before the last round the test waits for the follower to passpersistence-threshold + 2 * memory-block-buffer-targetblocks, so reth has persisted at least once and the sweep spans blocks on both sides of that. The follower is then swept from the window's first block to the last workload block for the message passer, both EOAs and the contract's two slots.Matrix (table-driven subtests): persistence threshold {7, 50} x state masking {0, 30} x proofs path {index =
verification-interval=0, execute =verification-interval=1}. The fourmasking_30cells skip naming ethereum-optimism/protocol-team#457 (the flag does not exist on develop's reth 2.5.2 build and #23046 rejects a nonzero value).Unit tests:
VerifyAccountProofagainst a synthetic state trie (valid account with storage, without storage, absent account, zero slot under an empty root; tampered balance, tampered storage value, nonzero slot under an empty root, absent account claiming a balance, wrong root),NormalizeProofResponse(five cases),ProofsSyncStatus.serves(five),sampleBlockNumbers(seven),proofsHistoryNodeArgsand the two new options.Local verification
An earlier run without
LastBlockpinned showed the catch-up wait firing (Proofs store behind target ... window [0, 20]) while the sweep's last block was still buffered in the ExEx.Proofs e2e suite after the migration (
rust/op-reth/tests/proofs/core, both nodes op-reth, debug binary):Not run: the kona-node CL variant (
DEVSTACK_L2CL_KIND=kona-node; no kona-node binary built locally). Thepruneandreorgpackages compile but stay dormant as before.Corruption check (not committed)
A copy of the test started the follower with
sysgo.OpRethWithProofsHistoryWindow(20), ran three times the workload so the pruner movedearliestto 25, and swept withopts.FirstBlock = 1:The sweep fails before any account is fetched, because the store no longer serves the block at all; a wrong proof for a served block fails one level deeper with
block N account 0x..: eth_getProof from <node> does not verify against state root .../... differs from reference <node>, which the unit tests exercise with tampered proofs.Findings
eth_getProofrejects anullstorage-key list (Invalid params: invalid type: null, expected a sequence) where op-geth accepts it. The old helpers always passed[]common.Hash{}; the DSL now sends an empty list for a nilStorageKeys.eth_getProoffail fast, three of four cells ended their 30s retry window withdial tcp 127.0.0.1:<proxy-port>: i/o timeoutagainst the follower's user-RPC proxy, the symptomsafeheaddb_elsyncalready skips on op-reth. It did not recur once requests stopped failing; noted in case it shows up again.MixedSingleChainNodeSpec.OpRethOpts), which is the production shape and saves a third node. The verification criterion expects the smaller-window failure to name "the block and account"; the oracle fails earlier, at the window check, naming the block, the node and the window (see above).Seams for the sibling PRs
L2ELNode.VerifyHistoricalProofsMatch/VerifyProofsMatchAt/WaitForProofsStoreBlock/ProofsSyncStatus.sysgo.OpRethWithProofsHistoryWindow,sysgo.OpRethWithEthProofWindow(a reference node needs the latter together withOpRethWithoutProofsHistory).Closes #23379
🤖 Generated with Claude Code