Pin the parent chain hash via ArbSys on an Arbitrum host chain - #448
Pin the parent chain hash via ArbSys on an Arbitrum host chain#448hkalodner wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Updates RollupCore.createNewAssertion so that, on Arbitrum host chains, the “next parent chain block hash” is pinned using ArbSys (instead of blockhash), aligning the pinned hash with what MEL tracks as the parent-chain hash and preventing BAD_PARENT_CHAIN_BLOCK_HASH failures in that environment.
Changes:
- Replace
blockhash(block.number - 1)with an_hostChainIsArbitrumconditional that usesArbSys.arbBlockHash(arbBlockNumber() - 1)on Arbitrum host chains. - Add inline commentary explaining why
blockhash()is incorrect on Arbitrum host chains for this value.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Both comments were right, and the second one meant this PR did not actually fix L3. Addressed in
Rather than copy the conditional, extracted Test added in
Verified it discriminates: reverting the fix fails 70 existing One thing worth recording for anyone else running these: tests need |
createNewAssertion recorded the next assertion's terminal as blockhash(block.number - 1). On an Arbitrum host chain that resolves through ProcessingHook.L1BlockHash to ArbOS's L1 block hash ring buffer, so it stored an L1 hash where afterMELState.parentChainBlockHash is a hash of this chain's own parent. The equality check in createNewAssertion then cannot pass, and because block.number - 1 sits inside the 256-block window it returns a plausible non-zero hash rather than reverting. This makes MEL unusable on any L3. Use ArbSys.arbBlockHash(arbBlockNumber() - 1) on an Arbitrum host, matching the branch this function already takes for _assertionCreatedAtArbSysBlock and the one SequencerInbox takes for keyset creation blocks. One block back is well inside arbBlockHash's 256-block window. Pairs with the nitro-side fix on fix/mel-l3-correctness. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review feedback: the fix was incomplete. createNewAssertion was changed but RollupAdminLogic.initialize still computed nextParentChainBlockHash from blockhash(block.number - 1), so on an Arbitrum host chain the genesis assertion pinned an L1 ring-buffer hash and the first post-genesis assertion would still fail BAD_PARENT_CHAIN_BLOCK_HASH. The two have to agree -- genesis pins the terminal its first child must extend from -- so fixing only one of them fixes nothing. Extract _nextParentChainBlockHash() into RollupCore and call it from both, rather than copying the conditional. RollupAdminLogic already inherits RollupCore and uses _hostChainIsArbitrum twenty-five lines below the bug; the immutable is initialized at construction, so it is available during initialize. Also adds the test the branch never had. ArbitrumChecker.runningOnArbitrum returns false under Foundry unless ArbSys answers, and because _hostChainIsArbitrum is an immutable the mock has to be in place before deployment, not after. A RollupAdminLogic-derived harness exposes the helper, which is enough to exercise both branches without standing up a rollup. Reverting the fix fails the Arbitrum case and leaves the off-Arbitrum one passing, so it discriminates. 70 existing RollupTest cases still pass. Note tests need FOUNDRY_PROFILE=test; the default profile sets skip = ['test/*']. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Swept for other places the new helper belongs. There are none -- every producer of nextParentChainBlockHash now routes through it, and the two remaining references consume a stored value rather than computing one (decreaseBaseStake reconstructs an existing configHash from a caller-supplied hash; EdgeChallengeManager reads prevConfig). The one site that invites a second look is the SAME_BLOCK_ASSERTION guard, which counts L1 blocks while the hash it protects now advances with arbBlockNumber(). That mismatch is real and it is sound: every host block carries exactly one block.number, so two assertions in the same host block always see the same one and are rejected, while many host blocks can share a block.number, so the guard can only reject assertions whose hashes would in fact have differed. Over-rejects, never under-rejects, at the cost of waiting for the next L1 block on a fast-block host. Comment only -- no behaviour change. Recorded so the next reader does not have to derive it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4db62e5 to
b42e891
Compare
createNewAssertionrecords the next assertion's terminal as:On an Arbitrum host chain
blockhashresolves throughProcessingHook.L1BlockHashto ArbOS's L1 block hash ring buffer, so this stores an L1 hash whereafterMELState.parentChainBlockHashis a hash of this chain's own parent. The equality check twenty lines earlier then cannot pass:No L3 assertion satisfies that, and because
block.number - 1is inside the 256-block window it returns a plausible non-zero hash rather than reverting — so it fails silently.This is new code missing an established pattern, not a latent bug
The class of issue is well known here.
mainalready has fivehostChainIsArbitrumbranches inSequencerInbox.soland_assertionCreatedAtArbSysBlockin this very file, commented "this will be the ArbSys blockNumber if the host chain is an Arbitrum chain."What's new is the call site.
blockhash(does not appear anywhere insrc/onmain— this is the only use of it in the contracts, introduced with MEL, and it didn't take the branch the codebase takes everywhere else. The fix mirrors the pattern thirty lines below it in the same function.ArbSys.arbBlockHashreverts only outsidecurrentBlockNum-256 <= n < currentBlockNum(precompiles/ArbSys.go), so one block back is comfortably inside the window.Both terminals, not just one
createNewAssertionis the recurring case;RollupAdminLogic.initializeseeds the genesis assertion's terminal the same way. Fixing only the first leaves the very first L3 assertion unmatchable, so both go through the shared_nextParentChainBlockHashhelper.Scope
Nothing in production is affected — none of
nextParentChainBlockHash,afterMELStateorMELStateexists onmain. On a non-Arbitrum host theelsebranch is the current code, so L1-parent chains are unchanged.Pairs with OffchainLabs/nitro-private#672
That one fixes MEL writing the parent chain block number where the Bridge hashed the L1 block number in delayed message headers.
Both are needed before MEL works on an L3; neither is sufficient alone. They can merge independently and in any order — each is a strict improvement and a no-op on L1 parents — but do not read either landing as "L3 fixed." nitro-private picks this up via a contracts submodule bump once this lands in the MEL stack.
Base branch
Retargeted from
mel-bold-changestofeat/mel-validation-changes(#427).mel-bold-changesis abandoned — its own PRs #436 and #438 were both closed unmerged and its tip has not moved since 2026-07-09, so nothing based on it could merge. #427 is whereSAME_BLOCK_ASSERTIONand the MEL assertion machinery this patches are introduced, which makes it the lowest point in the live stack that the fix belongs at.That also cleared the
Check Contracts Formatfailure.src/osp/OneStepProofEntry.solhad a line at 104 chars againstline_length = 100, pushed over by one of the five OSP commits exclusive to the old base; onfeat/mel-validation-changesit is exactly 100.Verification
forge fmt --checkclean,forge buildsucceeds, andFOUNDRY_PROFILE=test forge test --match-path test/foundry/RollupParentChainHash.t.solpasses 2/2 on the new base.