diff --git a/.github/workflows/node_e2e_sysgo_tests.yaml b/.github/workflows/node_e2e_sysgo_tests.yaml index 451de5ab93..cdb3d9e3a9 100644 --- a/.github/workflows/node_e2e_sysgo_tests.yaml +++ b/.github/workflows/node_e2e_sysgo_tests.yaml @@ -40,13 +40,13 @@ jobs: cache-dependency-path: "**/go.sum" - name: common tests for node with sysgo orchestrator - run: just test-e2e-sysgo node/common + run: just test-e2e-sysgo node node/common - name: restart tests for node with sysgo orchestrator - run: just test-e2e-sysgo node/restart + run: just test-e2e-sysgo node node/restart - name: l2 reorg tests for node with sysgo orchestrator - run: just test-e2e-sysgo node/l2_reorg + run: just test-e2e-sysgo node node/l2_reorg diff --git a/.github/workflows/supervisor_e2e_kurtosis.yaml b/.github/workflows/supervisor_e2e_kurtosis.yaml index 3a341f4dea..2a73047c50 100644 --- a/.github/workflows/supervisor_e2e_kurtosis.yaml +++ b/.github/workflows/supervisor_e2e_kurtosis.yaml @@ -19,7 +19,7 @@ jobs: - devnet-config: simple-supervisor test-pkg: rpc - devnet-config: simple-supervisor - test-pkg: l1reorg + test-pkg: l1reorg/kurtosis - devnet-config: simple-supervisor test-pkg: l2reorg - devnet-config: preinterop-supervisor diff --git a/.github/workflows/supervisor_e2e_sysgo.yaml b/.github/workflows/supervisor_e2e_sysgo.yaml new file mode 100644 index 0000000000..f0dbc0ae55 --- /dev/null +++ b/.github/workflows/supervisor_e2e_sysgo.yaml @@ -0,0 +1,42 @@ +name: Sysgo E2E Tests Supervisor +on: + workflow_dispatch: +env: + CARGO_TERM_COLOR: always +jobs: + supervisor-e2e-tests: + runs-on: ubuntu-latest + timeout-minutes: 40 + name: ${{ matrix.test-pkg }}-tests + strategy: + fail-fast: false + matrix: + test-pkg: ["pre_interop", "l1_reorg/sysgo"] + steps: + - name: Checkout sources + uses: actions/checkout@v5 + with: + submodules: true + + - uses: ./.github/actions/setup + with: + channel: stable + + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@main + with: + large-packages: false + + - uses: taiki-e/install-action@just + + - uses: jdx/mise-action@v3 # installs Mise + runs `mise install` + + - name: Setup Go 1.24.3 + uses: actions/setup-go@v5 + with: + # Semantic version range syntax or exact version of Go + go-version: '1.24.3' + cache-dependency-path: "**/go.sum" + + - name: test ${{ matrix.test-pkg }} + run: just test-e2e-sysgo supervisor "/supervisor/${{ matrix.test-pkg }}" diff --git a/justfile b/justfile index 5f2adfef23..22df6e4c36 100644 --- a/justfile +++ b/justfile @@ -22,6 +22,10 @@ default: build-node: cargo build --release --bin kona-node +# Build the supervisor +build-supervisor: + cargo build --release --bin kona-supervisor + # Run all tests (excluding online tests) tests: test test-docs diff --git a/tests/justfile b/tests/justfile index 065a649034..4fcd28afde 100644 --- a/tests/justfile +++ b/tests/justfile @@ -43,7 +43,7 @@ devnet DEVNET OP_PACKAGE_PATH="" CUSTOM_DEVNET_PATH="": cleanup-kurtosis: kurtosis clean -a -test-e2e-sysgo GO_PKG_NAME="" FILTER="" : unzip-contract-artifacts +test-e2e-sysgo BINARY GO_PKG_NAME="" FILTER="" : unzip-contract-artifacts #!/bin/bash if [ -z "{{GO_PKG_NAME}}" ]; then export GO_PKG_NAME="..." @@ -57,10 +57,20 @@ test-e2e-sysgo GO_PKG_NAME="" FILTER="" : unzip-contract-artifacts export OP_DEPLOYER_ARTIFACTS="{{SOURCE}}/artifacts" export DISABLE_OP_E2E_LEGACY=true - export KONA_NODE_EXEC_PATH="{{SOURCE}}/../target/release/kona-node" - export DEVSTACK_ORCHESTRATOR=sysgo - - cd {{SOURCE}}/.. && just build-node + + if [ "{{BINARY}}" = "node" ]; then + export KONA_NODE_EXEC_PATH="{{SOURCE}}/../target/release/kona-node" + export DEVSTACK_ORCHESTRATOR=sysgo + cd {{SOURCE}}/.. && just build-node + elif [ "{{BINARY}}" = "supervisor" ]; then + export DEVSTACK_SUPERVISOR_KIND=kona + export KONA_SUPERVISOR_EXEC_PATH="{{SOURCE}}/../target/release/kona-supervisor" + export DEVSTACK_ORCHESTRATOR=sysgo + cd {{SOURCE}}/.. && just build-supervisor + else + echo "Invalid BINARY specified. Must be either 'node' or 'supervisor'." + exit 1 + fi # Run the test with count=1 to avoid caching the test results. cd {{SOURCE}} && go test -count=1 -timeout 40m -v ./$GO_PKG_NAME $FILTER diff --git a/tests/supervisor/l1reorg/init_test.go b/tests/supervisor/l1reorg/kurtosis/init_test.go similarity index 100% rename from tests/supervisor/l1reorg/init_test.go rename to tests/supervisor/l1reorg/kurtosis/init_test.go diff --git a/tests/supervisor/l1reorg/reorg_test.go b/tests/supervisor/l1reorg/kurtosis/reorg_test.go similarity index 100% rename from tests/supervisor/l1reorg/reorg_test.go rename to tests/supervisor/l1reorg/kurtosis/reorg_test.go diff --git a/tests/supervisor/l1reorg/sysgo/init_test.go b/tests/supervisor/l1reorg/sysgo/init_test.go new file mode 100644 index 0000000000..0c2e38ec59 --- /dev/null +++ b/tests/supervisor/l1reorg/sysgo/init_test.go @@ -0,0 +1,14 @@ +package sysgo + +import ( + "testing" + + "github.com/ethereum-optimism/optimism/op-devstack/presets" + spresets "github.com/op-rs/kona/supervisor/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, spresets.WithSimpleInteropMinimal()) +} diff --git a/tests/supervisor/l1reorg/sysgo/reorg_test.go b/tests/supervisor/l1reorg/sysgo/reorg_test.go new file mode 100644 index 0000000000..7a99149d33 --- /dev/null +++ b/tests/supervisor/l1reorg/sysgo/reorg_test.go @@ -0,0 +1,165 @@ +package sysgo + +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/apis" + "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" + "github.com/stretchr/testify/require" +) + +type checksFunc func(t devtest.T, sys *presets.SimpleInterop) + +func TestL2ReorgAfterL1Reorg(gt *testing.T) { + gt.Run("unsafe reorg", func(gt *testing.T) { + var crossSafeRef, localSafeRef, unsafeRef eth.BlockID + pre := func(t devtest.T, sys *presets.SimpleInterop) { + ss := sys.Supervisor.FetchSyncStatus() + crossSafeRef = ss.Chains[sys.L2ChainA.ChainID()].CrossSafe + localSafeRef = ss.Chains[sys.L2ChainA.ChainID()].LocalSafe + unsafeRef = ss.Chains[sys.L2ChainA.ChainID()].LocalUnsafe.ID() + } + post := func(t devtest.T, sys *presets.SimpleInterop) { + require.True(t, sys.L2ELA.IsCanonical(crossSafeRef), "Previous cross-safe block should still be canonical") + require.True(t, sys.L2ELA.IsCanonical(localSafeRef), "Previous local-safe block should still be canonical") + require.False(t, sys.L2ELA.IsCanonical(unsafeRef), "Previous unsafe block should have been reorged") + } + testL2ReorgAfterL1Reorg(gt, 3, pre, post) + }) + + gt.Run("unsafe, local-safe, cross-unsafe, cross-safe reorgs", func(gt *testing.T) { + var crossSafeRef, crossUnsafeRef, localSafeRef, unsafeRef eth.BlockID + pre := func(t devtest.T, sys *presets.SimpleInterop) { + ss := sys.Supervisor.FetchSyncStatus() + crossUnsafeRef = ss.Chains[sys.L2ChainA.ChainID()].CrossUnsafe + crossSafeRef = ss.Chains[sys.L2ChainA.ChainID()].CrossSafe + localSafeRef = ss.Chains[sys.L2ChainA.ChainID()].LocalSafe + unsafeRef = ss.Chains[sys.L2ChainA.ChainID()].LocalUnsafe.ID() + } + post := func(t devtest.T, sys *presets.SimpleInterop) { + require.False(t, sys.L2ELA.IsCanonical(crossSafeRef), "Previous cross-safe block should have been reorged") + require.False(t, sys.L2ELA.IsCanonical(crossUnsafeRef), "Previous cross-unsafe block should have been reorged") + require.False(t, sys.L2ELA.IsCanonical(localSafeRef), "Previous local-safe block should have been reorged") + require.False(t, sys.L2ELA.IsCanonical(unsafeRef), "Previous unsafe block should have been reorged") + } + testL2ReorgAfterL1Reorg(gt, 10, pre, post) + }) +} + +// testL2ReorgAfterL1Reorg tests that the L2 chain reorgs after an L1 reorg, and takes n, number of blocks to reorg, as parameter +// for unsafe reorgs - n must be at least >= confDepth, which is 2 in our test deployments +// for cross-safe reorgs - n must be at least >= safe distance, which is 10 in our test deployments (set in +// op-e2e/e2eutils/geth/geth.go when initialising FakePoS) +// pre- and post-checks are sanity checks to ensure that the blocks we expected to be reorged were indeed reorged or not +func testL2ReorgAfterL1Reorg(gt *testing.T, n int, preChecks, postChecks checksFunc) { + t := devtest.SerialT(gt) + ctx := t.Ctx() + + sys := presets.NewSimpleInterop(t) + ts := sys.TestSequencer.Escape().ControlAPI(sys.L1Network.ChainID()) + + cl := sys.L1Network.Escape().L1CLNode(match.FirstL1CL) + + sys.L1Network.WaitForBlock() + + sys.ControlPlane.FakePoSState(cl.ID(), stack.Stop) + + // sequence a few L1 and L2 blocks + for range n + 1 { + sequenceL1Block(t, ts, common.Hash{}) + + sys.L2ChainA.WaitForBlock() + sys.L2ChainA.WaitForBlock() + } + + // select a divergence block to reorg from + var divergence eth.L1BlockRef + { + tip := sys.L1EL.BlockRefByLabel(eth.Unsafe) + require.Greater(t, tip.Number, uint64(n), "n is larger than L1 tip, cannot reorg out block number `tip-n`") + + divergence = sys.L1EL.BlockRefByNumber(tip.Number - uint64(n)) + } + + // print the chains before sequencing an alternative L1 block + sys.L2ChainA.PrintChain() + sys.L1Network.PrintChain() + + // pre reorg trigger validations and checks + preChecks(t, sys) + + tipL2_preReorg := sys.L2ELA.BlockRefByLabel(eth.Unsafe) + + // reorg the L1 chain -- sequence an alternative L1 block from divergence block parent + sequenceL1Block(t, ts, divergence.ParentHash) + + // continue building on the alternative L1 chain + sys.ControlPlane.FakePoSState(cl.ID(), stack.Start) + + // confirm L1 reorged + sys.L1EL.ReorgTriggered(divergence, 5) + + // wait until L2 chain A cross-safe ref caught up to where it was before the reorg + sys.L2CLA.Reached(types.CrossSafe, tipL2_preReorg.Number, 50) + + // test that latest chain A unsafe is not referencing a reorged L1 block (through the L1Origin field) + require.Eventually(t, func() bool { + unsafe := sys.L2ELA.BlockRefByLabel(eth.Unsafe) + + block, err := sys.L1EL.Escape().EthClient().InfoByNumber(ctx, unsafe.L1Origin.Number) + if err != nil { + sys.Log.Warn("failed to get L1 block info by number", "number", unsafe.L1Origin.Number, "err", err) + return false + } + + sys.Log.Info("current unsafe ref", "tip", unsafe, "tip_origin", unsafe.L1Origin, "l1blk", eth.InfoToL1BlockRef(block)) + + // print the chains so we have information to debug if the test fails + sys.L2ChainA.PrintChain() + sys.L1Network.PrintChain() + + return block.Hash() == unsafe.L1Origin.Hash + }, 120*time.Second, 7*time.Second, "L1 block origin hash should match hash of block on L1 at that number. If not, it means there was a reorg, and L2 blocks L1Origin field is referencing a reorged block.") + + // confirm all L1Origin fields point to canonical blocks + require.Eventually(t, func() bool { + ref := sys.L2ELA.BlockRefByLabel(eth.Unsafe) + var err error + + // wait until L2 chains' L1Origin points to a L1 block after the one that was reorged + if ref.L1Origin.Number < divergence.Number { + return false + } + + sys.Log.Info("L2 chain progressed, pointing to newer L1 block", "ref", ref, "ref_origin", ref.L1Origin, "divergence", divergence) + + for i := ref.Number; i > 0 && ref.L1Origin.Number >= divergence.Number; i-- { + ref, err = sys.L2ELA.Escape().L2EthClient().L2BlockRefByNumber(ctx, i) + if err != nil { + return false + } + + if !sys.L1EL.IsCanonical(ref.L1Origin) { + return false + } + } + + return true + }, 120*time.Second, 5*time.Second, "all L1Origin fields should point to canonical L1 blocks") + + // post reorg test validations and checks + postChecks(t, sys) +} + +func sequenceL1Block(t devtest.T, ts apis.TestSequencerControlAPI, parent common.Hash) { + require.NoError(t, ts.New(t.Ctx(), seqtypes.BuildOpts{Parent: parent})) + require.NoError(t, ts.Next(t.Ctx())) +} diff --git a/tests/supervisor/pre_interop/init_test.go b/tests/supervisor/pre_interop/init_test.go index 3ddb3a5798..f91c9fafea 100644 --- a/tests/supervisor/pre_interop/init_test.go +++ b/tests/supervisor/pre_interop/init_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/ethereum-optimism/optimism/op-devstack/presets" + spresets "github.com/op-rs/kona/supervisor/presets" ) // TestMain creates the test-setups against the shared backend @@ -12,6 +13,8 @@ func TestMain(m *testing.M) { // sleep to ensure the backend is ready presets.DoMain(m, - presets.WithSimpleInterop(), + spresets.WithSimpleInteropMinimal(), + presets.WithSuggestedInteropActivationOffset(30), presets.WithInteropNotAtGenesis()) + } diff --git a/tests/supervisor/pre_interop/pre_test.go b/tests/supervisor/pre_interop/pre_test.go index 188bfeef60..fdf3a25a95 100644 --- a/tests/supervisor/pre_interop/pre_test.go +++ b/tests/supervisor/pre_interop/pre_test.go @@ -104,7 +104,6 @@ func testPreInteropCheckAccessList(t devtest.T, sys *presets.SimpleInterop) { // Acceptance Test: https://github.com/ethereum-optimism/optimism/blob/develop/op-acceptance-tests/tests/interop/upgrade/pre_test.go func TestPreNoInbox(gt *testing.T) { - gt.Skip("This test requires op_contract_deployer_params setup in the kurtosis network, which is not available in the devnet setup.") t := devtest.ParallelT(gt) sys := presets.NewSimpleInterop(t) require := t.Require() @@ -131,7 +130,7 @@ func TestPreNoInbox(gt *testing.T) { interopTime := net.Escape().ChainConfig().InteropTime _, err := sys.Supervisor.Escape().QueryAPI().SyncStatus(t.Ctx()) - require.ErrorContains(err, "supervisor status tracker not ready") + require.ErrorContains(err, "chain database is not initialized") // confirm we are still pre-interop require.False(net.IsActivated(*interopTime)) diff --git a/tests/supervisor/presets/interop_minimal.go b/tests/supervisor/presets/interop_minimal.go new file mode 100644 index 0000000000..64d9c77e98 --- /dev/null +++ b/tests/supervisor/presets/interop_minimal.go @@ -0,0 +1,98 @@ +package presets + +import ( + "os" + "path/filepath" + + "github.com/ethereum-optimism/optimism/op-chain-ops/devkeys" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/artifacts" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-devstack/sysgo" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/intentbuilder" +) + +// WithSimpleInteropMinimal specifies a system that meets the SimpleInterop criteria removing the Challenger. +func WithSimpleInteropMinimal() stack.CommonOption { + return stack.MakeCommon(DefaultMinimalInteropSystem(&sysgo.DefaultInteropSystemIDs{})) +} + +func DefaultMinimalInteropSystem(dest *sysgo.DefaultInteropSystemIDs) stack.Option[*sysgo.Orchestrator] { + ids := sysgo.NewDefaultInteropSystemIDs(sysgo.DefaultL1ID, sysgo.DefaultL2AID, sysgo.DefaultL2BID) + opt := stack.Combine[*sysgo.Orchestrator]() + + // start with single chain interop system + opt.Add(baseInteropSystem(&ids.DefaultSingleChainInteropSystemIDs)) + + opt.Add(sysgo.WithDeployerOptions( + sysgo.WithPrefundedL2(ids.L1.ChainID(), ids.L2B.ChainID()), + sysgo.WithInteropAtGenesis(), // this can be overridden by later options + )) + opt.Add(sysgo.WithL2ELNode(ids.L2BEL, sysgo.L2ELWithSupervisor(ids.Supervisor))) + opt.Add(sysgo.WithL2CLNode(ids.L2BCL, ids.L1CL, ids.L1EL, ids.L2BEL, sysgo.L2CLSequencer(), sysgo.L2CLIndexing())) + opt.Add(sysgo.WithBatcher(ids.L2BBatcher, ids.L1EL, ids.L2BCL, ids.L2BEL)) + + opt.Add(sysgo.WithManagedBySupervisor(ids.L2BCL, ids.Supervisor)) + + // Note: we provide L2 CL nodes still, even though they are not used post-interop. + // Since we may create an interop infra-setup, before interop is even scheduled to run. + opt.Add(sysgo.WithProposer(ids.L2BProposer, ids.L1EL, &ids.L2BCL, &ids.Supervisor)) + + opt.Add(sysgo.WithFaucets([]stack.L1ELNodeID{ids.L1EL}, []stack.L2ELNodeID{ids.L2AEL, ids.L2BEL})) + + // Upon evaluation of the option, export the contents we created. + // Ids here are static, but other things may be exported too. + opt.Add(stack.Finally(func(orch *sysgo.Orchestrator) { + *dest = ids + })) + + return opt +} + +// baseInteropSystem defines a system that supports interop with a single chain +// Components which are shared across multiple chains are not started, allowing them to be added later including +// any additional chains that have been added. +func baseInteropSystem(ids *sysgo.DefaultSingleChainInteropSystemIDs) stack.Option[*sysgo.Orchestrator] { + opt := stack.Combine[*sysgo.Orchestrator]() + opt.Add(stack.BeforeDeploy(func(o *sysgo.Orchestrator) { + o.P().Logger().Info("Setting up") + })) + + opt.Add(sysgo.WithMnemonicKeys(devkeys.TestMnemonic)) + + // Get artifacts path + artifactsPath := os.Getenv("OP_DEPLOYER_ARTIFACTS") + if artifactsPath == "" { + panic("OP_DEPLOYER_ARTIFACTS is not set") + } + + opt.Add(sysgo.WithDeployer(), + sysgo.WithDeployerPipelineOption( + sysgo.WithDeployerCacheDir(artifactsPath), + ), + sysgo.WithDeployerOptions( + func(_ devtest.P, _ devkeys.Keys, builder intentbuilder.Builder) { + builder.WithL1ContractsLocator(artifacts.MustNewFileLocator(filepath.Join(artifactsPath, "src"))) + builder.WithL2ContractsLocator(artifacts.MustNewFileLocator(filepath.Join(artifactsPath, "src"))) + }, + sysgo.WithCommons(ids.L1.ChainID()), + sysgo.WithPrefundedL2(ids.L1.ChainID(), ids.L2A.ChainID()), + ), + ) + + opt.Add(sysgo.WithL1Nodes(ids.L1EL, ids.L1CL)) + + opt.Add(sysgo.WithSupervisor(ids.Supervisor, ids.Cluster, ids.L1EL)) + + opt.Add(sysgo.WithL2ELNode(ids.L2AEL, sysgo.L2ELWithSupervisor(ids.Supervisor))) + opt.Add(sysgo.WithL2CLNode(ids.L2ACL, ids.L1CL, ids.L1EL, ids.L2AEL, sysgo.L2CLSequencer(), sysgo.L2CLIndexing())) + opt.Add(sysgo.WithTestSequencer(ids.TestSequencer, ids.L1CL, ids.L2ACL, ids.L1EL, ids.L2AEL)) + opt.Add(sysgo.WithBatcher(ids.L2ABatcher, ids.L1EL, ids.L2ACL, ids.L2AEL)) + + opt.Add(sysgo.WithManagedBySupervisor(ids.L2ACL, ids.Supervisor)) + + // Note: we provide L2 CL nodes still, even though they are not used post-interop. + // Since we may create an interop infra-setup, before interop is even scheduled to run. + opt.Add(sysgo.WithProposer(ids.L2AProposer, ids.L1EL, &ids.L2ACL, &ids.Supervisor)) + return opt +}