diff --git a/op-acceptance-tests/tests/sync/elsync/reorg/init_test.go b/op-acceptance-tests/tests/sync/elsync/reorg/init_test.go new file mode 100644 index 00000000000..d0a15b5d4d0 --- /dev/null +++ b/op-acceptance-tests/tests/sync/elsync/reorg/init_test.go @@ -0,0 +1,16 @@ +package reorg + +import ( + "testing" + + "github.com/ethereum-optimism/optimism/op-devstack/compat" + "github.com/ethereum-optimism/optimism/op-devstack/presets" +) + +func TestMain(m *testing.M) { + presets.DoMain(m, + presets.WithNewSingleChainMultiNodeWithTestSeq(), + presets.WithCompatibleTypes(compat.SysGo), + presets.WithNoDiscovery(), + ) +} diff --git a/op-acceptance-tests/tests/sync/elsync/reorg/sync_test.go b/op-acceptance-tests/tests/sync/elsync/reorg/sync_test.go new file mode 100644 index 00000000000..d7b10d8c47e --- /dev/null +++ b/op-acceptance-tests/tests/sync/elsync/reorg/sync_test.go @@ -0,0 +1,392 @@ +package reorg + +import ( + "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-devstack/stack/match" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" + "github.com/ethereum-optimism/optimism/op-test-sequencer/sequencer/seqtypes" + "github.com/ethereum/go-ethereum/common" +) + +// TestUnsafeGapFillAfterSafeReorg demonstrates the sequence: +// 1. Verifier CLP2P is disconnected and Verifier CL is stopped. +// 2. Safe reorg occurs because L1 reorged. +// 3. Verifier restarts, and consolidation drops the verifier previously-unsafe blocks. +// 4. CLP2P is restored, the verifier backfills and the unsafe gap is closed. +func TestUnsafeGapFillAfterSafeReorg(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewSingleChainMultiNodeWithTestSeq(t) + require := t.Require() + logger := t.Logger() + ctx := t.Ctx() + + ts := sys.TestSequencer.Escape().ControlAPI(sys.L1Network.ChainID()) + cl := sys.L1Network.Escape().L1CLNode(match.FirstL1CL) + + // Pass the L1 genesis + sys.L1Network.WaitForBlock() + + // Stop auto advancing L1 + sys.ControlPlane.FakePoSState(cl.ID(), stack.Stop) + + startL1Block := sys.L1EL.BlockRefByLabel(eth.Unsafe) + + require.Eventually(func() bool { + // Advance single L1 block + require.NoError(ts.New(ctx, seqtypes.BuildOpts{Parent: common.Hash{}})) + require.NoError(ts.Next(ctx)) + l1head := sys.L1EL.BlockRefByLabel(eth.Unsafe) + l2Safe := sys.L2EL.BlockRefByLabel(eth.Safe) + logger.Info("l1 info", "l1_head", l1head, "l1_origin", l2Safe.L1Origin, "l2Safe", l2Safe) + // Wait until safe L2 block has L1 origin point after the startL1Block + return l2Safe.Number > 0 && l2Safe.L1Origin.Number > startL1Block.Number + }, 120*time.Second, 2*time.Second) + + l2BlockBeforeReorg := sys.L2EL.BlockRefByLabel(eth.Safe) + logger.Info("Target L2 Block to reorg", "l2", l2BlockBeforeReorg, "l1_origin", l2BlockBeforeReorg.L1Origin) + + // Make sure verifier safe head is also advanced from reorgL2Block or matched + sys.L2ELB.Reached(eth.Safe, l2BlockBeforeReorg.Number, 3) + + // Disconnect CLP2P + sys.L2CLB.DisconnectPeer(sys.L2CL) + sys.L2CL.DisconnectPeer(sys.L2CLB) + + // Stop verifier CL + sys.L2CLB.Stop() + + // Reorg L1 block which safe block L1 Origin points to + l1BlockBeforeReorg := sys.L1EL.BlockRefByNumber(l2BlockBeforeReorg.L1Origin.Number) + logger.Info("Triggering L1 reorg", "l1", l1BlockBeforeReorg) + require.NoError(ts.New(ctx, seqtypes.BuildOpts{Parent: l1BlockBeforeReorg.ParentHash})) + require.NoError(ts.Next(ctx)) + + // Start advancing L1 + sys.ControlPlane.FakePoSState(cl.ID(), stack.Start) + + // Make sure L1 reorged + sys.L1EL.WaitForBlockNumber(l1BlockBeforeReorg.Number) + l1BlockAfterReorg := sys.L1EL.BlockRefByNumber(l1BlockBeforeReorg.Number) + logger.Info("Triggered L1 reorg", "l1", l1BlockAfterReorg) + require.NotEqual(l1BlockAfterReorg.Hash, l1BlockBeforeReorg.Hash) + + // Need to poll until the L2CL detects L1 Reorg and trigger L2 Reorg + // What happens: + // L2CL detects L1 Reorg and reset the pipeline. op-node example logs: "reset: detected L1 reorg" + // L2EL detects L2 Reorg and reorgs. op-geth example logs: "Chain reorg detected" + sys.L2EL.ReorgTriggered(l2BlockBeforeReorg, 30) + l2BlockAfterReorg := sys.L2EL.BlockRefByNumber(l2BlockBeforeReorg.Number) + require.NotEqual(l2BlockAfterReorg.Hash, l2BlockBeforeReorg.Hash) + logger.Info("Triggered L2 reorg", "l2", l2BlockAfterReorg) + // Batcher re-submits batch using updated L1 view + sys.L2EL.Reached(eth.Safe, l2BlockAfterReorg.Number, 30) + require.GreaterOrEqual(sys.L1EL.BlockRefByNumber(l2BlockAfterReorg.L1Origin.Number).Number, l1BlockAfterReorg.Number) + + // Check the divergence before restarting verifier L2CLB + verUnsafe := sys.L2ELB.BlockRefByLabel(eth.Unsafe) + seqUnsafe := sys.L2EL.BlockRefByLabel(eth.Unsafe) + logger.Info("Unsafe heads", "seq", seqUnsafe, "ver", verUnsafe) + // Verifier unsafe head cannot advance yet because L2CLB is down + require.Greater(seqUnsafe.Number, verUnsafe.Number) + // Verifier unsafe head diverged + canonicalFromSeq := sys.L2EL.BlockRefByNumber(verUnsafe.Number) + require.NotEqual(canonicalFromSeq.Hash, verUnsafe.Hash) + logger.Info("Verifer unsafe head diverged", "verUnsafe", verUnsafe, "canonical", canonicalFromSeq) + var rewindTo eth.L2BlockRef + for i := verUnsafe.Number; i > 0; i-- { + ver := sys.L2ELB.BlockRefByNumber(i) + seq := sys.L2EL.BlockRefByNumber(i) + if ver.Hash == seq.Hash { + rewindTo = ver + break + } + } + logger.Info("Verifier diverged", "rewindTo", rewindTo) + require.Greater(l1BlockAfterReorg.Number, rewindTo.L1Origin.Number) + + // Restart verifier L2CL. CLP2P disabled + sys.L2CLB.Start() + + // Safe block reorged. Verifier L2CL will read the new L1 and reorg the safe chain + // Unsafe head will also be updated because safe chain reorged + sys.L2ELB.ReorgTriggered(l2BlockBeforeReorg, 10) + logger.Info("Triggered L2 safe reorg at verifier", "l2", l2BlockAfterReorg) + + sys.L2ELB.Matched(sys.L2EL, eth.Safe, 5) + + // L2CLB has no P2P connection, so unsafe gap always exists + seqUnsafe = sys.L2EL.BlockRefByLabel(eth.Unsafe) + verUnsafe = sys.L2ELB.BlockRefByLabel(eth.Unsafe) + logger.Info("Verifier unsafe gap", "gap", seqUnsafe.Number-verUnsafe.Number, "seqUnsafe", seqUnsafe.Number, "verUnsafe", verUnsafe.Number) + + // Reenable CLP2P + // L2CLB will receive unsafe payloads from sequencer + // Unsafe gap will be observed by the L2CLB, and it will be smart enough to close the gap, + // using RR Sync(soon be deprecated), or rely on EL Sync(desired) + sys.L2CLB.ConnectPeer(sys.L2CL) + sys.L2CL.ConnectPeer(sys.L2CLB) + + // Unsafe gap is closed + sys.L2ELB.Matched(sys.L2EL, types.LocalUnsafe, 50) + + seqUnsafe = sys.L2EL.BlockRefByLabel(eth.Unsafe) + verUnsafe = sys.L2ELB.BlockRefByLabel(eth.Unsafe) + logger.Info("Verifier unsafe gap closed", "gap", seqUnsafe.Number-verUnsafe.Number, "seqUnsafe", seqUnsafe.Number, "verUnsafe", verUnsafe.Number) + + gt.Cleanup(func() { + sys.L2CLB.Start() + sys.L2CLB.ConnectPeer(sys.L2CL) + sys.L2CL.ConnectPeer(sys.L2CLB) + }) +} + +// TestUnsafeGapFillAfterUnsafeReorg_RestartL2CL demonstrates the flow where: +// 1. Verifier L2CL is stopped. +// 2. Unsafe reorg occurs because L1 reorged, +// 3. Verifier restarts and detects the L1 reorg, triggering its own unsafe reorg, +// 4. Verifier then backfills and closes the unsafe gap once reconnected via CLP2P. +func TestUnsafeGapFillAfterUnsafeReorg_RestartL2CL(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewSingleChainMultiNodeWithTestSeq(t) + require := t.Require() + logger := t.Logger() + ctx := t.Ctx() + + // Stop the batcher not to advance safe head + sys.L2Batcher.Stop() + + ts := sys.TestSequencer.Escape().ControlAPI(sys.L1Network.ChainID()) + cl := sys.L1Network.Escape().L1CLNode(match.FirstL1CL) + + // Pass the L1 genesis + sys.L1Network.WaitForBlock() + + // Stop auto advancing L1 + sys.ControlPlane.FakePoSState(cl.ID(), stack.Stop) + + startL1Block := sys.L1EL.BlockRefByLabel(eth.Unsafe) + + require.Eventually(func() bool { + // Advance single L1 block + require.NoError(ts.New(ctx, seqtypes.BuildOpts{Parent: common.Hash{}})) + require.NoError(ts.Next(ctx)) + l1head := sys.L1EL.BlockRefByLabel(eth.Unsafe) + l2Unsafe := sys.L2EL.BlockRefByLabel(eth.Unsafe) + logger.Info("l1 info", "l1_head", l1head, "l1_origin", l2Unsafe.L1Origin, "l2Unsafe", l2Unsafe) + // Wait until unsafe L2 block has L1 origin point after the startL1Block + return l2Unsafe.Number > 0 && l2Unsafe.L1Origin.Number > startL1Block.Number + }, 120*time.Second, 2*time.Second) + + sys.L2ELB.Matched(sys.L2EL, types.LocalUnsafe, 5) + + // Pick reorg block + l2BlockBeforeReorg := sys.L2EL.BlockRefByLabel(eth.Unsafe) + logger.Info("Target L2 Block to reorg", "l2", l2BlockBeforeReorg, "l1_origin", l2BlockBeforeReorg.L1Origin) + + // Make few more unsafe blocks which will be reorged out + sys.L2EL.Advanced(eth.Unsafe, 4) + sys.L2ELB.Matched(sys.L2EL, types.LocalUnsafe, 5) + + // Stop Verifier CL + sys.L2CLB.Stop() + + // Reorg L1 block which unsafe block L1 Origin points to + l1BlockBeforeReorg := sys.L1EL.BlockRefByNumber(l2BlockBeforeReorg.L1Origin.Number) + logger.Info("Triggering L1 reorg", "l1", l1BlockBeforeReorg) + require.NoError(ts.New(ctx, seqtypes.BuildOpts{Parent: l1BlockBeforeReorg.ParentHash})) + require.NoError(ts.Next(ctx)) + + // Start advancing L1 + sys.ControlPlane.FakePoSState(cl.ID(), stack.Start) + + // Make sure L1 reorged + sys.L1EL.WaitForBlockNumber(l1BlockBeforeReorg.Number) + l1BlockAfterReorg := sys.L1EL.BlockRefByNumber(l1BlockBeforeReorg.Number) + logger.Info("Triggered L1 reorg", "l1", l1BlockAfterReorg) + require.NotEqual(l1BlockAfterReorg.Hash, l1BlockBeforeReorg.Hash) + + // Need to poll until the L2CL detects L1 Reorg and trigger L2 Reorg + // What happens: + // L2CL detects L1 Reorg and reset the pipeline. op-node example logs: "reset: detected L1 reorg" + // L2EL detects L2 Reorg and reorgs. op-geth example logs: "Chain reorg detected" + sys.L2EL.ReorgTriggered(l2BlockBeforeReorg, 30) + l2BlockAfterReorg := sys.L2EL.BlockRefByNumber(l2BlockBeforeReorg.Number) + require.NotEqual(l2BlockAfterReorg.Hash, l2BlockBeforeReorg.Hash) + logger.Info("Triggered L2 reorg", "l2", l2BlockAfterReorg) + + // Check the divergence before restarting verifier L2CLB + verUnsafe := sys.L2ELB.BlockRefByLabel(eth.Unsafe) + seqUnsafe := sys.L2EL.BlockRefByLabel(eth.Unsafe) + logger.Info("Unsafe heads", "seq", seqUnsafe, "ver", verUnsafe) + // Verifier unsafe head cannot advance yet because L2CLB is down + require.Greater(seqUnsafe.Number, verUnsafe.Number) + // Verifier unsafe head diverged + canonicalFromSeq := sys.L2EL.BlockRefByNumber(verUnsafe.Number) + require.NotEqual(canonicalFromSeq.Hash, verUnsafe.Hash) + logger.Info("Verifer unsafe head diverged", "verUnsafe", verUnsafe, "canonical", canonicalFromSeq) + var rewindTo eth.L2BlockRef + for i := verUnsafe.Number; i > 0; i-- { + ver := sys.L2ELB.BlockRefByNumber(i) + seq := sys.L2EL.BlockRefByNumber(i) + if ver.Hash == seq.Hash { + rewindTo = ver + break + } + } + logger.Info("Verifier diverged", "rewindTo", rewindTo) + require.Greater(l1BlockAfterReorg.Number, rewindTo.L1Origin.Number) + + // Restart verifier L2CL + // L2CL walks back. op-node example logs "walking sync start" + // Dropping L2 blocks which has invalid L1 origin, until we reach rewindTo + sys.L2CLB.Start() + + // Make sure CLP2P is connected + sys.L2CLB.ConnectPeer(sys.L2CL) + sys.L2CL.ConnectPeer(sys.L2CLB) + + // L2CLB will receive unsafe payloads from sequencer + // Unsafe gap will be observed by the L2CLB, and it will be smart enough to close the gap, + // using RR Sync(soon be deprecated), or rely on EL Sync(desired) + + // Unsafe gap is closed + sys.L2ELB.Matched(sys.L2EL, types.LocalUnsafe, 50) + + seqUnsafe = sys.L2EL.BlockRefByLabel(eth.Unsafe) + verUnsafe = sys.L2ELB.BlockRefByLabel(eth.Unsafe) + logger.Info("Verifier unsafe gap closed", "gap", seqUnsafe.Number-verUnsafe.Number, "seqUnsafe", seqUnsafe.Number, "verUnsafe", verUnsafe.Number) + + gt.Cleanup(func() { + sys.L2Batcher.Start() + sys.L2CLB.Start() + sys.L2CLB.ConnectPeer(sys.L2CL) + sys.L2CL.ConnectPeer(sys.L2CLB) + }) +} + +// TestUnsafeGapFillAfterUnsafeReorg_RestartCLP2P demonstrates the flow where: +// 1. Verifier CLP2P is disconnected. +// 2. Unsafe reorg occurs because L1 reorged. +// 3. Verifier detects the L1 reorg, triggering its own unsafe reorg. +// 4. CLP2P is restored Verifier, the verifier backfills and the unsafe gap is closed. +func TestUnsafeGapFillAfterUnsafeReorg_RestartCLP2P(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewSingleChainMultiNodeWithTestSeq(t) + require := t.Require() + logger := t.Logger() + ctx := t.Ctx() + + // Stop the batcher not to advance safe head + sys.L2Batcher.Stop() + + ts := sys.TestSequencer.Escape().ControlAPI(sys.L1Network.ChainID()) + cl := sys.L1Network.Escape().L1CLNode(match.FirstL1CL) + + // Pass the L1 genesis + sys.L1Network.WaitForBlock() + + // Stop auto advancing L1 + sys.ControlPlane.FakePoSState(cl.ID(), stack.Stop) + + startL1Block := sys.L1EL.BlockRefByLabel(eth.Unsafe) + + require.Eventually(func() bool { + // Advance single L1 block + require.NoError(ts.New(ctx, seqtypes.BuildOpts{Parent: common.Hash{}})) + require.NoError(ts.Next(ctx)) + l1head := sys.L1EL.BlockRefByLabel(eth.Unsafe) + l2Unsafe := sys.L2EL.BlockRefByLabel(eth.Unsafe) + logger.Info("l1 info", "l1_head", l1head, "l1_origin", l2Unsafe.L1Origin, "l2Unsafe", l2Unsafe) + // Wait until unsafe L2 block has L1 origin point after the startL1Block + return l2Unsafe.Number > 0 && l2Unsafe.L1Origin.Number > startL1Block.Number + }, 120*time.Second, 2*time.Second) + + sys.L2ELB.Matched(sys.L2EL, types.LocalUnsafe, 5) + + // Pick reorg block + l2BlockBeforeReorg := sys.L2EL.BlockRefByLabel(eth.Unsafe) + logger.Info("Target L2 Block to reorg", "l2", l2BlockBeforeReorg, "l1_origin", l2BlockBeforeReorg.L1Origin) + + // Make few more unsafe blocks which will be reorged out + sys.L2EL.Advanced(eth.Unsafe, 4) + sys.L2ELB.Matched(sys.L2EL, types.LocalUnsafe, 5) + + // Disconnect CLP2P + sys.L2CLB.DisconnectPeer(sys.L2CL) + sys.L2CL.DisconnectPeer(sys.L2CLB) + + // verUnsafe will eventually reorged out + verUnsafe := sys.L2ELB.BlockRefByLabel(eth.Unsafe) + + // Reorg L1 block which unsafe block L1 Origin points to + l1BlockBeforeReorg := sys.L1EL.BlockRefByNumber(l2BlockBeforeReorg.L1Origin.Number) + logger.Info("Triggering L1 reorg", "l1", l1BlockBeforeReorg) + require.NoError(ts.New(ctx, seqtypes.BuildOpts{Parent: l1BlockBeforeReorg.ParentHash})) + require.NoError(ts.Next(ctx)) + + // Start advancing L1 + sys.ControlPlane.FakePoSState(cl.ID(), stack.Start) + + // Make sure L1 reorged + sys.L1EL.WaitForBlockNumber(l1BlockBeforeReorg.Number) + l1BlockAfterReorg := sys.L1EL.BlockRefByNumber(l1BlockBeforeReorg.Number) + logger.Info("Triggered L1 reorg", "l1", l1BlockAfterReorg) + require.NotEqual(l1BlockAfterReorg.Hash, l1BlockBeforeReorg.Hash) + + // Need to poll until the L2CL detects L1 Reorg and trigger L2 Reorg + // What happens: + // L2CL detects L1 Reorg and reset the pipeline. op-node example logs: "reset: detected L1 reorg" + // L2EL detects L2 Reorg and reorgs. op-geth example logs: "Chain reorg detected" + sys.L2EL.ReorgTriggered(l2BlockBeforeReorg, 30) + l2BlockAfterReorg := sys.L2EL.BlockRefByNumber(l2BlockBeforeReorg.Number) + require.NotEqual(l2BlockAfterReorg.Hash, l2BlockBeforeReorg.Hash) + logger.Info("Triggered L2 reorg", "l2", l2BlockAfterReorg) + + // L2CLB is still up but only have access to L1 to update canonical view + // verifier cannot advance unsafe head, but only reorging out blocks + // Test can still independently find rewindTo + rewindTo := sys.L2ELB.BlockRefByNumber(0) + for i := verUnsafe.Number; i > 0; i-- { + ref, err := sys.L2ELB.Escape().L2EthClient().L2BlockRefByNumber(ctx, i) + if err != nil { + // May be not found since verifier EL reorging + continue + } + if ref.L1Origin.Number < l1BlockAfterReorg.Number { + rewindTo = ref + break + } + } + logger.Info("Verifier diverged", "rewindTo", rewindTo) + + // Wait until verifier reset and dropped all reorg blocks + sys.L2CLB.Reset(types.LocalUnsafe, rewindTo) + logger.Info("Verifier rewind done", "rewindTo", rewindTo) + + // Make sure CLP2P is connected + sys.L2CLB.ConnectPeer(sys.L2CL) + sys.L2CL.ConnectPeer(sys.L2CLB) + + // L2CLB will receive unsafe payloads from sequencer + // Unsafe gap will be observed by the L2CLB, and it will be smart enough to close the gap, + // using RR Sync(soon be deprecated), or rely on EL Sync(desired) + + // Unsafe gap is closed + sys.L2ELB.Matched(sys.L2EL, types.LocalUnsafe, 50) + + seqUnsafe := sys.L2EL.BlockRefByLabel(eth.Unsafe) + verUnsafe = sys.L2ELB.BlockRefByLabel(eth.Unsafe) + logger.Info("Verifier unsafe gap closed", "gap", seqUnsafe.Number-verUnsafe.Number, "seqUnsafe", seqUnsafe.Number, "verUnsafe", verUnsafe.Number) + + gt.Cleanup(func() { + sys.L2Batcher.Start() + sys.L2CLB.ConnectPeer(sys.L2CL) + sys.L2CL.ConnectPeer(sys.L2CLB) + }) +} diff --git a/op-devstack/dsl/l2_cl.go b/op-devstack/dsl/l2_cl.go index d064f0019b1..2c565f19eff 100644 --- a/op-devstack/dsl/l2_cl.go +++ b/op-devstack/dsl/l2_cl.go @@ -375,3 +375,15 @@ func (cl *L2CLNode) SignalTarget(el *L2ELNode, targetNum uint64) { }) cl.require.NoErrorf(err, "failed to post unsafe payload via admin API: target %d", targetNum) } + +func (cl *L2CLNode) Reset(lvl types.SafetyLevel, target eth.L2BlockRef) { + cl.require.NoError(retry.Do0(cl.ctx, 5, &retry.FixedStrategy{Dur: 2 * time.Second}, + func() error { + res := cl.HeadBlockRef(lvl) + cl.log.Info("Chain sync Status", lvl, res) + if res.Hash == target.Hash { + return nil + } + return errors.New("waiting to reset") + })) +} diff --git a/op-devstack/dsl/l2_el.go b/op-devstack/dsl/l2_el.go index 33b8b73fd6f..fe0c2518eab 100644 --- a/op-devstack/dsl/l2_el.go +++ b/op-devstack/dsl/l2_el.go @@ -12,6 +12,7 @@ import ( "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/predeploys" "github.com/ethereum-optimism/optimism/op-service/retry" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" "github.com/ethereum/go-ethereum/common" ) @@ -273,3 +274,27 @@ func (el *L2ELNode) FinishedELSync(refNode *L2ELNode, unsafe, safe, finalized ui return errors.New("EL Sync not yet triggered") })) } + +func (el *L2ELNode) ChainSyncStatus(chainID eth.ChainID, lvl types.SafetyLevel) eth.BlockID { + el.require.Equal(chainID, el.inner.ID().ChainID(), "chain ID mismatch") + var blockRef eth.L2BlockRef + switch lvl { + case types.Finalized: + blockRef = el.BlockRefByLabel(eth.Finalized) + case types.CrossSafe, types.LocalSafe: + blockRef = el.BlockRefByLabel(eth.Safe) + case types.CrossUnsafe, types.LocalUnsafe: + blockRef = el.BlockRefByLabel(eth.Unsafe) + default: + el.require.NoError(errors.New("invalid safety level")) + } + return blockRef.ID() +} + +func (el *L2ELNode) MatchedFn(refNode SyncStatusProvider, lvl types.SafetyLevel, attempts int) CheckFunc { + return MatchedFn(el, refNode, el.log, el.ctx, lvl, el.ChainID(), attempts) +} + +func (el *L2ELNode) Matched(refNode SyncStatusProvider, lvl types.SafetyLevel, attempts int) { + el.require.NoError(el.MatchedFn(refNode, lvl, attempts)()) +} diff --git a/op-devstack/presets/cl_config.go b/op-devstack/presets/cl_config.go index 95a3d900ae6..1a425dfc8de 100644 --- a/op-devstack/presets/cl_config.go +++ b/op-devstack/presets/cl_config.go @@ -39,3 +39,11 @@ func WithReqRespSyncDisabled() stack.CommonOption { cfg.EnableReqRespSync = false }))) } + +func WithNoDiscovery() stack.CommonOption { + return stack.MakeCommon( + sysgo.WithGlobalL2CLOption(sysgo.L2CLOptionFn( + func(_ devtest.P, id stack.L2CLNodeID, cfg *sysgo.L2CLConfig) { + cfg.NoDiscovery = true + }))) +} diff --git a/op-devstack/presets/singlechain_multinode.go b/op-devstack/presets/singlechain_multinode.go index c417b435f67..e51ca6ea8a1 100644 --- a/op-devstack/presets/singlechain_multinode.go +++ b/op-devstack/presets/singlechain_multinode.go @@ -57,3 +57,40 @@ func NewSingleChainMultiNodeWithoutCheck(t devtest.T) *SingleChainMultiNode { func WithSingleChainMultiNodeWithoutP2P() stack.CommonOption { return stack.MakeCommon(sysgo.DefaultSingleChainMultiNodeSystemWithoutP2P(&sysgo.DefaultSingleChainMultiNodeSystemIDs{})) } + +type SingleChainMultiNodeWithTestSeq struct { + SingleChainMultiNode + + TestSequencer *dsl.TestSequencer +} + +func NewSingleChainMultiNodeWithTestSeq(t devtest.T) *SingleChainMultiNodeWithTestSeq { + system := shim.NewSystem(t) + orch := Orchestrator() + orch.Hydrate(system) + minimal := minimalFromSystem(t, system, orch) + l2 := system.L2Network(match.Assume(t, match.L2ChainA)) + verifierCL := l2.L2CLNode(match.Assume(t, + match.And( + match.Not(match.WithSequencerActive(t.Ctx())), + match.Not[stack.L2CLNodeID, stack.L2CLNode](minimal.L2CL.ID()), + ))) + verifierEL := l2.L2ELNode(match.Assume(t, + match.And( + match.EngineFor(verifierCL), + match.Not[stack.L2ELNodeID, stack.L2ELNode](minimal.L2EL.ID())))) + preset := &SingleChainMultiNode{ + Minimal: *minimal, + L2ELB: dsl.NewL2ELNode(verifierEL, orch.ControlPlane()), + L2CLB: dsl.NewL2CLNode(verifierCL, orch.ControlPlane()), + } + out := &SingleChainMultiNodeWithTestSeq{ + SingleChainMultiNode: *preset, + TestSequencer: dsl.NewTestSequencer(system.TestSequencer(match.Assume(t, match.FirstTestSequencer))), + } + return out +} + +func WithNewSingleChainMultiNodeWithTestSeq() stack.CommonOption { + return stack.MakeCommon(sysgo.DefaultSingleChainMultiNodeWithTestSeqSystem(&sysgo.DefaultSingleChainMultiNodeWithTestSeqSystemIDs{})) +} diff --git a/op-devstack/sysgo/l2_cl.go b/op-devstack/sysgo/l2_cl.go index 2c95439b693..8e69113d8eb 100644 --- a/op-devstack/sysgo/l2_cl.go +++ b/op-devstack/sysgo/l2_cl.go @@ -30,6 +30,9 @@ type L2CLConfig struct { // EnableReqRespSync is the flag to enable/disable req-resp sync. EnableReqRespSync bool + + // NoDiscovery is the flag to enable/disable discovery + NoDiscovery bool } func L2CLSequencer() L2CLOption { @@ -52,6 +55,7 @@ func DefaultL2CLConfig() *L2CLConfig { IsSequencer: false, IndexingMode: false, EnableReqRespSync: true, + NoDiscovery: false, } } diff --git a/op-devstack/sysgo/l2_cl_opnode.go b/op-devstack/sysgo/l2_cl_opnode.go index 99fcc60b066..dbcf1465cd5 100644 --- a/op-devstack/sysgo/l2_cl_opnode.go +++ b/op-devstack/sysgo/l2_cl_opnode.go @@ -226,6 +226,7 @@ func WithOpNode(l2CLID stack.L2CLNodeID, l1CLID stack.L1CLNodeID, l1ELID stack.L } p2pConfig, err = p2pcli.NewConfig(cliCtx, l2Net.rollupCfg.BlockTime) require.NoError(err, "failed to load p2p config") + p2pConfig.NoDiscovery = cfg.NoDiscovery } // specify interop config, but do not configure anything, to disable indexing mode diff --git a/op-devstack/sysgo/system_singlechain_multinode.go b/op-devstack/sysgo/system_singlechain_multinode.go index 93bf33ae013..c4892038369 100644 --- a/op-devstack/sysgo/system_singlechain_multinode.go +++ b/op-devstack/sysgo/system_singlechain_multinode.go @@ -12,6 +12,12 @@ type DefaultSingleChainMultiNodeSystemIDs struct { L2ELB stack.L2ELNodeID } +type DefaultSingleChainMultiNodeWithTestSeqSystemIDs struct { + DefaultSingleChainMultiNodeSystemIDs + + TestSequencer stack.TestSequencerID +} + func NewDefaultSingleChainMultiNodeSystemIDs(l1ID, l2ID eth.ChainID) DefaultSingleChainMultiNodeSystemIDs { minimal := NewDefaultMinimalSystemIDs(l1ID, l2ID) return DefaultSingleChainMultiNodeSystemIDs{ @@ -21,6 +27,13 @@ func NewDefaultSingleChainMultiNodeSystemIDs(l1ID, l2ID eth.ChainID) DefaultSing } } +func NewDefaultSingleChainMultiNodeWithTestSeqSystemIDs(l1ID, l2ID eth.ChainID) DefaultSingleChainMultiNodeWithTestSeqSystemIDs { + return DefaultSingleChainMultiNodeWithTestSeqSystemIDs{ + DefaultSingleChainMultiNodeSystemIDs: NewDefaultSingleChainMultiNodeSystemIDs(l1ID, l2ID), + TestSequencer: "dev", + } +} + func DefaultSingleChainMultiNodeSystem(dest *DefaultSingleChainMultiNodeSystemIDs) stack.Option[*Orchestrator] { ids := NewDefaultSingleChainMultiNodeSystemIDs(DefaultL1ID, DefaultL2AID) @@ -40,6 +53,19 @@ func DefaultSingleChainMultiNodeSystem(dest *DefaultSingleChainMultiNodeSystemID return opt } +func DefaultSingleChainMultiNodeWithTestSeqSystem(dest *DefaultSingleChainMultiNodeWithTestSeqSystemIDs) stack.Option[*Orchestrator] { + ids := NewDefaultSingleChainMultiNodeWithTestSeqSystemIDs(DefaultL1ID, DefaultL2AID) + opt := stack.Combine[*Orchestrator]() + opt.Add(DefaultSingleChainMultiNodeSystem(&dest.DefaultSingleChainMultiNodeSystemIDs)) + + opt.Add(WithTestSequencer(ids.TestSequencer, ids.L1CL, ids.L2CL, ids.L1EL, ids.L2EL)) + + opt.Add(stack.Finally(func(orch *Orchestrator) { + *dest = ids + })) + return opt +} + func DefaultSingleChainMultiNodeSystemWithoutP2P(dest *DefaultSingleChainMultiNodeSystemIDs) stack.Option[*Orchestrator] { ids := NewDefaultSingleChainMultiNodeSystemIDs(DefaultL1ID, DefaultL2AID)