Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelog/exocognosis-nit-4718.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
### Fixed
- Prevent sequencer parent-chain block number jumps from leaving stale hashes in the ArbOS blockhash ring buffer.
7 changes: 7 additions & 0 deletions execution/gethexec/sequencer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
24 changes: 24 additions & 0 deletions execution/gethexec/sequencer_l1_block.go
Original file line number Diff line number Diff line change
@@ -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
}
106 changes: 106 additions & 0 deletions execution/gethexec/sequencer_l1_block_test.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading