diff --git a/op-node/rollup/sequencing/origin_selector.go b/op-node/rollup/sequencing/origin_selector.go index 255522d4644..c5f23407d76 100644 --- a/op-node/rollup/sequencing/origin_selector.go +++ b/op-node/rollup/sequencing/origin_selector.go @@ -73,6 +73,8 @@ func (los *L1OriginSelector) OnEvent(ctx context.Context, ev event.Event) bool { // FindL1Origin determines what the next L1 Origin should be. // The L1 Origin is either the L2 Head's Origin, or the following L1 block // if the next L2 block's time is greater than or equal to the L2 Head's Origin. +// The origin selection relies purely on block numbers and it is the caller's +// responsibility to detect and handle L1 reorgs. func (los *L1OriginSelector) FindL1Origin(ctx context.Context, l2Head eth.L2BlockRef) (eth.L1BlockRef, error) { currentOrigin, nextOrigin, err := los.CurrentAndNextOrigin(ctx, l2Head) if err != nil { @@ -170,8 +172,10 @@ func (los *L1OriginSelector) maybeSetNextOrigin(nextOrigin eth.L1BlockRef) { los.mu.Lock() defer los.mu.Unlock() - // Set the next origin if it is the immediate child of the current origin. - if nextOrigin.ParentHash == los.currentOrigin.Hash { + // Set the next origin if it is the subsequent block by number. + // On reorgs, this might not be the immediate child of the current origin + // since the hash is not checked. + if nextOrigin.Number == los.currentOrigin.Number+1 { los.nextOrigin = nextOrigin } } diff --git a/op-node/rollup/sequencing/origin_selector_test.go b/op-node/rollup/sequencing/origin_selector_test.go index 9ec91af0901..be8a0dc7625 100644 --- a/op-node/rollup/sequencing/origin_selector_test.go +++ b/op-node/rollup/sequencing/origin_selector_test.go @@ -124,7 +124,6 @@ func TestOriginSelectorFetchNextError(t *testing.T) { // is no conf depth to stop the origin selection so block `b` should // be the next L1 origin, and then block `c` is the subsequent L1 origin. func TestOriginSelectorAdvances(t *testing.T) { - testOriginSelectorAdvances := func(t *testing.T, recoverMode bool) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -340,6 +339,85 @@ func TestOriginSelectorFetchesNextOrigin(t *testing.T) { require.Equal(t, b, next) } +// TestOriginSelectorHandlesReorg ensures that the origin selector +// can handle the current origin being reorged out +// +// There are 3 blocks [a, b, c]. After advancing to b, a reorg is simulated +// where b is reorged and replaced by providing a `c` next that has a different parent hash. +// The origin should still provide c as the next origin so upstream services can detect the reorg. +func TestOriginSelectorHandlesReorg(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + log := testlog.Logger(t, log.LevelDebug) + cfg := &rollup.Config{ + MaxSequencerDrift: 500, + BlockTime: 2, + } + l1 := &testutils.MockL1Source{} + defer l1.AssertExpectations(t) + a := eth.L1BlockRef{ + Hash: common.Hash{'a'}, + Number: 10, + Time: 20, + } + b := eth.L1BlockRef{ + Hash: common.Hash{'b'}, + Number: 11, + Time: 22, + ParentHash: a.Hash, + } + l2Head := eth.L2BlockRef{ + L1Origin: a.ID(), + Time: 24, + } + + // This is called as part of the background prefetch job + l1.ExpectL1BlockRefByNumber(b.Number, b, nil) + + s := NewL1OriginSelector(ctx, log, cfg, l1) + s.currentOrigin = a + + requireFindl1OriginEqual := func(l1ref eth.L1BlockRef) { + next, err := s.FindL1Origin(ctx, l2Head) + require.NoError(t, err) + require.Equal(t, l1ref, next) + } + + requireFindl1OriginEqual(a) + + // Selection is stable until the next origin is fetched + requireFindl1OriginEqual(a) + + // Trigger the background fetch via a forkchoice update + handled := s.OnEvent(context.Background(), engine.ForkchoiceUpdateEvent{UnsafeL2Head: l2Head}) + require.True(t, handled) + + // The next origin should be `b` now. + requireFindl1OriginEqual(b) + + // A reorg happens and `b` is replaced by a block with a different hash + c := eth.L1BlockRef{ + Hash: common.Hash{'c'}, + Number: 12, + Time: 24, + ParentHash: common.Hash{'b', '2'}, + } + l1.ExpectL1BlockRefByNumber(c.Number, c, nil) + l2Head = eth.L2BlockRef{ + L1Origin: b.ID(), + Time: 26, + } + + // Trigger the background fetch via a forkchoice update + handled = s.OnEvent(context.Background(), engine.ForkchoiceUpdateEvent{UnsafeL2Head: l2Head}) + require.True(t, handled) + + // The next origin should be `c` now, otherwise an upstream service cannot detect the reorg + // and the origin will be stuck at `b` + requireFindl1OriginEqual(c) +} + // TestOriginSelectorRespectsOriginTiming ensures that the origin selector // does not pick an origin that is ahead of the next L2 block time //