diff --git a/.github/workflows/e2e_tests_supervisor.yaml b/.github/workflows/e2e_tests_supervisor.yaml index cf1d8b0aa1..d449cca0cb 100644 --- a/.github/workflows/e2e_tests_supervisor.yaml +++ b/.github/workflows/e2e_tests_supervisor.yaml @@ -18,6 +18,8 @@ jobs: test-pkg: message - devnet-config: simple-supervisor test-pkg: rpc + - devnet-config: simple-supervisor + test-pkg: l2reorg - devnet-config: preinterop-supervisor test-pkg: pre_interop steps: diff --git a/tests/go.mod b/tests/go.mod index ffe85a3e33..0c313bad7d 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -3,7 +3,7 @@ module github.com/op-rs/kona go 1.24.3 // We're using the "develop" branch of the Optimism repo to include the latest changes to the `devstack` package. -require github.com/ethereum-optimism/optimism v1.13.6-0.20250815152137-c12091dfd6a8 +require github.com/ethereum-optimism/optimism v1.13.6-0.20250818173129-f6c1dcf0ca26 require github.com/stretchr/testify v1.10.0 diff --git a/tests/go.sum b/tests/go.sum index 6647cc5abe..698cfecf69 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -197,6 +197,8 @@ github.com/ethereum-optimism/op-geth v1.101511.1-dev.1.0.20250608235258-6005dd53 github.com/ethereum-optimism/op-geth v1.101511.1-dev.1.0.20250608235258-6005dd53e1b5/go.mod h1:SkytozVEPtnUeBlquwl0Qv5JKvrN/Y5aqh+VkQo/EOI= github.com/ethereum-optimism/optimism v1.13.6-0.20250815152137-c12091dfd6a8 h1:BhDvgZiRFg4uCweTN6sHPENRXBLIl4XQVGe3lZMWF+4= github.com/ethereum-optimism/optimism v1.13.6-0.20250815152137-c12091dfd6a8/go.mod h1:+8BrJe6eeys3M4bDLKul3e8ui4vwHppq9mgUqVMXZhU= +github.com/ethereum-optimism/optimism v1.13.6-0.20250818173129-f6c1dcf0ca26 h1:/L1WadYxiU0pCPFn3v48sxhkvDhpqqv19sGzUFhxgI8= +github.com/ethereum-optimism/optimism v1.13.6-0.20250818173129-f6c1dcf0ca26/go.mod h1:+8BrJe6eeys3M4bDLKul3e8ui4vwHppq9mgUqVMXZhU= github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20250603144016-9c45ca7d4508 h1:A/3QVFt+Aa9ozpPVXxUTLui8honBjSusAaiCVRbafgs= github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20250603144016-9c45ca7d4508/go.mod h1:NZ816PzLU1TLv1RdAvYAb6KWOj4Zm5aInT0YpDVml2Y= github.com/ethereum/c-kzg-4844/v2 v2.1.0 h1:gQropX9YFBhl3g4HYhwE70zq3IHFRgbbNPw0Shwzf5w= diff --git a/tests/supervisor/l2reorg/init_exec_msg_test.go b/tests/supervisor/l2reorg/init_exec_msg_test.go new file mode 100644 index 0000000000..b54f5e8203 --- /dev/null +++ b/tests/supervisor/l2reorg/init_exec_msg_test.go @@ -0,0 +1,247 @@ +package l2reorg + +import ( + "math/rand" + "testing" + "time" + + "github.com/ethereum-optimism/optimism/devnet-sdk/contracts/bindings" + "github.com/ethereum-optimism/optimism/devnet-sdk/contracts/constants" + "github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/interop" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-devstack/stack/match" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/txintent" + "github.com/ethereum-optimism/optimism/op-service/txplan" + "github.com/ethereum-optimism/optimism/op-test-sequencer/sequencer/seqtypes" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/require" +) + +// Acceptance Test: https://github.com/ethereum-optimism/optimism/blob/develop/op-acceptance-tests/tests/interop/reorgs/init_exec_msg_test.go#L25 +func TestReorgInitExecMsg(gt *testing.T) { + t := devtest.SerialT(gt) + ctx := t.Ctx() + + sys := presets.NewSimpleInterop(t) + l := sys.Log + + ia := sys.TestSequencer.Escape().ControlAPI(sys.L2ChainA.ChainID()) + + // three EOAs for triggering the init and exec interop txs, as well as a simple transfer tx + alice := sys.FunderA.NewFundedEOA(eth.OneHundredthEther) + bob := sys.FunderB.NewFundedEOA(eth.OneHundredthEther) + cathrine := sys.FunderA.NewFundedEOA(eth.OneHundredthEther) + + sys.L1Network.WaitForBlock() + sys.L2ChainA.WaitForBlock() + + // stop batchers on chain A and on chain B + sys.L2BatcherA.Stop() + sys.L2BatcherB.Stop() + + // deploy event logger on chain A + var eventLoggerAddress common.Address + { + tx := txplan.NewPlannedTx(txplan.Combine( + alice.Plan(), + txplan.WithData(common.FromHex(bindings.EventloggerBin)), + )) + res, err := tx.Included.Eval(ctx) + require.NoError(t, err) + + eventLoggerAddress = res.ContractAddress + l.Info("deployed EventLogger", "chainID", tx.ChainID.Value(), "address", eventLoggerAddress) + } + + sys.L1Network.WaitForBlock() + + var initTrigger *txintent.InitTrigger + // prepare init trigger (i.e. what logs to emit on chain A) + { + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + nTopics := 3 + lenData := 10 + initTrigger = interop.RandomInitTrigger(rng, eventLoggerAddress, nTopics, lenData) + + l.Info("created init trigger", "address", eventLoggerAddress, "topics", nTopics, "lenData", lenData) + } + + // wait for chain B to catch up to chain A if necessary + sys.L2ChainB.CatchUpTo(sys.L2ChainA) + + var initTx *txintent.IntentTx[*txintent.InitTrigger, *txintent.InteropOutput] + var initReceipt *types.Receipt + // prepare and include initiating message on chain A + { + initTx = txintent.NewIntent[*txintent.InitTrigger, *txintent.InteropOutput](alice.Plan()) + initTx.Content.Set(initTrigger) + var err error + initReceipt, err = initTx.PlannedTx.Included.Eval(ctx) + require.NoError(t, err) + + l.Info("initiating message included", "chain", sys.L2ChainA.ChainID(), "block_number", initReceipt.BlockNumber, "block_hash", initReceipt.BlockHash, "now", time.Now().Unix()) + } + + // stop sequencer on chain A so that we later force a reorg/removal of the init msg + sys.L2CLA.StopSequencer() + + // at least one block between the init tx on chain A and the exec tx on chain B + sys.L2ChainB.WaitForBlock() + + var execTx *txintent.IntentTx[*txintent.ExecTrigger, *txintent.InteropOutput] + var execReceipt *types.Receipt + // prepare and include executing message on chain B + { + execTx = txintent.NewIntent[*txintent.ExecTrigger, *txintent.InteropOutput](bob.Plan()) + execTx.Content.DependOn(&initTx.Result) + // single event in tx so index is 0. ExecuteIndexed returns a lambda to transform InteropOutput to a new ExecTrigger + execTx.Content.Fn(txintent.ExecuteIndexed(constants.CrossL2Inbox, &initTx.Result, 0)) + var err error + execReceipt, err = execTx.PlannedTx.Included.Eval(ctx) + require.NoError(t, err) + require.Equal(t, 1, len(execReceipt.Logs)) + + l.Info("executing message included", "chain", sys.L2ChainB.ChainID(), "block_number", execReceipt.BlockNumber, "block_hash", execReceipt.BlockHash, "now", time.Now().Unix()) + } + + // record divergence block numbers and original refs for future validation checks + var divergenceBlockNumber_A, divergenceBlockNumber_B uint64 + var originalRef_A, originalRef_B eth.L2BlockRef + + // sequence a conflicting block with a simple transfer tx, based on the parent of the parent of the unsafe head + { + var err error + divergenceBlockNumber_B = execReceipt.BlockNumber.Uint64() + originalRef_B, err = sys.L2ELB.Escape().L2EthClient().L2BlockRefByHash(ctx, execReceipt.BlockHash) + require.NoError(t, err, "Expected to be able to call L2BlockRefByHash API, but got error") + + headToReorgA := initReceipt.BlockHash + headToReorgARef, err := sys.L2ELA.Escape().L2EthClient().L2BlockRefByHash(ctx, headToReorgA) + require.NoError(t, err, "Expected to be able to call L2BlockRefByHash API, but got error") + + divergenceBlockNumber_A = headToReorgARef.Number + originalRef_A = headToReorgARef + + parentOfHeadToReorgA := headToReorgARef.ParentID() + parentsL1Origin, err := sys.L2ELA.Escape().L2EthClient().L2BlockRefByHash(ctx, parentOfHeadToReorgA.Hash) + require.NoError(t, err, "Expected to be able to call L2BlockRefByHash API, but got error") + + nextL1Origin := parentsL1Origin.L1Origin.Number + 1 + l1Origin, err := sys.L1Network.Escape().L1ELNode(match.FirstL1EL).EthClient().InfoByNumber(ctx, nextL1Origin) + require.NoError(t, err, "Expected to get block number %v from L1 execution client", nextL1Origin) + l1OriginHash := l1Origin.Hash() + + l.Info("Sequencing a conflicting block", "chain", sys.L2ChainA.ChainID(), "newL1Origin", eth.ToBlockID(l1Origin), "headToReorgA", headToReorgARef, "parent", parentOfHeadToReorgA, "parent_l1_origin", parentsL1Origin.L1Origin) + + err = ia.New(ctx, seqtypes.BuildOpts{ + Parent: parentOfHeadToReorgA.Hash, + L1Origin: &l1OriginHash, + }) + require.NoError(t, err, "Expected to be able to create a new block job for sequencing on op-test-sequencer, but got error") + + // include simple transfer tx in opened block + { + to := cathrine.PlanTransfer(alice.Address(), eth.OneGWei) + opt := txplan.Combine(to) + ptx := txplan.NewPlannedTx(opt) + signed_tx, err := ptx.Signed.Eval(ctx) + require.NoError(t, err, "Expected to be able to evaluate a planned transaction on op-test-sequencer, but got error") + txdata, err := signed_tx.MarshalBinary() + require.NoError(t, err, "Expected to be able to marshal a signed transaction on op-test-sequencer, but got error") + + err = ia.IncludeTx(ctx, txdata) + require.NoError(t, err, "Expected to be able to include a signed transaction on op-test-sequencer, but got error") + } + + err = ia.Next(ctx) + require.NoError(t, err, "Expected to be able to call Next() after New() on op-test-sequencer, but got error") + } + + // sequence a second block with op-test-sequencer + { + unsafe := sys.L2ELA.BlockRefByLabel(eth.Unsafe) + l.Info("Current unsafe ref", "unsafeHead", unsafe) + err := ia.New(ctx, seqtypes.BuildOpts{ + Parent: unsafe.Hash, + L1Origin: nil, + }) + require.NoError(t, err, "Expected to be able to create a new block job for sequencing on op-test-sequencer, but got error") + + err = ia.Next(ctx) + require.NoError(t, err, "Expected to be able to call Next() after New() on op-test-sequencer, but got error") + } + + // continue sequencing with op-node + sys.L2CLA.StartSequencer() + + // start batchers on chain A and on chain B + sys.L2BatcherA.Start() + sys.L2BatcherB.Start() + + // wait and confirm reorgs on chain A and B + dsl.CheckAll(t, + sys.L2ELA.ReorgTriggeredFn(eth.L2BlockRef{ + Number: divergenceBlockNumber_A, + Hash: originalRef_A.Hash, + ParentHash: originalRef_A.ParentID().Hash, + }, 30), + sys.L2ELB.ReorgTriggeredFn(eth.L2BlockRef{ + Number: divergenceBlockNumber_B, + Hash: originalRef_B.Hash, + ParentHash: originalRef_B.ParentID().Hash, + }, 30), + ) + + // executing tx should eventually be no longer confirmed on chain B + require.Eventually(t, func() bool { + receipt, err := sys.L2ELB.Escape().EthClient().TransactionReceipt(ctx, execReceipt.TxHash) + if err == nil || err.Error() != "not found" { // want to get "not found" error + return false + } + if receipt != nil { // want to get nil receipt + return false + } + return true + }, 60*time.Second, 3*time.Second, "Expected for the executing tx to be removed from chain B") + + err := wait.For(ctx, 5*time.Second, func() (bool, error) { + safeL2Head_supervisor_A := sys.Supervisor.SafeBlockID(sys.L2ChainA.ChainID()).Hash + safeL2Head_supervisor_B := sys.Supervisor.SafeBlockID(sys.L2ChainB.ChainID()).Hash + safeL2Head_sequencer_A := sys.L2CLA.SafeL2BlockRef() + safeL2Head_sequencer_B := sys.L2CLB.SafeL2BlockRef() + + if safeL2Head_sequencer_A.Number < divergenceBlockNumber_A { + l.Info("Safe ref number is still behind divergence block A number", "divergence", divergenceBlockNumber_A, "safe", safeL2Head_sequencer_A.Number) + return false, nil + } + + if safeL2Head_sequencer_B.Number < divergenceBlockNumber_B { + l.Info("Safe ref number is still behind divergence block B number", "divergence", divergenceBlockNumber_B, "safe", safeL2Head_sequencer_B.Number) + return false, nil + } + + if safeL2Head_sequencer_A.Hash.Cmp(safeL2Head_supervisor_A) != 0 { + l.Info("Safe ref still not the same on supervisor and sequencer A", "supervisor", safeL2Head_supervisor_A, "sequencer", safeL2Head_sequencer_A.Hash) + return false, nil + } + + if safeL2Head_sequencer_B.Hash.Cmp(safeL2Head_supervisor_B) != 0 { + l.Info("Safe ref still not the same on supervisor and sequencer B", "supervisor", safeL2Head_supervisor_B, "sequencer", safeL2Head_sequencer_B.Hash) + return false, nil + } + + l.Info("Safe ref the same across supervisor and sequencers", + "supervisor_A", safeL2Head_supervisor_A, + "sequencer_A", safeL2Head_sequencer_A.Hash, + "supervisor_B", safeL2Head_supervisor_B, + "sequencer_B", safeL2Head_sequencer_B.Hash) + + return true, nil + }) + require.NoError(t, err, "Expected to get same safe ref on both supervisor and sequencer eventually") +} diff --git a/tests/supervisor/l2reorg/init_test.go b/tests/supervisor/l2reorg/init_test.go new file mode 100644 index 0000000000..a20170b148 --- /dev/null +++ b/tests/supervisor/l2reorg/init_test.go @@ -0,0 +1,13 @@ +package l2reorg + +import ( + "testing" + + "github.com/ethereum-optimism/optimism/op-devstack/presets" +) + +// TestMain creates the test-setups against the shared backend +func TestMain(m *testing.M) { + // Other setups may be added here, hydrated from the same orchestrator + presets.DoMain(m, presets.WithSimpleInterop()) +} diff --git a/tests/supervisor/l2reorg/invalid_exec_msgs_test.go b/tests/supervisor/l2reorg/invalid_exec_msgs_test.go new file mode 100644 index 0000000000..e082a6cc4b --- /dev/null +++ b/tests/supervisor/l2reorg/invalid_exec_msgs_test.go @@ -0,0 +1,244 @@ +package l2reorg + +import ( + "context" + "fmt" + "math/rand" + "testing" + "time" + + "github.com/ethereum-optimism/optimism/devnet-sdk/contracts/bindings" + "github.com/ethereum-optimism/optimism/devnet-sdk/contracts/constants" + "github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/interop" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/txintent" + "github.com/ethereum-optimism/optimism/op-service/txplan" + suptypes "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" + "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/require" +) + +// TestReorgInvalidExecMsgs tests that the supervisor reorgs the chain when an invalid exec msg is included +// Each subtest runs a test with a different invalid message, by modifying the message in the txModifierFn +// Acceptance Test: https://github.com/ethereum-optimism/optimism/blob/develop/op-acceptance-tests/tests/interop/reorgs/invalid_exec_msgs_test.go#L28 +func TestReorgInvalidExecMsgs(gt *testing.T) { + gt.Run("invalid log index", func(gt *testing.T) { + testReorgInvalidExecMsg(gt, func(msg *suptypes.Message) { + msg.Identifier.LogIndex = 1024 + }) + }) + + gt.Run("invalid block number", func(gt *testing.T) { + testReorgInvalidExecMsg(gt, func(msg *suptypes.Message) { + msg.Identifier.BlockNumber = msg.Identifier.BlockNumber - 1 + }) + }) + + gt.Run("invalid chain id", func(gt *testing.T) { + testReorgInvalidExecMsg(gt, func(msg *suptypes.Message) { + msg.Identifier.ChainID = eth.ChainIDFromUInt64(1024) + }) + }) +} + +func testReorgInvalidExecMsg(gt *testing.T, txModifierFn func(msg *suptypes.Message)) { + t := devtest.SerialT(gt) + ctx := t.Ctx() + + sys := presets.NewSimpleInterop(t) + l := sys.Log + + ia := sys.TestSequencer.Escape().ControlAPI(sys.L2ChainA.ChainID()) + + // three EOAs for triggering the init and exec interop txs, as well as a simple transfer tx + alice := sys.FunderA.NewFundedEOA(eth.OneHundredthEther) + bob := sys.FunderB.NewFundedEOA(eth.OneHundredthEther) + cathrine := sys.FunderA.NewFundedEOA(eth.OneHundredthEther) + + sys.L1Network.WaitForBlock() + sys.L2ChainA.WaitForBlock() + + // stop batcher on chain A + sys.L2BatcherA.Stop() + + // deploy event logger on chain B + var eventLoggerAddress common.Address + { + tx := txplan.NewPlannedTx(txplan.Combine( + bob.Plan(), + txplan.WithData(common.FromHex(bindings.EventloggerBin)), + )) + res, err := tx.Included.Eval(ctx) + require.NoError(t, err) + + eventLoggerAddress = res.ContractAddress + l.Info("deployed EventLogger", "chainID", tx.ChainID.Value(), "address", eventLoggerAddress) + } + + sys.L1Network.WaitForBlock() + + var initTrigger *txintent.InitTrigger + // prepare init trigger (i.e. what logs to emit on chain A) + { + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + nTopics := 3 + lenData := 10 + initTrigger = interop.RandomInitTrigger(rng, eventLoggerAddress, nTopics, lenData) + + l.Info("created init trigger", "address", eventLoggerAddress, "topics", nTopics, "lenData", lenData) + } + + // wait for chain B to catch up to chain A if necessary + sys.L2ChainB.CatchUpTo(sys.L2ChainA) + + var initTx *txintent.IntentTx[*txintent.InitTrigger, *txintent.InteropOutput] + var initReceipt *types.Receipt + // prepare and include initiating message on chain B + { + initTx = txintent.NewIntent[*txintent.InitTrigger, *txintent.InteropOutput](bob.Plan()) + initTx.Content.Set(initTrigger) + var err error + initReceipt, err = initTx.PlannedTx.Included.Eval(ctx) + require.NoError(t, err) + + l.Info("initiating message included in chain B", "chain", sys.L2ChainB.ChainID(), "block_number", initReceipt.BlockNumber, "block_hash", initReceipt.BlockHash, "now", time.Now().Unix()) + } + + // at least one block between the init tx on chain B and the exec tx on chain A + sys.L2ChainA.WaitForBlock() + + // stop sequencer on chain A so that we later force include an invalid exec msg + latestUnsafe_A := sys.L2CLA.StopSequencer() + + var execTx *txintent.IntentTx[*txintent.ExecTrigger, *txintent.InteropOutput] + var execSignedTx *types.Transaction + var execTxEncoded []byte + // prepare and include invalid executing message on chain B via the op-test-sequencer (no other way to force-include an invalid message) + { + execTx = txintent.NewIntent[*txintent.ExecTrigger, *txintent.InteropOutput](alice.Plan()) + execTx.Content.DependOn(&initTx.Result) + // single event in tx so index is 0. + index := 0 + // lambda to transform InteropOutput to a new broken ExecTrigger + execTx.Content.Fn(func(ctx context.Context) (*txintent.ExecTrigger, error) { + events := initTx.Result.Value() + if x := len(events.Entries); x <= index { + return nil, fmt.Errorf("invalid index: %d, only have %d events", index, x) + } + msg := events.Entries[index] + // modify the message in order to make it invalid + txModifierFn(&msg) + return &txintent.ExecTrigger{ + Executor: constants.CrossL2Inbox, + Msg: msg, + }, nil + }) + + var err error + execSignedTx, err = execTx.PlannedTx.Signed.Eval(ctx) + require.NoError(t, err) + + l.Info("executing message signed", "to", execSignedTx.To(), "nonce", execSignedTx.Nonce(), "data", len(execSignedTx.Data())) + + execTxEncoded, err = execSignedTx.MarshalBinary() + require.NoError(t, err, "Expected to be able to marshal a signed transaction on op-test-sequencer, but got error") + } + + // sequence a new block with an invalid executing msg on chain A + { + l.Info("Building chain A with op-test-sequencer, and include invalid exec msg", "chain", sys.L2ChainA.ChainID(), "unsafeHead", latestUnsafe_A) + + err := ia.New(ctx, seqtypes.BuildOpts{ + Parent: latestUnsafe_A, + L1Origin: nil, + }) + require.NoError(t, err, "Expected to be able to create a new block job for sequencing on op-test-sequencer, but got error") + + // include invalid executing msg in opened block + err = ia.IncludeTx(ctx, execTxEncoded) + require.NoError(t, err, "Expected to be able to include a signed transaction on op-test-sequencer, but got error") + + err = ia.Next(ctx) + require.NoError(t, err, "Expected to be able to call Next() after New() on op-test-sequencer, but got error") + } + + // record divergence block numbers and original refs for future validation checks + var divergenceBlockNumber_A uint64 + var originalHash_A common.Hash + var originalParentHash_A common.Hash + // sequence a second block with op-test-sequencer + { + currentUnsafeRef := sys.L2ELA.BlockRefByLabel(eth.Unsafe) + + l.Info("Unsafe head after invalid exec msg has been included in chain A", "chain", sys.L2ChainA.ChainID(), "unsafeHead", currentUnsafeRef, "parent", currentUnsafeRef.ParentID()) + + divergenceBlockNumber_A = currentUnsafeRef.Number + originalHash_A = currentUnsafeRef.Hash + originalParentHash_A = currentUnsafeRef.ParentHash + l.Info("Continue building chain A with another block with op-test-sequencer", "chain", sys.L2ChainA.ChainID(), "unsafeHead", currentUnsafeRef, "parent", currentUnsafeRef.ParentID()) + err := ia.New(ctx, seqtypes.BuildOpts{ + Parent: currentUnsafeRef.Hash, + L1Origin: nil, + }) + require.NoError(t, err, "Expected to be able to create a new block job for sequencing on op-test-sequencer, but got error") + time.Sleep(2 * time.Second) + + // include simple transfer tx in opened block + { + to := cathrine.PlanTransfer(alice.Address(), eth.OneGWei) + opt := txplan.Combine(to) + ptx := txplan.NewPlannedTx(opt) + signed_tx, err := ptx.Signed.Eval(ctx) + require.NoError(t, err, "Expected to be able to evaluate a planned transaction on op-test-sequencer, but got error") + txdata, err := signed_tx.MarshalBinary() + require.NoError(t, err, "Expected to be able to marshal a signed transaction on op-test-sequencer, but got error") + + err = ia.IncludeTx(ctx, txdata) + require.NoError(t, err, "Expected to be able to include a signed transaction on op-test-sequencer, but got error") + } + + err = ia.Next(ctx) + require.NoError(t, err, "Expected to be able to call Next() after New() on op-test-sequencer, but got error") + time.Sleep(2 * time.Second) + } + + // continue sequencing with op-node + sys.L2CLA.StartSequencer() + + // start batcher on chain A + sys.L2BatcherA.Start() + + // wait for reorg on chain A + sys.L2ELA.ReorgTriggered(eth.L2BlockRef{ + Number: divergenceBlockNumber_A, + Hash: originalHash_A, + ParentHash: originalParentHash_A, + }, 30) + + err := wait.For(ctx, 5*time.Second, func() (bool, error) { + safeL2Head_supervisor_A := sys.Supervisor.SafeBlockID(sys.L2ChainA.ChainID()).Hash + safeL2Head_sequencer_A := sys.L2CLA.SafeL2BlockRef() + + if safeL2Head_sequencer_A.Number < divergenceBlockNumber_A { + l.Info("Safe ref number is still behind divergence block A number", "divergence", divergenceBlockNumber_A, "safe", safeL2Head_sequencer_A.Number) + return false, nil + } + + if safeL2Head_sequencer_A.Hash.Cmp(safeL2Head_supervisor_A) != 0 { + l.Info("Safe ref still not the same on supervisor and sequencer A", "supervisor", safeL2Head_supervisor_A, "sequencer", safeL2Head_sequencer_A.Hash) + return false, nil + } + + l.Info("Safe ref the same across supervisor and sequencers", + "supervisor_A", safeL2Head_supervisor_A, + "sequencer_A", safeL2Head_sequencer_A.Hash) + + return true, nil + }) + require.NoError(t, err, "Expected to get same safe ref on both supervisor and sequencer eventually") +} diff --git a/tests/supervisor/l2reorg/unsafe_head_test.go b/tests/supervisor/l2reorg/unsafe_head_test.go new file mode 100644 index 0000000000..a6c1411952 --- /dev/null +++ b/tests/supervisor/l2reorg/unsafe_head_test.go @@ -0,0 +1,134 @@ +package l2reorg + +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-e2e/e2eutils/wait" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/txplan" + "github.com/ethereum-optimism/optimism/op-test-sequencer/sequencer/seqtypes" + "github.com/stretchr/testify/require" +) + +// TestReorgUnsafeHead starts an interop chain with an op-test-sequencer, which takes control over sequencing the L2 chain and introduces a reorg on the unsafe head +// Acceptance Test: https://github.com/ethereum-optimism/optimism/blob/develop/op-acceptance-tests/tests/interop/reorgs/unsafe_head_test.go#L17 +func TestReorgUnsafeHead(gt *testing.T) { + t := devtest.SerialT(gt) + ctx := t.Ctx() + + sys := presets.NewSimpleInterop(t) + l := sys.Log + + ia := sys.TestSequencer.Escape().ControlAPI(sys.L2ChainA.ChainID()) + + // stop batcher on chain A + sys.L2BatcherA.Stop() + + // two EOAs for a sample transfer tx used later in a conflicting block + alice := sys.FunderA.NewFundedEOA(eth.OneHundredthEther) + bob := sys.Wallet.NewEOA(sys.L2ELA) + + sys.L1Network.WaitForBlock() + + sys.L2ChainA.WaitForBlock() + // waiting for two blocks in order to make sure we are not jumping ahead of a L1 origin (i.e. can't build a chain with L1Origin gaps) + sys.L2ChainA.WaitForBlock() + sys.L2ChainA.WaitForBlock() + + unsafeHead := sys.L2CLA.StopSequencer() + + var divergenceBlockNumber_A uint64 + var originalRef_A eth.L2BlockRef + // prepare and sequence a conflicting block for the L2A chain + { + unsafeHeadRef := sys.L2ELA.BlockRefByLabel(eth.Unsafe) + + l.Info("Current unsafe ref", "unsafeHead", unsafeHead, "parent", unsafeHeadRef.ParentID().Hash, "l1_origin", unsafeHeadRef.L1Origin) + + l.Info("Expect to reorg the chain on current unsafe block", "number", unsafeHeadRef.Number, "head", unsafeHead, "parent", unsafeHeadRef.ParentID().Hash) + divergenceBlockNumber_A = unsafeHeadRef.Number + originalRef_A = unsafeHeadRef + + parentOfUnsafeHead := unsafeHeadRef.ParentID() + + l.Info("Sequencing a conflicting block", "unsafeHead", unsafeHeadRef, "parent", parentOfUnsafeHead) + + // sequence a conflicting block with a simple transfer tx, based on the parent of the parent of the unsafe head + { + err := ia.New(ctx, seqtypes.BuildOpts{ + Parent: parentOfUnsafeHead.Hash, + L1Origin: nil, + }) + require.NoError(t, err, "Expected to be able to create a new block job for sequencing on op-test-sequencer, but got error") + + // include simple transfer tx in opened block + { + to := alice.PlanTransfer(bob.Address(), eth.OneGWei) + opt := txplan.Combine(to) + ptx := txplan.NewPlannedTx(opt) + signed_tx, err := ptx.Signed.Eval(ctx) + require.NoError(t, err, "Expected to be able to evaluate a planned transaction on op-test-sequencer, but got error") + txdata, err := signed_tx.MarshalBinary() + require.NoError(t, err, "Expected to be able to marshal a signed transaction on op-test-sequencer, but got error") + + err = ia.IncludeTx(ctx, txdata) + require.NoError(t, err, "Expected to be able to include a signed transaction on op-test-sequencer, but got error") + } + + err = ia.Next(ctx) + require.NoError(t, err, "Expected to be able to call Next() after New() on op-test-sequencer, but got error") + } + } + + // start batcher on chain A + sys.L2BatcherA.Start() + + // sequence a second block with op-test-sequencer (no L1 origin override) + { + l.Info("Sequencing with op-test-sequencer (no L1 origin override)") + err := ia.New(ctx, seqtypes.BuildOpts{ + Parent: sys.L2ELA.BlockRefByLabel(eth.Unsafe).Hash, + L1Origin: nil, + }) + require.NoError(t, err, "Expected to be able to create a new block job for sequencing on op-test-sequencer, but got error") + time.Sleep(2 * time.Second) + + err = ia.Next(ctx) + require.NoError(t, err, "Expected to be able to call Next() after New() on op-test-sequencer, but got error") + time.Sleep(2 * time.Second) + } + + // continue sequencing with consensus node (op-node) + sys.L2CLA.StartSequencer() + + sys.L2ChainA.WaitForBlock() + + reorgedRef_A, err := sys.L2ELA.Escape().EthClient().BlockRefByNumber(ctx, divergenceBlockNumber_A) + require.NoError(t, err, "Expected to be able to call BlockRefByNumber API, but got error") + + l.Info("Reorged chain A on divergence block number (prior the reorg)", "number", divergenceBlockNumber_A, "head", originalRef_A.Hash, "parent", originalRef_A.ParentID().Hash) + l.Info("Reorged chain A on divergence block number (after the reorg)", "number", divergenceBlockNumber_A, "head", reorgedRef_A.Hash, "parent", reorgedRef_A.ParentID().Hash) + require.NotEqual(t, originalRef_A.Hash, reorgedRef_A.Hash, "Expected to get different heads on divergence block number, but got the same hash, so no reorg happened on chain A") + require.Equal(t, originalRef_A.ParentID().Hash, reorgedRef_A.ParentHash, "Expected to get same parent hashes on divergence block number, but got different hashes") + + err = wait.For(ctx, 5*time.Second, func() (bool, error) { + safeL2Head_A_supervisor := sys.Supervisor.SafeBlockID(sys.L2ChainA.ChainID()).Hash + safeL2Head_A_sequencer := sys.L2CLA.SafeL2BlockRef() + + if safeL2Head_A_sequencer.Number <= divergenceBlockNumber_A { + l.Info("Safe ref number is still behind divergence block number", "divergence", divergenceBlockNumber_A, "safe", safeL2Head_A_sequencer.Number) + return false, nil + } + if safeL2Head_A_sequencer.Hash.Cmp(safeL2Head_A_supervisor) != 0 { + l.Info("Safe ref still not the same on supervisor and sequencer", "supervisor", safeL2Head_A_supervisor, "sequencer", safeL2Head_A_sequencer.Hash) + return false, nil + } + l.Info("Safe ref is the same on both supervisor and sequencer", "supervisor", safeL2Head_A_supervisor, "sequencer", safeL2Head_A_sequencer.Hash) + + return true, nil + }) + require.NoError(t, err, "Expected to get same safe ref on both supervisor and sequencer eventually") +}