From b82fb1ff272d65ca49e83c323b4cec4fe94ba227 Mon Sep 17 00:00:00 2001 From: Rick Glenn Date: Tue, 4 Aug 2026 14:27:07 -0700 Subject: [PATCH] Prevent skipped parent-chain blockhash entries --- changelog/exocognosis-nit-4718.md | 2 + execution/gethexec/sequencer.go | 7 ++ execution/gethexec/sequencer_l1_block.go | 24 ++++ execution/gethexec/sequencer_l1_block_test.go | 106 ++++++++++++++++++ 4 files changed, 139 insertions(+) create mode 100644 changelog/exocognosis-nit-4718.md create mode 100644 execution/gethexec/sequencer_l1_block.go create mode 100644 execution/gethexec/sequencer_l1_block_test.go diff --git a/changelog/exocognosis-nit-4718.md b/changelog/exocognosis-nit-4718.md new file mode 100644 index 0000000000..74b2025af4 --- /dev/null +++ b/changelog/exocognosis-nit-4718.md @@ -0,0 +1,2 @@ +### Fixed +- Prevent sequencer parent-chain block number jumps from leaving stale hashes in the ArbOS blockhash ring buffer. diff --git a/execution/gethexec/sequencer.go b/execution/gethexec/sequencer.go index 59e667fccd..9b05b992ff 100644 --- a/execution/gethexec/sequencer.go +++ b/execution/gethexec/sequencer.go @@ -1430,6 +1430,13 @@ func (s *Sequencer) createBlock(ctx context.Context) (returnValue bool) { return true } + selectedL1Block := nextSequencerParentChainBlockNumber(l1Block, lastBlock) + if selectedL1Block != l1Block { + log.Debug("limiting parent chain block advancement to preserve blockhash history", + "observed", l1Block, "selected", selectedL1Block) + l1Block = selectedL1Block + } + header := &arbostypes.L1IncomingMessageHeader{ Kind: arbostypes.L1MessageType_L2Message, Poster: l1pricing.BatchPosterAddress, diff --git a/execution/gethexec/sequencer_l1_block.go b/execution/gethexec/sequencer_l1_block.go new file mode 100644 index 0000000000..0b2d661665 --- /dev/null +++ b/execution/gethexec/sequencer_l1_block.go @@ -0,0 +1,24 @@ +// Copyright 2021-2026, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md + +package gethexec + +import "github.com/ethereum/go-ethereum/core/types" + +// nextSequencerParentChainBlockNumber limits each sequenced L2 block to one +// parent-chain block of progress. HeaderReader subscribers may miss intermediate +// headers, especially when polling, but ArbOS associates the previous L2 block +// hash with each parent-chain block transition. Advancing one block at a time +// ensures every transition is recorded in order instead of leaving a stale +// ring-buffer slot behind. +func nextSequencerParentChainBlockNumber(observed uint64, lastL2Block *types.Header) uint64 { + lastBlockInfo := types.DeserializeHeaderExtraInformation(lastL2Block) + if lastBlockInfo.ArbOSFormatVersion == 0 { + return observed + } + previous := lastBlockInfo.L1BlockNumber + if observed > previous && observed-previous > 1 { + return previous + 1 + } + return observed +} diff --git a/execution/gethexec/sequencer_l1_block_test.go b/execution/gethexec/sequencer_l1_block_test.go new file mode 100644 index 0000000000..452682ce7c --- /dev/null +++ b/execution/gethexec/sequencer_l1_block_test.go @@ -0,0 +1,106 @@ +// Copyright 2021-2026, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md + +package gethexec + +import ( + "math" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" +) + +func TestNextSequencerParentChainBlockNumber(t *testing.T) { + tests := []struct { + name string + format uint64 + previous uint64 + observed uint64 + expected uint64 + }{ + { + name: "legacy header is unchanged", + observed: 102, + expected: 102, + }, + { + name: "same parent chain block", + format: params.ArbosVersion_51, + previous: 100, + observed: 100, + expected: 100, + }, + { + name: "next parent chain block", + format: params.ArbosVersion_51, + previous: 100, + observed: 101, + expected: 101, + }, + { + name: "one skipped parent chain block", + format: params.ArbosVersion_51, + previous: 100, + observed: 102, + expected: 101, + }, + { + name: "multiple skipped parent chain blocks", + format: params.ArbosVersion_51, + previous: 100, + observed: 500, + expected: 101, + }, + { + name: "lower observation is unchanged", + format: params.ArbosVersion_51, + previous: 100, + observed: 99, + expected: 99, + }, + { + name: "maximum block number does not overflow", + format: params.ArbosVersion_51, + previous: math.MaxUint64 - 1, + observed: math.MaxUint64, + expected: math.MaxUint64, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + lastL2Block := arbosL2Header(test.previous, test.format) + actual := nextSequencerParentChainBlockNumber(test.observed, lastL2Block) + if actual != test.expected { + t.Fatalf("expected parent chain block %d, got %d", test.expected, actual) + } + }) + } +} + +func TestNextSequencerParentChainBlockNumberCatchesUpInOrder(t *testing.T) { + const observed = uint64(105) + lastL2Block := arbosL2Header(100, params.ArbosVersion_51) + + for expected := uint64(101); expected <= observed; expected++ { + actual := nextSequencerParentChainBlockNumber(observed, lastL2Block) + if actual != expected { + t.Fatalf("expected parent chain block %d, got %d", expected, actual) + } + lastL2Block = arbosL2Header(actual, params.ArbosVersion_51) + } +} + +func arbosL2Header(l1BlockNumber uint64, arbosFormatVersion uint64) *types.Header { + header := &types.Header{ + BaseFee: big.NewInt(1), + Difficulty: big.NewInt(1), + } + types.HeaderInfo{ + L1BlockNumber: l1BlockNumber, + ArbOSFormatVersion: arbosFormatVersion, + }.UpdateHeaderWithInfo(header) + return header +}