diff --git a/.circleci/config.yml b/.circleci/config.yml index ba7e2a76491..3f3e01483b8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -160,7 +160,6 @@ commands: pip3 install -r requirements.txt python3 main.py "<>" - install-solc-compilers: description: "Install the solc compilers" parameters: @@ -245,33 +244,46 @@ commands: echo "Resolved TARGET_BRANCH=$TARGET_BRANCH" echo "export TARGET_BRANCH=$TARGET_BRANCH" >> "$BASH_ENV" - setup-dev-features: - description: "Set up dev feature environment variables from comma-separated list" + setup-features: + description: "Set up dev and system feature environment variables. Features are auto-classified based on system_features registry." parameters: - dev_features: - description: "Comma-separated list of dev features to enable" + features: + description: "Comma-separated list of features (can mix dev and system features, e.g., 'OPTIMISM_PORTAL_INTEROP,CUSTOM_GAS_TOKEN')" type: string default: "" + system_features: + description: "Registry of system features (others treated as dev features)" + type: string + default: "CUSTOM_GAS_TOKEN" steps: - run: - name: Set dev feature environment variables + name: Set feature environment variables command: | - # Set dev feature environment variables if provided - if [ -n "<>" ]; then - DEV_FEATURES_STRING="<>" + # Define which features are system features (registry) + SYSTEM_FEATURES="<>" - # Check if this is just "main" (baseline with no dev features) - if [ "$(echo "$DEV_FEATURES_STRING" | tr '[:upper:]' '[:lower:]')" = "main" ]; then - echo "Running with baseline configuration (no dev features enabled)" + if [ -n "<>" ]; then + FEATURES_STRING="<>" + + # Check if this is just "main" (baseline with no features) + if [ "$(echo "$FEATURES_STRING" | tr '[:upper:]' '[:lower:]')" = "main" ]; then + echo "Running with baseline configuration (no features enabled)" else - echo "Enabling dev features: <>" + echo "Processing features: <>" IFS=',' - for feature in $DEV_FEATURES_STRING; do + for feature in $FEATURES_STRING; do feature=$(echo "$feature" | xargs) # trim whitespace if [ -n "$feature" ] && [ "$(echo "$feature" | tr '[:upper:]' '[:lower:]')" != "main" ]; then - env_var="DEV_FEATURE__${feature}" - echo "Setting ${env_var}=true" - echo "export ${env_var}=true" >> $BASH_ENV + # Check if this feature is in the system features registry + if echo "$SYSTEM_FEATURES" | grep -qw "$feature"; then + env_var="SYS_FEATURE__${feature}" + echo "Setting ${env_var}=true (system feature)" + echo "export ${env_var}=true" >> $BASH_ENV + else + env_var="DEV_FEATURE__${feature}" + echo "Setting ${env_var}=true (dev feature)" + echo "export ${env_var}=true" >> $BASH_ENV + fi fi done unset IFS @@ -492,7 +504,7 @@ jobs: default: false steps: - utils/checkout-with-mise: - checkout-method: blobless + checkout-method: blobless - attach_workspace: at: . - check-changed: @@ -544,7 +556,7 @@ jobs: resource_class: xlarge steps: - utils/checkout-with-mise: - checkout-method: blobless + checkout-method: blobless - run: name: Check `RISCV.sol` bytecode working_directory: packages/contracts-bedrock @@ -594,7 +606,7 @@ jobs: default: ci steps: - utils/checkout-with-mise: - checkout-method: blobless + checkout-method: blobless - install-zstd - install-contracts-dependencies - run: @@ -692,7 +704,7 @@ jobs: docker_layer_caching: true # we rely on this for faster builds, and actively warm it up for builds with common stages steps: - utils/checkout-with-mise: - checkout-method: blobless + checkout-method: blobless - run: command: mkdir -p /tmp/docker_images - when: @@ -913,13 +925,13 @@ jobs: description: List of changed files to run tests on type: string default: contracts-bedrock - dev_features: - description: Comma-separated list of dev features to enable (e.g., "OPTIMISM_PORTAL_INTEROP,ANOTHER_FEATURE") + features: + description: Comma-separated list of features to enable (e.g., "OPTIMISM_PORTAL_INTEROP", "CUSTOM_GAS_TOKEN") type: string default: "" steps: - utils/checkout-with-mise: - checkout-method: blobless + checkout-method: blobless - install-zstd - run: name: Check if test list is empty @@ -949,8 +961,8 @@ jobs: working_directory: packages/contracts-bedrock - go-save-cache: namespace: packages/contracts-bedrock/scripts/go-ffi - - setup-dev-features: - dev_features: <> + - setup-features: + features: <> - run: name: Run tests command: | @@ -983,7 +995,7 @@ jobs: resource_class: 2xlarge steps: - utils/checkout-with-mise: - checkout-method: blobless + checkout-method: blobless - install-contracts-dependencies - install-zstd - run: @@ -1032,7 +1044,7 @@ jobs: resource_class: medium steps: - utils/checkout-with-mise: - checkout-method: blobless + checkout-method: blobless - run: name: Check Python version command: python3 --version @@ -1086,13 +1098,13 @@ jobs: description: Profile to use for testing type: string default: ci - dev_features: - description: Comma-separated list of dev features to enable (e.g., "OPTIMISM_PORTAL_INTEROP,ANOTHER_FEATURE") + features: + description: Comma-separated list of features to enable (e.g., "OPTIMISM_PORTAL_INTEROP", "CUSTOM_GAS_TOKEN") type: string default: "" steps: - utils/checkout-with-mise: - checkout-method: blobless + checkout-method: blobless - install-contracts-dependencies - install-zstd - attach_workspace: @@ -1123,8 +1135,8 @@ jobs: - restore_cache: name: Restore forked state key: forked-state-contracts-bedrock-tests-upgrade-{{ checksum "packages/contracts-bedrock/pinnedBlockNumber.txt" }} - - setup-dev-features: - dev_features: <> + - setup-features: + features: <> - run: name: Build go-ffi command: just build-go-ffi @@ -1178,8 +1190,8 @@ jobs: fork_base_rpc: description: Fork Base RPC type: string - dev_features: - description: Comma-separated list of dev features to enable (e.g., "OPTIMISM_PORTAL_INTEROP,ANOTHER_FEATURE") + features: + description: Comma-separated list of features to enable (e.g., "OPTIMISM_PORTAL_INTEROP", "CUSTOM_GAS_TOKEN") type: string default: "" docker: @@ -1214,8 +1226,8 @@ jobs: - restore_cache: name: Restore forked state key: forked-state-contracts-bedrock-tests-upgrade-<>-<>-{{ checksum "packages/contracts-bedrock/pinnedBlockNumber.txt" }} - - setup-dev-features: - dev_features: <> + - setup-features: + features: <> - run: name: Run tests command: just test-upgrade @@ -1353,7 +1365,7 @@ jobs: resource_class: xlarge steps: - utils/checkout-with-mise: - checkout-method: blobless + checkout-method: blobless - check-changed: patterns: "<>" - attach_workspace: @@ -1385,7 +1397,7 @@ jobs: resource_class: large steps: - utils/checkout-with-mise: - checkout-method: blobless + checkout-method: blobless - restore_cache: key: golangci-v1-{{ checksum ".golangci.yaml" }} - run: @@ -1434,7 +1446,7 @@ jobs: parallelism: <> steps: - utils/checkout-with-mise: - checkout-method: blobless + checkout-method: blobless - attach_workspace: at: . - restore_cache: @@ -1509,7 +1521,7 @@ jobs: resource_class: <> steps: - utils/checkout-with-mise: - checkout-method: blobless + checkout-method: blobless - attach_workspace: at: . - run: @@ -1573,7 +1585,7 @@ jobs: circleci_ip_ranges: true steps: - utils/checkout-with-mise: - checkout-method: blobless + checkout-method: blobless # Restore cached Go modules - restore_cache: keys: @@ -1671,7 +1683,7 @@ jobs: resource_class: 2xlarge+ steps: - utils/checkout-with-mise: - checkout-method: blobless + checkout-method: blobless - attach_workspace: at: . # Restore cached Go modules @@ -1765,7 +1777,7 @@ jobs: parallelism: << pipeline.parameters.flake-shake-workers >> steps: - utils/checkout-with-mise: - checkout-method: blobless + checkout-method: blobless - restore_cache: keys: - go-mod-v1-{{ checksum "go.sum" }} @@ -1812,7 +1824,7 @@ jobs: resource_class: large steps: - utils/checkout-with-mise: - checkout-method: blobless + checkout-method: blobless - attach_workspace: at: . - run: @@ -1863,7 +1875,7 @@ jobs: resource_class: large steps: - utils/checkout-with-mise: - checkout-method: blobless + checkout-method: blobless - run: name: Lint/Vet/Build op-acceptance-tests/cmd working_directory: op-acceptance-tests @@ -1942,7 +1954,7 @@ jobs: resource_class: large steps: - utils/checkout-with-mise: - checkout-method: blobless + checkout-method: blobless - run: name: Install tools command: | @@ -1965,7 +1977,7 @@ jobs: resource_class: xlarge steps: - utils/checkout-with-mise: - checkout-method: blobless + checkout-method: blobless - restore_cache: name: Restore cannon prestate cache key: cannon-prestate-{{ checksum "./cannon/bin/cannon" }}-{{ checksum "op-program/bin/op-program-client.elf" }} @@ -1993,7 +2005,7 @@ jobs: - image: <> steps: - utils/checkout-with-mise: - checkout-method: blobless + checkout-method: blobless - setup_remote_docker - run: name: Build prestates @@ -2009,7 +2021,7 @@ jobs: docker_layer_caching: true # we rely on this for faster builds, and actively warm it up for builds with common stages steps: - utils/checkout-with-mise: - checkout-method: blobless + checkout-method: blobless - restore_cache: name: Restore kona cache key: kona-prestate-{{ checksum "./kona/justfile" }}-{{ checksum "./kona/version.json" }} @@ -2032,7 +2044,7 @@ jobs: - image: <> steps: - utils/checkout-with-mise: - checkout-method: blobless + checkout-method: blobless - restore_cache: name: Restore kona host cache key: kona-host-{{ checksum "./kona/justfile" }}-{{ checksum "./kona/version.json" }} @@ -2205,7 +2217,7 @@ jobs: resource_class: xlarge steps: - utils/checkout-with-mise: - checkout-method: blobless + checkout-method: blobless - setup_remote_docker - run: name: Run Analyzer @@ -2219,7 +2231,7 @@ jobs: resource_class: large steps: - utils/checkout-with-mise: - checkout-method: blobless + checkout-method: blobless - run: name: Verify Compatibility command: | @@ -2232,7 +2244,7 @@ jobs: resource_class: large steps: - utils/checkout-with-mise: - checkout-method: blobless + checkout-method: blobless - check-changed: patterns: op-node - run: @@ -2245,7 +2257,7 @@ jobs: resource_class: large steps: - utils/checkout-with-mise: - checkout-method: blobless + checkout-method: blobless - check-changed: patterns: op-service - run: @@ -2257,7 +2269,7 @@ jobs: - image: <> steps: - utils/checkout-with-mise: - checkout-method: blobless + checkout-method: blobless - run: command: just check-forge-version working_directory: op-deployer @@ -2361,7 +2373,7 @@ jobs: resource_class: medium steps: - utils/checkout-with-mise: - checkout-method: blobless + checkout-method: blobless - install-contracts-dependencies - run: name: Build contracts @@ -2552,52 +2564,51 @@ workflows: - circleci-repo-readonly-authenticated-github-token - contracts-bedrock-tests: # Heavily fuzz any fuzz tests within added or modified test files. - name: contracts-bedrock-tests-heavy-fuzz-modified <> + name: contracts-bedrock-tests-heavy-fuzz-modified <> test_list: git diff origin/develop...HEAD --name-only --diff-filter=AM -- './test/**/*.t.sol' | sed 's|packages/contracts-bedrock/||' test_timeout: 1h test_profile: ciheavy - dev_features: <> + features: <> matrix: parameters: - dev_features: &dev_features_matrix + features: &features_matrix - main - OPTIMISM_PORTAL_INTEROP - CANNON_KONA,DEPLOY_V2_DISPUTE_GAMES - - CUSTOM_GAS_TOKEN - OPCM_V2 - OPCM_V2,CUSTOM_GAS_TOKEN context: - circleci-repo-readonly-authenticated-github-token - contracts-bedrock-tests: - name: contracts-bedrock-tests <> + name: contracts-bedrock-tests <> test_list: find test -name "*.t.sol" - dev_features: <> + features: <> matrix: parameters: - dev_features: *dev_features_matrix + features: *features_matrix context: - circleci-repo-readonly-authenticated-github-token check_changed_patterns: contracts-bedrock,op-node - contracts-bedrock-coverage: # Generate coverage reports. - name: contracts-bedrock-coverage <> + name: contracts-bedrock-coverage <> test_timeout: 1h test_profile: cicoverage - dev_features: <> + features: <> matrix: parameters: - dev_features: *dev_features_matrix + features: *features_matrix context: - circleci-repo-readonly-authenticated-github-token - contracts-bedrock-tests-upgrade: - name: contracts-bedrock-tests-upgrade op-mainnet <> + name: contracts-bedrock-tests-upgrade op-mainnet <> fork_op_chain: op fork_base_chain: mainnet fork_base_rpc: https://ci-mainnet-l1-archive.optimism.io - dev_features: <> + features: <> matrix: parameters: - dev_features: *dev_features_matrix + features: *features_matrix context: - circleci-repo-readonly-authenticated-github-token - contracts-bedrock-tests-upgrade: diff --git a/Makefile b/Makefile index 6dd03ee1538..b5a7f61d890 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,7 @@ golang-docker: ## Builds Docker images for Go components using buildx GIT_COMMIT=$$(git rev-parse HEAD) \ GIT_DATE=$$(git show -s --format='%ct') \ IMAGE_TAGS=$$(git rev-parse HEAD),latest \ + KONA_VERSION=$$(jq -r .version kona/version.json) \ docker buildx bake \ --progress plain \ --load \ diff --git a/docker-bake.hcl b/docker-bake.hcl index d19ab640f31..7a96988365f 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -10,10 +10,6 @@ variable "KONA_VERSION" { default = "none" } -variable "ASTERISC_VERSION" { - default = "v1.3.0" -} - variable "GIT_COMMIT" { default = "dev" } @@ -149,7 +145,6 @@ target "op-challenger" { GIT_DATE = "${GIT_DATE}" OP_CHALLENGER_VERSION = "${OP_CHALLENGER_VERSION}" KONA_VERSION="${KONA_VERSION}" - ASTERISC_VERSION="${ASTERISC_VERSION}" } target = "op-challenger-target" platforms = split(",", PLATFORMS) diff --git a/op-acceptance-tests/tests/batcher/batcher_test.go b/op-acceptance-tests/tests/batcher/batcher_test.go new file mode 100644 index 00000000000..5cfc2b58036 --- /dev/null +++ b/op-acceptance-tests/tests/batcher/batcher_test.go @@ -0,0 +1,116 @@ +package batcher + +import ( + "testing" + "time" + + "github.com/davecgh/go-spew/spew" + "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" + "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-service/txplan" + "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" +) + +func TestBatcherFullChannelsAfterDowntime(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewSingleChainMultiNodeWithTestSeq(t) + l := t.Logger() + ts_L2 := sys.TestSequencer.Escape().ControlAPI(sys.L2EL.ChainID()) + + alice := sys.FunderL2.NewFundedEOA(eth.OneWei) + cathrine := sys.FunderL2.NewFundedEOA(eth.OneTenthEther) + + cl := sys.L1Network.Escape().L1CLNode(match.FirstL1CL) + + sys.ControlPlane.FakePoSState(cl.ID(), stack.Stop) + + latestUnsafe_A := sys.L2CL.StopSequencer() + l.Info("Latest unsafe block after stopping the L2 sequencer", "latestUnsafe", latestUnsafe_A) + + parent := latestUnsafe_A + nonce := uint64(0) + for j := 0; j < 200; j++ { + l1Origin := sys.L1EL.BlockRefByLabel(eth.Unsafe).Hash + + for i := 0; i < 5; i++ { + l.Debug("Sequencing L2 block", "iteration", i, "parent", parent) + sequenceBlockWithL1Origin(t, ts_L2, parent, l1Origin, alice, cathrine, nonce) + nonce++ + + parent = sys.L2CL.HeadBlockRef(types.LocalUnsafe).Hash + + sys.AdvanceTime(time.Second * 2) + time.Sleep(20 * time.Millisecond) // failed to force-include tx: type: 2 sender; err: nonce too high + } + + l.Debug("Sequencing L1 block", "iteration_j", j) + sys.TestSequencer.SequenceBlock(t, sys.L1Network.ChainID(), common.Hash{}) + } + + sys.L2CL.StartSequencer() + + l.Info("Current L1 unsafe block", "currentL1Unsafe", sys.L1EL.BlockRefByLabel(eth.Unsafe)) + sys.ControlPlane.FakePoSState(cl.ID(), stack.Start) + + sys.L2Batcher.Start() + + channels, channelFrames, l2Txs := sys.L2Chain.DeriveData(4) // over the next 4 blocks, collect batches/channels/frames submitted by the batcher on the L1 network, and parse them + { + for _, c := range channels { + l.Info("Channel details", "channelID", c.String(), "frameCount", len(channelFrames[c]), "dataLength_frame0", len(channelFrames[c][0].Data)) + } + + require.Equal(t, 2, len(channels)) // we expect a total of 2 channels + + // values are dependent on: + // - MaxPendingTransactions + // - number of blocks and transactions sent in the test - 1000 L2 blocks with 1 transaction from cathrine to alice + // - MaxL1TxSize (this is set to 40_000 bytes for test purposes) + sizeRanges := []struct { + min int + max int + note string + }{ + {min: 30_000, max: 40_000, note: "channel 0 - filled to the max capacity"}, + {min: 30_000, max: 40_000, note: "channel 1 - remaining data, filling channel close to max capacity"}, + } + + for i, entry := range sizeRanges { + require.LessOrEqual(t, len(channelFrames[channels[i]][0].Data), entry.max, entry.note) + require.GreaterOrEqual(t, len(channelFrames[channels[i]][0].Data), entry.min, entry.note) + } + + require.Equal(t, len(l2Txs[cathrine.Address()]), 1000) // we expect 1000 transactions total sent from cathrine to alice + } + + status := sys.L2CL.SyncStatus() + spew.Dump(status) +} + +func sequenceBlockWithL1Origin(t devtest.T, ts apis.TestSequencerControlAPI, parent common.Hash, l1Origin common.Hash, alice *dsl.EOA, cathrine *dsl.EOA, nonce uint64) { + require.NoError(t, ts.New(t.Ctx(), seqtypes.BuildOpts{Parent: parent, L1Origin: &l1Origin})) + + // include simple transfer tx in opened block + { + to := cathrine.PlanTransfer(alice.Address(), eth.OneWei) + opt := txplan.Combine(to, txplan.WithStaticNonce(nonce)) + ptx := txplan.NewPlannedTx(opt) + signed_tx, err := ptx.Signed.Eval(t.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 = ts.IncludeTx(t.Ctx(), txdata) + require.NoError(t, err, "Expected to be able to include a signed transaction on op-test-sequencer, but got error") + } + + require.NoError(t, ts.Next(t.Ctx())) +} diff --git a/op-acceptance-tests/tests/batcher/init_test.go b/op-acceptance-tests/tests/batcher/init_test.go new file mode 100644 index 00000000000..1a86b3b9589 --- /dev/null +++ b/op-acceptance-tests/tests/batcher/init_test.go @@ -0,0 +1,33 @@ +package batcher + +import ( + "testing" + "time" + + bss "github.com/ethereum-optimism/optimism/op-batcher/batcher" + "github.com/ethereum-optimism/optimism/op-devstack/compat" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-devstack/sysgo" +) + +func TestMain(m *testing.M) { + presets.DoMain(m, presets.WithSingleChainMultiNode(), + presets.WithExecutionLayerSyncOnVerifiers(), + presets.WithCompatibleTypes(compat.SysGo), + presets.WithNoDiscovery(), + presets.WithTimeTravel(), + stack.MakeCommon(sysgo.WithBatcherOption(func(id stack.L2BatcherID, cfg *bss.CLIConfig) { + cfg.Stopped = true + + // set the blob max size to 40_000 bytes for test purposes + cfg.MaxL1TxSize = 40_000 + cfg.TestUseMaxTxSizeForBlobs = true + + cfg.PollInterval = 1000 * time.Millisecond + + cfg.MaxChannelDuration = 50 + cfg.MaxPendingTransactions = 7 + })), + ) +} diff --git a/op-acceptance-tests/tests/flashblocks/flashblocks_stream_test.go b/op-acceptance-tests/tests/flashblocks/flashblocks_stream_test.go index cd08cb0e20c..323ffe51759 100644 --- a/op-acceptance-tests/tests/flashblocks/flashblocks_stream_test.go +++ b/op-acceptance-tests/tests/flashblocks/flashblocks_stream_test.go @@ -82,7 +82,7 @@ func TestFlashblocksStream(gt *testing.T) { defer close(builderOutput) builderDone := make(chan struct{}) go func() { - err := oprbuilderNode.FlashblocksClient().ListenFor(ctx, logger.With("stream_source", "op-rbuilder"), testDuration, builderOutput, builderDone) + err := oprbuilderNode.FlashblocksClient().ReadAll(ctx, logger.With("stream_source", "op-rbuilder"), testDuration, builderOutput, builderDone) require.NoError(t, err) }() builderMessages := make([]string, 0) @@ -92,7 +92,7 @@ func TestFlashblocksStream(gt *testing.T) { doneListening := make(chan struct{}) streamedMessages := make([]string, 0) go func() { - err := rollupBoostNode.FlashblocksClient().ListenFor(ctx, logger.With("stream_source", "rollup-boost"), testDuration, output, doneListening) + err := rollupBoostNode.FlashblocksClient().ReadAll(ctx, logger.With("stream_source", "rollup-boost"), testDuration, output, doneListening) require.NoError(t, err) }() diff --git a/op-acceptance-tests/tests/flashblocks/flashblocks_transfer_test.go b/op-acceptance-tests/tests/flashblocks/flashblocks_transfer_test.go index 77c925955c4..80cf2e3c8f6 100644 --- a/op-acceptance-tests/tests/flashblocks/flashblocks_transfer_test.go +++ b/op-acceptance-tests/tests/flashblocks/flashblocks_transfer_test.go @@ -45,9 +45,6 @@ func TestFlashblocksTransfer(gt *testing.T) { _, span = tracer.Start(ctx, fmt.Sprintf("test chain %s", sys.L2Chain.String())) defer span.End() - doneListening := make(chan struct{}) - output := make(chan []byte, 100) - // Drive a couple blocks on the test sequencer so the faucet L2 funding tx has a chance to land before we rely on it. driveViaTestSequencer(t, sys, 2) @@ -55,11 +52,15 @@ func TestFlashblocksTransfer(gt *testing.T) { bob := sys.Wallet.NewEOA(sys.L2EL) bobAddress := bob.Address().Hex() - // flashblocks listener + // flashblocks listener - start goroutine and wait for it to be running flashblocksClient := sys.L2RollupBoost.FlashblocksClient() + output := make(chan []byte, 100) + doneListening := make(chan struct{}) go func() { - err := flashblocksClient.ListenFor(ctx, logger, 20*time.Second, output, doneListening) - t.Require().NoError(err, "failed to listen for flashblocks") + err := flashblocksClient.ReadAll(ctx, logger.With("stream_source", "rollup-boost"), 20*time.Second, output, doneListening) + if err != nil { + t.Require().NoError(err, "failed to listen for flashblocks") + } }() var executedTransaction *txplan.PlannedTx diff --git a/op-acceptance-tests/tests/interop/reorgs/l2_reorgs_after_l1_reorg_test.go b/op-acceptance-tests/tests/interop/reorgs/l2_reorgs_after_l1_reorg_test.go index 9c7592a1c87..a46eb0a7de8 100644 --- a/op-acceptance-tests/tests/interop/reorgs/l2_reorgs_after_l1_reorg_test.go +++ b/op-acceptance-tests/tests/interop/reorgs/l2_reorgs_after_l1_reorg_test.go @@ -8,10 +8,8 @@ import ( "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" ) @@ -66,7 +64,6 @@ func testL2ReorgAfterL1Reorg(gt *testing.T, n int, preChecks, postChecks checksF ctx := t.Ctx() sys := presets.NewSimpleInterop(t) - ts := sys.TestSequencer.Escape().ControlAPI(sys.L1Network.ChainID()) cl := sys.L1Network.Escape().L1CLNode(match.FirstL1CL) @@ -76,7 +73,7 @@ func testL2ReorgAfterL1Reorg(gt *testing.T, n int, preChecks, postChecks checksF // sequence a few L1 and L2 blocks for range n + 1 { - sequenceL1Block(t, ts, common.Hash{}) + sys.TestSequencer.SequenceBlock(t, sys.L1Network.ChainID(), common.Hash{}) sys.L2ChainA.WaitForBlock() sys.L2ChainA.WaitForBlock() @@ -101,7 +98,7 @@ func testL2ReorgAfterL1Reorg(gt *testing.T, n int, preChecks, postChecks checksF 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) + sys.TestSequencer.SequenceBlock(t, sys.L1Network.ChainID(), divergence.ParentHash) // continue building on the alternative L1 chain sys.ControlPlane.FakePoSState(cl.ID(), stack.Start) @@ -160,8 +157,3 @@ func testL2ReorgAfterL1Reorg(gt *testing.T, n int, preChecks, postChecks checksF // 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/op-batcher/batcher/channel.go b/op-batcher/batcher/channel.go index b9299d11bf4..82dd79b03c4 100644 --- a/op-batcher/batcher/channel.go +++ b/op-batcher/batcher/channel.go @@ -27,7 +27,7 @@ type channel struct { } func newChannel(log log.Logger, metr metrics.Metricer, cfg ChannelConfig, rollupCfg *rollup.Config, latestL1OriginBlockNum uint64, channelOut derive.ChannelOut) *channel { - cb := NewChannelBuilderWithChannelOut(cfg, rollupCfg, latestL1OriginBlockNum, channelOut) + cb := NewChannelBuilderWithChannelOut(log, cfg, rollupCfg, latestL1OriginBlockNum, channelOut) return &channel{ ChannelBuilder: cb, log: log, diff --git a/op-batcher/batcher/channel_builder.go b/op-batcher/batcher/channel_builder.go index eab2f203726..966b43653b4 100644 --- a/op-batcher/batcher/channel_builder.go +++ b/op-batcher/batcher/channel_builder.go @@ -11,6 +11,7 @@ import ( "github.com/ethereum-optimism/optimism/op-node/rollup/derive" "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/queue" + "github.com/ethereum/go-ethereum/log" ) var ( @@ -47,6 +48,7 @@ type frameData struct { // ChannelBuilder uses a ChannelOut to create a channel with output frame // size approximation. type ChannelBuilder struct { + log log.Logger cfg ChannelConfig rollupCfg *rollup.Config @@ -87,8 +89,9 @@ type ChannelBuilder struct { outputBytes int } -func NewChannelBuilderWithChannelOut(cfg ChannelConfig, rollupCfg *rollup.Config, latestL1OriginBlockNum uint64, channelOut derive.ChannelOut) *ChannelBuilder { +func NewChannelBuilderWithChannelOut(log log.Logger, cfg ChannelConfig, rollupCfg *rollup.Config, latestL1OriginBlockNum uint64, channelOut derive.ChannelOut) *ChannelBuilder { cb := &ChannelBuilder{ + log: log.With("channel_id", channelOut.ID()), cfg: cfg, rollupCfg: rollupCfg, co: channelOut, @@ -224,8 +227,7 @@ func (c *ChannelBuilder) updateDurationTimeout(l1BlockNum uint64) { if c.cfg.MaxChannelDuration == 0 { return } - timeout := l1BlockNum + c.cfg.MaxChannelDuration - c.updateTimeout(timeout, ErrMaxDurationReached) + c.updateTimeout(l1BlockNum+c.cfg.MaxChannelDuration, ErrMaxDurationReached) } // updateSwTimeout updates the block timeout with the sequencer window timeout @@ -244,6 +246,7 @@ func (c *ChannelBuilder) updateSwTimeout(l1InfoNumber uint64) { // full error reason in case the timeout is hit in the future. func (c *ChannelBuilder) updateTimeout(timeoutBlockNum uint64, reason error) { if c.timeout == 0 || c.timeout > timeoutBlockNum { + c.log.Debug("setting timeout", "number", timeoutBlockNum, "timeout", c.timeout) c.timeout = timeoutBlockNum c.timeoutReason = reason } @@ -252,17 +255,12 @@ func (c *ChannelBuilder) updateTimeout(timeoutBlockNum uint64, reason error) { // CheckTimeout checks if the channel is timed out at the given block number and // in this case marks the channel as full, if it wasn't full already. func (c *ChannelBuilder) CheckTimeout(l1BlockNum uint64) { - if !c.IsFull() && c.TimedOut(l1BlockNum) { + if c.timeout != 0 && !c.IsFull() && l1BlockNum >= c.timeout { + c.log.Debug("checking timeout", "l1blockNum", l1BlockNum, "timeout", c.timeout) c.setFullErr(c.timeoutReason) } } -// TimedOut returns whether the passed block number is after the timeout block -// number. If no block timeout is set yet, it returns false. -func (c *ChannelBuilder) TimedOut(blockNum uint64) bool { - return c.timeout != 0 && blockNum >= c.timeout -} - // IsFull returns whether the channel is full. // FullErr returns the reason for the channel being full. func (c *ChannelBuilder) IsFull() bool { diff --git a/op-batcher/batcher/channel_builder_test.go b/op-batcher/batcher/channel_builder_test.go index becb123c667..4596dd48c6a 100644 --- a/op-batcher/batcher/channel_builder_test.go +++ b/op-batcher/batcher/channel_builder_test.go @@ -14,8 +14,10 @@ import ( "github.com/ethereum-optimism/optimism/op-node/rollup/derive" dtest "github.com/ethereum-optimism/optimism/op-node/rollup/derive/test" "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/testlog" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/trie" @@ -32,13 +34,13 @@ var defaultTestRollupConfig = &rollup.Config{ // newChannelBuilder creates a new channel builder or returns an error if the // channel out could not be created. // it acts as a factory for either a span or singular channel out -func newChannelBuilder(cfg ChannelConfig, rollupCfg *rollup.Config, latestL1OriginBlockNum uint64) (*ChannelBuilder, error) { +func newChannelBuilder(log log.Logger, cfg ChannelConfig, rollupCfg *rollup.Config, latestL1OriginBlockNum uint64) (*ChannelBuilder, error) { co, err := NewChannelOut(cfg, rollupCfg) if err != nil { return nil, fmt.Errorf("creating channel out: %w", err) } - return NewChannelBuilderWithChannelOut(cfg, rollupCfg, latestL1OriginBlockNum, co), nil + return NewChannelBuilderWithChannelOut(log, cfg, rollupCfg, latestL1OriginBlockNum, co), nil } // addMiniBlock adds a minimal valid L2 block to the channel builder using the @@ -166,23 +168,6 @@ func addTooManyBlocks(cb *ChannelBuilder, blockCount int) (int, error) { return blockCount, nil } -// FuzzDurationTimeoutZeroMaxChannelDuration ensures that when whenever the MaxChannelDuration -// is set to 0, the channel builder cannot have a duration timeout. -func FuzzDurationTimeoutZeroMaxChannelDuration(f *testing.F) { - for i := range [10]int{} { - f.Add(uint64(i)) - } - f.Fuzz(func(t *testing.T, l1BlockNum uint64) { - channelConfig := defaultTestChannelConfig() - channelConfig.MaxChannelDuration = 0 - cb, err := newChannelBuilder(channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) - require.NoError(t, err) - cb.timeout = 0 - cb.updateDurationTimeout(l1BlockNum) - require.False(t, cb.TimedOut(l1BlockNum)) - }) -} - // FuzzChannelBuilder_DurationZero ensures that when whenever the MaxChannelDuration // is not set to 0, the channel builder will always have a duration timeout // as long as the channel builder's timeout is set to 0. @@ -195,10 +180,11 @@ func FuzzChannelBuilder_DurationZero(f *testing.F) { t.Skip("Max channel duration cannot be 0") } + log := testlog.Logger(t, log.LvlInfo) // Create the channel builder channelConfig := defaultTestChannelConfig() channelConfig.MaxChannelDuration = maxChannelDuration - cb, err := newChannelBuilder(channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) + cb, err := newChannelBuilder(log, channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) require.NoError(t, err) // Whenever the timeout is set to 0, the channel builder should have a duration timeout @@ -222,10 +208,11 @@ func FuzzDurationTimeoutMaxChannelDuration(f *testing.F) { t.Skip("Max channel duration cannot be 0") } + log := testlog.Logger(t, log.LvlInfo) // Create the channel builder channelConfig := defaultTestChannelConfig() channelConfig.MaxChannelDuration = maxChannelDuration - cb, err := newChannelBuilder(channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) + cb, err := newChannelBuilder(log, channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) require.NoError(t, err) // Whenever the timeout is greater than the l1BlockNum, @@ -255,11 +242,12 @@ func FuzzChannelCloseTimeout(f *testing.F) { f.Add(uint64(i), uint64(i), uint64(i), uint64(i*5)) } f.Fuzz(func(t *testing.T, l1BlockNum uint64, channelTimeout uint64, subSafetyMargin uint64, timeout uint64) { + log := testlog.Logger(t, log.LvlInfo) // Create the channel builder channelConfig := defaultTestChannelConfig() channelConfig.ChannelTimeout = channelTimeout channelConfig.SubSafetyMargin = subSafetyMargin - cb, err := newChannelBuilder(channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) + cb, err := newChannelBuilder(log, channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) require.NoError(t, err) // Check the timeout @@ -283,11 +271,12 @@ func FuzzChannelZeroCloseTimeout(f *testing.F) { f.Add(uint64(i), uint64(i), uint64(i)) } f.Fuzz(func(t *testing.T, l1BlockNum uint64, channelTimeout uint64, subSafetyMargin uint64) { + log := testlog.Logger(t, log.LvlInfo) // Create the channel builder channelConfig := defaultTestChannelConfig() channelConfig.ChannelTimeout = channelTimeout channelConfig.SubSafetyMargin = subSafetyMargin - cb, err := newChannelBuilder(channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) + cb, err := newChannelBuilder(log, channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) require.NoError(t, err) // Check the timeout @@ -310,11 +299,12 @@ func FuzzSeqWindowClose(f *testing.F) { f.Add(uint64(i), uint64(i), uint64(i), uint64(i*5)) } f.Fuzz(func(t *testing.T, epochNum uint64, seqWindowSize uint64, subSafetyMargin uint64, timeout uint64) { + log := testlog.Logger(t, log.LvlInfo) // Create the channel builder channelConfig := defaultTestChannelConfig() channelConfig.SeqWindowSize = seqWindowSize channelConfig.SubSafetyMargin = subSafetyMargin - cb, err := newChannelBuilder(channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) + cb, err := newChannelBuilder(log, channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) require.NoError(t, err) // Check the timeout @@ -338,11 +328,12 @@ func FuzzSeqWindowZeroTimeoutClose(f *testing.F) { f.Add(uint64(i), uint64(i), uint64(i)) } f.Fuzz(func(t *testing.T, epochNum uint64, seqWindowSize uint64, subSafetyMargin uint64) { + log := testlog.Logger(t, log.LvlInfo) // Create the channel builder channelConfig := defaultTestChannelConfig() channelConfig.SeqWindowSize = seqWindowSize channelConfig.SubSafetyMargin = subSafetyMargin - cb, err := newChannelBuilder(channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) + cb, err := newChannelBuilder(log, channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) require.NoError(t, err) // Check the timeout @@ -387,10 +378,11 @@ func TestChannelBuilderBatchType(t *testing.T) { // TestChannelBuilder_NextFrame tests calling NextFrame on a ChannelBuilder with only one frame func TestChannelBuilder_NextFrame(t *testing.T) { + log := testlog.Logger(t, log.LvlInfo) channelConfig := defaultTestChannelConfig() // Create a new channel builder - cb, err := newChannelBuilder(channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) + cb, err := newChannelBuilder(log, channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) require.NoError(t, err) // Mock the internals of `ChannelBuilder.outputFrame` @@ -427,11 +419,12 @@ func TestChannelBuilder_NextFrame(t *testing.T) { // TestChannelBuilder_OutputWrongFramePanic tests that a panic is thrown when we try to rewind the cursor with an invalid frame id func ChannelBuilder_OutputWrongFramePanic(t *testing.T, batchType uint) { + log := testlog.Logger(t, log.LvlInfo) channelConfig := defaultTestChannelConfig() channelConfig.BatchType = batchType // Construct a channel builder - cb, err := newChannelBuilder(channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) + cb, err := newChannelBuilder(log, channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) require.NoError(t, err) // Mock the internals of `ChannelBuilder.outputFrame` @@ -461,13 +454,14 @@ func ChannelBuilder_OutputWrongFramePanic(t *testing.T, batchType uint) { // TestChannelBuilder_OutputFrames tests [ChannelBuilder.OutputFrames] for singular batches. func TestChannelBuilder_OutputFrames(t *testing.T) { + log := testlog.Logger(t, log.LvlInfo) channelConfig := defaultTestChannelConfig() channelConfig.MaxFrameSize = derive.FrameV0OverHeadSize + 1 channelConfig.TargetNumFrames = 1000 channelConfig.InitNoneCompressor() // Construct the channel builder - cb, err := newChannelBuilder(channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) + cb, err := newChannelBuilder(log, channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) require.NoError(t, err) require.False(t, cb.IsFull()) require.Equal(t, 0, cb.PendingFrames()) @@ -510,6 +504,7 @@ func TestChannelBuilder_OutputFrames_SpanBatch(t *testing.T) { } func ChannelBuilder_OutputFrames_SpanBatch(t *testing.T, algo derive.CompressionAlgo) { + log := testlog.Logger(t, log.LvlInfo) channelConfig := defaultTestChannelConfig() channelConfig.MaxFrameSize = 20 + derive.FrameV0OverHeadSize if algo.IsBrotli() { @@ -521,7 +516,7 @@ func ChannelBuilder_OutputFrames_SpanBatch(t *testing.T, algo derive.Compression channelConfig.InitRatioCompressor(1, algo) // Construct the channel builder - cb, err := newChannelBuilder(channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) + cb, err := newChannelBuilder(log, channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) require.NoError(t, err) require.False(t, cb.IsFull()) require.Equal(t, 0, cb.PendingFrames()) @@ -572,6 +567,7 @@ func ChannelBuilder_OutputFrames_SpanBatch(t *testing.T, algo derive.Compression // function errors when the max RLP bytes per channel is reached. func ChannelBuilder_MaxRLPBytesPerChannel(t *testing.T, batchType uint) { t.Parallel() + log := testlog.Logger(t, log.LvlInfo) channelConfig := defaultTestChannelConfig() chainSpec := rollup.NewChainSpec(defaultTestRollupConfig) channelConfig.MaxFrameSize = chainSpec.MaxRLPBytesPerChannel(latestL1BlockOrigin) * 2 @@ -579,7 +575,7 @@ func ChannelBuilder_MaxRLPBytesPerChannel(t *testing.T, batchType uint) { channelConfig.BatchType = batchType // Construct the channel builder - cb, err := newChannelBuilder(channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) + cb, err := newChannelBuilder(log, channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) require.NoError(t, err) // Add a block that overflows the [ChannelOut] @@ -594,6 +590,7 @@ func ChannelBuilder_MaxRLPBytesPerChannel(t *testing.T, batchType uint) { // then check postFjord w/ double the amount of blocks func ChannelBuilder_MaxRLPBytesPerChannelFjord(t *testing.T, batchType uint) { t.Parallel() + log := testlog.Logger(t, log.LvlInfo) channelConfig := defaultTestChannelConfig() chainSpec := rollup.NewChainSpec(defaultTestRollupConfig) channelConfig.MaxFrameSize = chainSpec.MaxRLPBytesPerChannel(latestL1BlockOrigin) * 2 @@ -601,7 +598,7 @@ func ChannelBuilder_MaxRLPBytesPerChannelFjord(t *testing.T, batchType uint) { channelConfig.BatchType = batchType // Construct the channel builder - cb, err := newChannelBuilder(channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) + cb, err := newChannelBuilder(log, channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) require.NoError(t, err) // Count how many a block that overflows the [ChannelOut] @@ -622,7 +619,7 @@ func ChannelBuilder_MaxRLPBytesPerChannelFjord(t *testing.T, batchType uint) { channelConfig.InitNoneCompressor() channelConfig.BatchType = batchType - cb, err = newChannelBuilder(channelConfig, rollupConfig, latestL1BlockOrigin) + cb, err = newChannelBuilder(log, channelConfig, rollupConfig, latestL1BlockOrigin) require.NoError(t, err) // try add double the amount of block, it should not error @@ -634,6 +631,7 @@ func ChannelBuilder_MaxRLPBytesPerChannelFjord(t *testing.T, batchType uint) { // ChannelBuilder_OutputFramesMaxFrameIndex tests the [ChannelBuilder.OutputFrames] // function errors when the max frame index is reached. func ChannelBuilder_OutputFramesMaxFrameIndex(t *testing.T, batchType uint) { + log := testlog.Logger(t, log.LvlInfo) channelConfig := defaultTestChannelConfig() channelConfig.MaxFrameSize = derive.FrameV0OverHeadSize + 1 channelConfig.TargetNumFrames = math.MaxUint16 + 1 @@ -645,7 +643,7 @@ func ChannelBuilder_OutputFramesMaxFrameIndex(t *testing.T, batchType uint) { // Continuously add blocks until the max frame index is reached // This should cause the [ChannelBuilder.OutputFrames] function // to error - cb, err := newChannelBuilder(channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) + cb, err := newChannelBuilder(log, channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) require.NoError(t, err) require.False(t, cb.IsFull()) require.Equal(t, 0, cb.PendingFrames()) @@ -673,6 +671,7 @@ func ChannelBuilder_OutputFramesMaxFrameIndex(t *testing.T, batchType uint) { // [derive.FrameV0OverHeadSize] in [MaxDataSize] is omitted, which has been the // case before it got fixed it #9887. func TestChannelBuilder_FullShadowCompressor(t *testing.T) { + log := testlog.Logger(t, log.LvlInfo) require := require.New(t) cfg := ChannelConfig{ MaxFrameSize: 752, @@ -681,7 +680,7 @@ func TestChannelBuilder_FullShadowCompressor(t *testing.T) { } cfg.InitShadowCompressor(derive.Zlib) - cb, err := newChannelBuilder(cfg, defaultTestRollupConfig, latestL1BlockOrigin) + cb, err := newChannelBuilder(log, cfg, defaultTestRollupConfig, latestL1BlockOrigin) require.NoError(err) rng := rand.New(rand.NewSource(420)) @@ -703,6 +702,7 @@ func TestChannelBuilder_FullShadowCompressor(t *testing.T) { } func ChannelBuilder_AddBlock(t *testing.T, batchType uint) { + log := testlog.Logger(t, log.LvlInfo) channelConfig := defaultTestChannelConfig() channelConfig.BatchType = batchType @@ -713,7 +713,7 @@ func ChannelBuilder_AddBlock(t *testing.T, batchType uint) { channelConfig.InitRatioCompressor(1, derive.Zlib) // Construct the channel builder - cb, err := newChannelBuilder(channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) + cb, err := newChannelBuilder(log, channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) require.NoError(t, err) // Add a nonsense block to the channel builder @@ -736,10 +736,11 @@ func ChannelBuilder_AddBlock(t *testing.T, batchType uint) { } func TestChannelBuilder_CheckTimeout(t *testing.T) { + log := testlog.Logger(t, log.LvlInfo) channelConfig := defaultTestChannelConfig() // Construct the channel builder - cb, err := newChannelBuilder(channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) + cb, err := newChannelBuilder(log, channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) require.NoError(t, err) // Assert timeout is setup correctly @@ -757,14 +758,34 @@ func TestChannelBuilder_CheckTimeout(t *testing.T) { require.ErrorIs(t, cb.FullErr(), ErrMaxDurationReached) } +func TestChannelBuilder_MaxChannelDurationZero(t *testing.T) { + log := testlog.Logger(t, log.LvlInfo) + channelConfig := defaultTestChannelConfig() + channelConfig.MaxChannelDuration = 0 + + // Construct the channel builder + cb, err := newChannelBuilder(log, channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) + require.NoError(t, err) + + // Assert timeout is setup correctly + require.Equal(t, uint64(0), channelConfig.MaxChannelDuration) + + // Check a new L1 block which should not update the timeout, due to config setting MaxChannelDuration to 0 + cb.CheckTimeout(uint64(12345)) + + require.Equal(t, uint64(0), cb.timeout) + require.NoError(t, cb.FullErr()) +} + func TestChannelBuilder_CheckTimeoutZeroMaxChannelDuration(t *testing.T) { + log := testlog.Logger(t, log.LvlInfo) channelConfig := defaultTestChannelConfig() // Set the max channel duration to 0 channelConfig.MaxChannelDuration = 0 // Construct the channel builder - cb, err := newChannelBuilder(channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) + cb, err := newChannelBuilder(log, channelConfig, defaultTestRollupConfig, latestL1BlockOrigin) require.NoError(t, err) // Without a max channel duration, timeout should not be set @@ -781,13 +802,14 @@ func TestChannelBuilder_CheckTimeoutZeroMaxChannelDuration(t *testing.T) { } func TestChannelBuilder_FramePublished(t *testing.T) { + log := testlog.Logger(t, log.LvlInfo) cfg := defaultTestChannelConfig() cfg.MaxChannelDuration = 10_000 cfg.ChannelTimeout = 1000 cfg.SubSafetyMargin = 100 // Construct the channel builder - cb, err := newChannelBuilder(cfg, defaultTestRollupConfig, latestL1BlockOrigin) + cb, err := newChannelBuilder(log, cfg, defaultTestRollupConfig, latestL1BlockOrigin) require.NoError(t, err) require.Equal(t, latestL1BlockOrigin+cfg.MaxChannelDuration, cb.timeout) @@ -804,9 +826,10 @@ func TestChannelBuilder_FramePublished(t *testing.T) { } func TestChannelBuilder_LatestL1Origin(t *testing.T) { - cb, err := newChannelBuilder(defaultTestChannelConfig(), defaultTestRollupConfig, latestL1BlockOrigin) + log := testlog.Logger(t, log.LvlInfo) + cb, err := newChannelBuilder(log, defaultTestChannelConfig(), defaultTestRollupConfig, latestL1BlockOrigin) require.NoError(t, err) - require.Equal(t, eth.BlockID{}, cb.LatestL1Origin()) + require.Equal(t, eth.BlockID{}, cb.LatestL1Origin(), "LatestL1Origin should be empty") _, err = cb.AddBlock(SizedBlock{Block: newMiniL2BlockWithNumberParentAndL1Information(0, big.NewInt(1), common.Hash{}, 1, 100)}) require.NoError(t, err) @@ -826,7 +849,8 @@ func TestChannelBuilder_LatestL1Origin(t *testing.T) { } func TestChannelBuilder_OldestL1Origin(t *testing.T) { - cb, err := newChannelBuilder(defaultTestChannelConfig(), defaultTestRollupConfig, latestL1BlockOrigin) + log := testlog.Logger(t, log.LvlInfo) + cb, err := newChannelBuilder(log, defaultTestChannelConfig(), defaultTestRollupConfig, latestL1BlockOrigin) require.NoError(t, err) require.Equal(t, eth.BlockID{}, cb.OldestL1Origin()) @@ -848,7 +872,8 @@ func TestChannelBuilder_OldestL1Origin(t *testing.T) { } func TestChannelBuilder_LatestL2(t *testing.T) { - cb, err := newChannelBuilder(defaultTestChannelConfig(), defaultTestRollupConfig, latestL1BlockOrigin) + log := testlog.Logger(t, log.LvlInfo) + cb, err := newChannelBuilder(log, defaultTestChannelConfig(), defaultTestRollupConfig, latestL1BlockOrigin) require.NoError(t, err) require.Equal(t, eth.BlockID{}, cb.LatestL2()) @@ -870,7 +895,8 @@ func TestChannelBuilder_LatestL2(t *testing.T) { } func TestChannelBuilder_OldestL2(t *testing.T) { - cb, err := newChannelBuilder(defaultTestChannelConfig(), defaultTestRollupConfig, latestL1BlockOrigin) + log := testlog.Logger(t, log.LvlInfo) + cb, err := newChannelBuilder(log, defaultTestChannelConfig(), defaultTestRollupConfig, latestL1BlockOrigin) require.NoError(t, err) require.Equal(t, eth.BlockID{}, cb.OldestL2()) @@ -892,6 +918,7 @@ func TestChannelBuilder_OldestL2(t *testing.T) { } func ChannelBuilder_PendingFrames_TotalFrames(t *testing.T, batchType uint) { + log := testlog.Logger(t, log.LvlInfo) const tnf = 9 rng := rand.New(rand.NewSource(94572314)) require := require.New(t) @@ -900,7 +927,7 @@ func ChannelBuilder_PendingFrames_TotalFrames(t *testing.T, batchType uint) { cfg.TargetNumFrames = tnf cfg.BatchType = batchType cfg.InitShadowCompressor(derive.Zlib) - cb, err := newChannelBuilder(cfg, defaultTestRollupConfig, latestL1BlockOrigin) + cb, err := newChannelBuilder(log, cfg, defaultTestRollupConfig, latestL1BlockOrigin) require.NoError(err) // initial builder should be empty @@ -936,6 +963,7 @@ func ChannelBuilder_PendingFrames_TotalFrames(t *testing.T, batchType uint) { } func ChannelBuilder_InputBytes(t *testing.T, batchType uint) { + log := testlog.Logger(t, log.LvlInfo) require := require.New(t) rng := rand.New(rand.NewSource(4982432)) cfg := defaultTestChannelConfig() @@ -945,7 +973,7 @@ func ChannelBuilder_InputBytes(t *testing.T, batchType uint) { chainId := big.NewInt(1234) spanBatch = derive.NewSpanBatch(uint64(0), chainId) } - cb, err := newChannelBuilder(cfg, defaultTestRollupConfig, latestL1BlockOrigin) + cb, err := newChannelBuilder(log, cfg, defaultTestRollupConfig, latestL1BlockOrigin) require.NoError(err) require.Zero(cb.InputBytes()) @@ -975,6 +1003,7 @@ func ChannelBuilder_InputBytes(t *testing.T, batchType uint) { } func ChannelBuilder_OutputBytes(t *testing.T, batchType uint) { + log := testlog.Logger(t, log.LvlInfo) require := require.New(t) rng := rand.New(rand.NewSource(9860372)) cfg := defaultTestChannelConfig() @@ -982,7 +1011,7 @@ func ChannelBuilder_OutputBytes(t *testing.T, batchType uint) { cfg.TargetNumFrames = 16 cfg.BatchType = batchType cfg.InitRatioCompressor(1.0, derive.Zlib) - cb, err := newChannelBuilder(cfg, defaultTestRollupConfig, latestL1BlockOrigin) + cb, err := newChannelBuilder(log, cfg, defaultTestRollupConfig, latestL1BlockOrigin) require.NoError(err, "NewChannelBuilder") require.Zero(cb.OutputBytes()) diff --git a/op-batcher/batcher/channel_manager.go b/op-batcher/batcher/channel_manager.go index 4e1ca799cc3..f65844a6745 100644 --- a/op-batcher/batcher/channel_manager.go +++ b/op-batcher/batcher/channel_manager.go @@ -109,7 +109,6 @@ func (s *channelManager) TxFailed(_id txID) { // TxConfirmed marks a transaction as confirmed on L1. Only if the channel timed out // the channelManager's state is modified. func (s *channelManager) TxConfirmed(_id txID, inclusionBlock eth.BlockID) { - id := _id.String() if channel, ok := s.txChannels[id]; ok { delete(s.txChannels, id) @@ -225,8 +224,8 @@ func (s *channelManager) nextTxData(channel *channel) (txData, error) { // It will decide whether to switch DA type automatically. // When switching DA type, the channelManager state will be rebuilt // with a new ChannelConfig. -func (s *channelManager) TxData(l1Head eth.BlockID, isPectra, isThrottling, forcePublish bool) (txData, error) { - channel, err := s.getReadyChannel(l1Head, forcePublish) +func (s *channelManager) TxData(l1Head eth.BlockID, isPectra bool, isThrottling bool, pi pubInfo) (txData, error) { + channel, err := s.getReadyChannel(l1Head, pi) if err != nil { return emptyTxData, err } @@ -260,13 +259,23 @@ func (s *channelManager) TxData(l1Head eth.BlockID, isPectra, isThrottling, forc s.defaultCfg = newCfg // Try again to get data to send on chain. - channel, err = s.getReadyChannel(l1Head, forcePublish) + channel, err = s.getReadyChannel(l1Head, pi) if err != nil { return emptyTxData, err } return s.nextTxData(channel) } +// pubInfo is a struct that contains signal information sent on the publishSignal channel +type pubInfo struct { + // forcePublish is set to true if the current channel should be force-closed and submitted now. + forcePublish bool + + // ignoreMaxChannelDuration is set to true if we should keep the current channel open even if it's duration is exceeded. + // For example, if we know there are more blocks to load and we want to pack those into the current channel before sending it. + ignoreMaxChannelDuration bool +} + // getReadyChannel returns the next channel ready to submit data, or an error. // It will create a new channel if necessary. // If there is no data ready to send, it adds blocks from the block queue @@ -275,9 +284,8 @@ func (s *channelManager) TxData(l1Head eth.BlockID, isPectra, isThrottling, forc // there is no channel with txData // If forcePublish is true, it will force close channels and // generate frames for them. -func (s *channelManager) getReadyChannel(l1Head eth.BlockID, forcePublish bool) (*channel, error) { - - if forcePublish && s.currentChannel.TotalFrames() == 0 { +func (s *channelManager) getReadyChannel(l1Head eth.BlockID, pi pubInfo) (*channel, error) { + if pi.forcePublish && s.currentChannel.TotalFrames() == 0 { s.log.Info("Force-closing channel and creating frames", "channel_id", s.currentChannel.ID()) s.currentChannel.Close() if err := s.currentChannel.OutputFrames(); err != nil { @@ -315,10 +323,14 @@ func (s *channelManager) getReadyChannel(l1Head eth.BlockID, forcePublish bool) return nil, err } - // Register current L1 head only after all pending blocks have been - // processed. Even if a timeout will be triggered now, it is better to have - // all pending blocks be included in this channel for submission. - s.registerL1Block(l1Head) + if !pi.ignoreMaxChannelDuration { + // Register current L1 head (which checks for the max duration timeout) + // only after all blocks in the manager's state have been + // processed, and only if we weren't told to ignore the max channel duration. + // The aim is to prefer to optimally pack blocks into channels when + // instead of timing out the channel when more blocks soon to be processed. + s.registerL1Block(l1Head) + } if err := s.outputFrames(); err != nil { return nil, err @@ -461,6 +473,8 @@ func (s *channelManager) outputFrames() error { s.log.Info("Channel closed", "id", s.currentChannel.ID(), "blocks_pending", s.pendingBlocks(), + "block_cursor", s.blockCursor, + "blocks_len", s.blocks.Len(), "num_frames", s.currentChannel.TotalFrames(), "input_bytes", inBytes, "output_bytes", outBytes, @@ -540,7 +554,6 @@ func (s *channelManager) PruneChannels(num int) { if clearCurrentChannel { s.currentChannel = nil } - } // PendingDABytes returns the current number of bytes pending to be written to the DA layer (from blocks fetched from L2 diff --git a/op-batcher/batcher/channel_manager_memory_test.go b/op-batcher/batcher/channel_manager_memory_test.go index 47b89c6e9a7..fe305aa2fe6 100644 --- a/op-batcher/batcher/channel_manager_memory_test.go +++ b/op-batcher/batcher/channel_manager_memory_test.go @@ -124,7 +124,7 @@ func runMemoryTest(t *testing.T, batchType uint, compressorType string, compress require.NoError(t, m.processBlocks()) // Try to get transaction data to fill channels - _, err := m.TxData(eth.BlockID{}, false, false, false) + _, err := m.TxData(eth.BlockID{}, false, false, pubInfo{}) // It's okay if there's no data ready (io.EOF) if err != nil && err.Error() != "EOF" { require.NoError(t, err) diff --git a/op-batcher/batcher/channel_manager_test.go b/op-batcher/batcher/channel_manager_test.go index 2aafe1f315d..0c4feb9e0eb 100644 --- a/op-batcher/batcher/channel_manager_test.go +++ b/op-batcher/batcher/channel_manager_test.go @@ -103,9 +103,9 @@ func ChannelManagerReturnsErrReorgWhenDrained(t *testing.T, batchType uint) { require.NoError(t, m.AddL2Block(a)) - _, err := m.TxData(eth.BlockID{}, false, false, false) + _, err := m.TxData(eth.BlockID{}, false, false, pubInfo{}) require.NoError(t, err) - _, err = m.TxData(eth.BlockID{}, false, false, false) + _, err = m.TxData(eth.BlockID{}, false, false, pubInfo{}) require.ErrorIs(t, err, io.EOF) require.ErrorIs(t, m.AddL2Block(x), ErrReorg) @@ -207,7 +207,7 @@ func ChannelManager_TxResend(t *testing.T, batchType uint) { require.NoError(m.AddL2Block(a)) - txdata0, err := m.TxData(eth.BlockID{}, false, false, false) + txdata0, err := m.TxData(eth.BlockID{}, false, false, pubInfo{}) require.NoError(err) txdata0bytes := txdata0.CallData() data0 := make([]byte, len(txdata0bytes)) @@ -215,13 +215,13 @@ func ChannelManager_TxResend(t *testing.T, batchType uint) { copy(data0, txdata0bytes) // ensure channel is drained - _, err = m.TxData(eth.BlockID{}, false, false, false) + _, err = m.TxData(eth.BlockID{}, false, false, pubInfo{}) require.ErrorIs(err, io.EOF) // requeue frame m.TxFailed(txdata0.ID()) - txdata1, err := m.TxData(eth.BlockID{}, false, false, false) + txdata1, err := m.TxData(eth.BlockID{}, false, false, pubInfo{}) require.NoError(err) data1 := txdata1.CallData() @@ -318,6 +318,48 @@ func newFakeDynamicEthChannelConfig(lgr log.Logger, } } +// TestChannelManager_IgnoreMaxChannelDuration tests that the channel manager will not time out +// when ignoreMaxChannelDuration is set to true in the signal struct. +func TestChannelManager_IgnoreMaxChannelDuration(t *testing.T) { + l := testlog.Logger(t, log.LevelCrit) + + cfg := channelManagerTestConfig(10000, derive.SingularBatchType) + cfg.MaxChannelDuration = 20 + cfg.InitNoneCompressor() + + m := NewChannelManager(l, metrics.NoopMetrics, cfg, defaultTestRollupConfig) + + // Seed channel manager with blocks + rng := rand.New(rand.NewSource(99)) + for range 2 { + block := derivetest.RandomL2BlockWithChainId(rng, 2, defaultTestRollupConfig.L2ChainID) + m.blocks.Enqueue(SizedBlock{Block: block}) + } + + // Call TxData a first time - if `ignoreMaxChannelDuration` is `false`, channel would be timed out, + // but since `ignoreMaxChannelDuration` is `true`, we expect it to be not timed out. + _, err := m.TxData(eth.BlockID{Number: 21}, false, false, pubInfo{ignoreMaxChannelDuration: true}) + require.ErrorIs(t, err, io.EOF) + + // Add more blocks to the channel manager + for range 2 { + block := derivetest.RandomL2BlockWithChainId(rng, 2, defaultTestRollupConfig.L2ChainID) + m.blocks.Enqueue(SizedBlock{Block: block}) + } + + require.NotEmpty(t, m.channelQueue) + require.False(t, m.channelQueue[0].IsFull()) + + // Call TxData again, with ignoreMaxChannelDuration unset. + _, err = m.TxData(eth.BlockID{Number: 22}, false, false, pubInfo{}) + require.NoError(t, err) + require.NotEmpty(t, m.channelQueue) + + // Given that ignoreMaxChannelDuration was unset, the channel should be timed out + require.True(t, m.channelQueue[0].IsFull()) + require.ErrorIs(t, m.channelQueue[0].FullErr(), ErrMaxDurationReached) +} + // TestChannelManager_TxData seeds the channel manager with blocks and triggers the // blocks->channels pipeline multiple times. Values are chosen such that a channel // is created under one set of market conditions, and then submitted under a different @@ -364,7 +406,7 @@ func TestChannelManager_TxData(t *testing.T) { m.blocks = queue.Queue[SizedBlock]{SizedBlock{Block: blockA}} // Call TxData a first time to trigger blocks->channels pipeline - _, err := m.TxData(eth.BlockID{}, false, false, false) + _, err := m.TxData(eth.BlockID{}, false, false, pubInfo{}) require.ErrorIs(t, err, io.EOF) // The test requires us to have something in the channel queue @@ -383,7 +425,7 @@ func TestChannelManager_TxData(t *testing.T) { var data txData for { m.blocks.Enqueue(SizedBlock{Block: blockA}) - data, err = m.TxData(eth.BlockID{}, false, false, false) + data, err = m.TxData(eth.BlockID{}, false, false, pubInfo{}) if err == nil && data.Len() > 0 { break } @@ -607,11 +649,12 @@ func TestChannelManager_PruneBlocks(t *testing.T) { func TestChannelManager_PruneChannels(t *testing.T) { cfg := channelManagerTestConfig(100, derive.SingularBatchType) - A, err := newChannelWithChannelOut(nil, metrics.NoopMetrics, cfg, defaultTestRollupConfig, 0) + l := testlog.Logger(t, log.LevelCrit) + A, err := newChannelWithChannelOut(l, metrics.NoopMetrics, cfg, defaultTestRollupConfig, 0) require.NoError(t, err) - B, err := newChannelWithChannelOut(nil, metrics.NoopMetrics, cfg, defaultTestRollupConfig, 0) + B, err := newChannelWithChannelOut(l, metrics.NoopMetrics, cfg, defaultTestRollupConfig, 0) require.NoError(t, err) - C, err := newChannelWithChannelOut(nil, metrics.NoopMetrics, cfg, defaultTestRollupConfig, 0) + C, err := newChannelWithChannelOut(l, metrics.NoopMetrics, cfg, defaultTestRollupConfig, 0) require.NoError(t, err) type testCase struct { @@ -710,7 +753,7 @@ func TestChannelManager_TxData_ForcePublish(t *testing.T) { m.blocks = queue.Queue[SizedBlock]{SizedBlock{Block: blockA}} // Call TxData a first time to trigger blocks->channels pipeline - txData, err := m.TxData(eth.BlockID{}, false, false, false) + txData, err := m.TxData(eth.BlockID{}, false, false, pubInfo{}) require.ErrorIs(t, err, io.EOF) require.Zero(t, txData.Len(), 0) @@ -720,7 +763,7 @@ func TestChannelManager_TxData_ForcePublish(t *testing.T) { require.False(t, m.channelQueue[0].IsFull()) // Call TxData with force publish enabled - txData, err = m.TxData(eth.BlockID{}, false, false, true) + txData, err = m.TxData(eth.BlockID{}, false, false, pubInfo{forcePublish: true}) // Despite no additional blocks being added, we should have tx data: require.NoError(t, err) @@ -827,7 +870,7 @@ func TestChannelManagerUnsafeBytes(t *testing.T) { _, err = manager.TxData(eth.BlockID{ Hash: common.Hash{}, Number: 0, - }, true, false, false) + }, true, false, pubInfo{}) } assert.Equal(t, tc.afterAddingToChannel, manager.UnsafeDABytes()) diff --git a/op-batcher/batcher/driver.go b/op-batcher/batcher/driver.go index 4630df4a6f3..7cce08003ef 100644 --- a/op-batcher/batcher/driver.go +++ b/op-batcher/batcher/driver.go @@ -117,7 +117,7 @@ type BatchSubmitter struct { throttleController *throttler.ThrottleController - publishSignal chan bool // true if we should force a tx to be published now, false if we should check the usual conditions (timeouts) + publishSignal chan pubInfo } // NewBatchSubmitter initializes the BatchSubmitter driver from a preconfigured DriverSetup @@ -173,7 +173,7 @@ func (l *BatchSubmitter) StartBatchSubmitting() error { // Channels used to signal between the loops unsafeBytesUpdated := make(chan int64, 1) - publishSignal := make(chan bool, 1) + publishSignal := make(chan pubInfo, 1) l.publishSignal = publishSignal // DA throttling loop should always be started except for testing (indicated by ThrottleThreshold == 0) @@ -268,13 +268,13 @@ func (l *BatchSubmitter) Flush(ctx context.Context) error { } l.Log.Info("Flushing Batch Submitter") - trySignal(l.publishSignal, true) + l.tryPublishSignal(l.publishSignal, pubInfo{forcePublish: true}) return nil } // loadBlocksIntoState loads the blocks between start and end (inclusive). // If there is a reorg, it will return an error. -func (l *BatchSubmitter) loadBlocksIntoState(ctx context.Context, start, end uint64, publishSignal chan bool, unsafeBytesUpdated chan int64) error { +func (l *BatchSubmitter) loadBlocksIntoState(ctx context.Context, start, end uint64, publishSignal chan pubInfo, unsafeBytesUpdated chan int64) error { if end < start { return fmt.Errorf("start number is > end number %d,%d", start, end) } @@ -302,7 +302,7 @@ func (l *BatchSubmitter) loadBlocksIntoState(ctx context.Context, start, end uin // This allows the batcher to start publishing sooner in the // case of a large backlog of blocks to load. l.sendToThrottlingLoop(unsafeBytesUpdated) - trySignal(publishSignal, false) + l.tryPublishSignal(publishSignal, pubInfo{ignoreMaxChannelDuration: i < end}) } } @@ -328,7 +328,6 @@ func (l *BatchSubmitter) loadBlockIntoState(ctx context.Context, blockNumber uin defer cancel() block, err := l2Client.BlockByNumber(cCtx, new(big.Int).SetUint64(blockNumber)) - if err != nil { return nil, fmt.Errorf("getting L2 block: %w", err) } @@ -437,12 +436,13 @@ func (l *BatchSubmitter) sendToThrottlingLoop(unsafeBytesUpdated chan int64) { } } -// trySignal tries to send an empty struct on the provided channel. +// trySignal tries to send the provided value on the provided channel. // It is not blocking, no signal will be sent if the channel is full. -func trySignal(c chan bool, value bool) { +func (l *BatchSubmitter) tryPublishSignal(c chan pubInfo, value pubInfo) { select { case c <- value: default: + l.Log.Warn("publishSignal channel is full, skipping signal") } } @@ -487,7 +487,7 @@ func (l *BatchSubmitter) syncAndPrune(syncStatus *eth.SyncStatus) *inclusiveBloc // - waits for a signal that blocks have been loaded // - drives the creation of channels and frames // - sends transactions to the DA layer -func (l *BatchSubmitter) publishingLoop(ctx context.Context, wg *sync.WaitGroup, receiptsCh chan txmgr.TxReceipt[txRef], publishSignal chan bool) { +func (l *BatchSubmitter) publishingLoop(ctx context.Context, wg *sync.WaitGroup, receiptsCh chan txmgr.TxReceipt[txRef], publishSignal chan pubInfo) { defer close(receiptsCh) defer wg.Done() @@ -499,9 +499,9 @@ func (l *BatchSubmitter) publishingLoop(ctx context.Context, wg *sync.WaitGroup, } txQueue := txmgr.NewQueue[txRef](ctx, l.Txmgr, l.Config.MaxPendingTransactions) - for forcePublish := range publishSignal { - l.Log.Debug("publishing loop received signal", "force_publish", forcePublish) - l.publishStateToL1(ctx, txQueue, receiptsCh, daGroup, forcePublish) + for pi := range l.publishSignal { + l.Log.Debug("publishing loop received signal", "force_publish", pi.forcePublish) + l.publishStateToL1(ctx, txQueue, receiptsCh, daGroup, pi) } // First wait for all DA requests to finish to prevent new transactions being queued @@ -524,7 +524,7 @@ func (l *BatchSubmitter) publishingLoop(ctx context.Context, wg *sync.WaitGroup, // - polls the sequencer, // - prunes the channel manager state (i.e. safe blocks) // - loads unsafe blocks from the sequencer -func (l *BatchSubmitter) blockLoadingLoop(ctx context.Context, wg *sync.WaitGroup, unsafeBytesUpdated chan int64, publishSignal chan bool) { +func (l *BatchSubmitter) blockLoadingLoop(ctx context.Context, wg *sync.WaitGroup, unsafeBytesUpdated chan int64, publishSignal chan pubInfo) { ticker := time.NewTicker(l.Config.PollInterval) defer ticker.Stop() defer close(unsafeBytesUpdated) @@ -556,7 +556,7 @@ func (l *BatchSubmitter) blockLoadingLoop(ctx context.Context, wg *sync.WaitGrou l.sendToThrottlingLoop(unsafeBytesUpdated) // we have increased the unsafe data. Signal the throttling loop to check if it should throttle. } } - trySignal(publishSignal, false) // always signal the write loop to ensure we periodically publish even if we aren't loading blocks + l.tryPublishSignal(publishSignal, pubInfo{}) // always signal the publishing loop to ensure we periodically publish even if we aren't loading blocks case <-ctx.Done(): l.Log.Info("blockLoadingLoop returning") return @@ -771,7 +771,7 @@ func (l *BatchSubmitter) waitNodeSync() error { // publishStateToL1 queues up all pending TxData to be published to the L1, returning when there is no more data to // queue for publishing or if there was an error queuing the data. -func (l *BatchSubmitter) publishStateToL1(ctx context.Context, queue *txmgr.Queue[txRef], receiptsCh chan txmgr.TxReceipt[txRef], daGroup *errgroup.Group, forcePublish bool) { +func (l *BatchSubmitter) publishStateToL1(ctx context.Context, queue *txmgr.Queue[txRef], receiptsCh chan txmgr.TxReceipt[txRef], daGroup *errgroup.Group, pi pubInfo) { for { select { case <-ctx.Done(): @@ -788,7 +788,7 @@ func (l *BatchSubmitter) publishStateToL1(ctx context.Context, queue *txmgr.Queu return } - err := l.publishTxToL1(ctx, queue, receiptsCh, daGroup, forcePublish) + err := l.publishTxToL1(ctx, queue, receiptsCh, daGroup, pi) if err != nil { if err != io.EOF { l.Log.Error("Error publishing tx to l1", "err", err) @@ -842,7 +842,7 @@ func (l *BatchSubmitter) clearState(ctx context.Context) { } // publishTxToL1 submits a single state tx to the L1 -func (l *BatchSubmitter) publishTxToL1(ctx context.Context, queue *txmgr.Queue[txRef], receiptsCh chan txmgr.TxReceipt[txRef], daGroup *errgroup.Group, forcePublish bool) error { +func (l *BatchSubmitter) publishTxToL1(ctx context.Context, queue *txmgr.Queue[txRef], receiptsCh chan txmgr.TxReceipt[txRef], daGroup *errgroup.Group, pi pubInfo) error { // send all available transactions l1tip, isPectra, err := l.l1Tip(ctx) if err != nil { @@ -855,7 +855,7 @@ func (l *BatchSubmitter) publishTxToL1(ctx context.Context, queue *txmgr.Queue[t // Collect next transaction data. This pulls data out of the channel, so we need to make sure // to put it back if ever da or txmgr requests fail, by calling l.recordFailedDARequest/recordFailedTx. l.channelMgrMutex.Lock() - txdata, err := l.channelMgr.TxData(l1tip.ID(), isPectra, params.IsThrottling(), forcePublish) + txdata, err := l.channelMgr.TxData(l1tip.ID(), isPectra, params.IsThrottling(), pi) l.channelMgrMutex.Unlock() if err == io.EOF { diff --git a/op-challenger/cmd/main_test.go b/op-challenger/cmd/main_test.go index 2e5ae1652b3..0647e675f2c 100644 --- a/op-challenger/cmd/main_test.go +++ b/op-challenger/cmd/main_test.go @@ -150,7 +150,7 @@ func TestOpSupervisor(t *testing.T) { func TestGameTypes(t *testing.T) { t.Run("Default", func(t *testing.T) { - expectedDefault := []gameTypes.GameType{gameTypes.CannonGameType, gameTypes.AsteriscKonaGameType, gameTypes.CannonKonaGameType} + expectedDefault := []gameTypes.GameType{gameTypes.CannonGameType, gameTypes.CannonKonaGameType} cfg := configForArgs(t, addRequiredArgsForMultipleGameTypesExcept(expectedDefault, "--game-types")) require.Equal(t, expectedDefault, cfg.GameTypes) }) @@ -1409,7 +1409,7 @@ func requiredArgs(gameType gameTypes.GameType) map[string]string { addRequiredSuperCannonKonaArgs(args) case gameTypes.SuperAsteriscKonaGameType: addRequiredSuperAsteriscKonaArgs(args) - case gameTypes.AlphabetGameType, gameTypes.FastGameType: + case gameTypes.OptimisticZKGameType, gameTypes.AlphabetGameType, gameTypes.FastGameType: addRequiredOutputRootArgs(args) } return args diff --git a/op-challenger/config/config.go b/op-challenger/config/config.go index 469fba793f2..fa37498de70 100644 --- a/op-challenger/config/config.go +++ b/op-challenger/config/config.go @@ -365,6 +365,11 @@ func (c Config) Check() error { return err } } + if c.GameTypeEnabled(gameTypes.OptimisticZKGameType) { + if c.RollupRpc == "" { + return ErrMissingRollupRpc + } + } if c.GameTypeEnabled(gameTypes.AlphabetGameType) || c.GameTypeEnabled(gameTypes.FastGameType) { if c.RollupRpc == "" { return ErrMissingRollupRpc diff --git a/op-challenger/config/config_test.go b/op-challenger/config/config_test.go index 3b1b0107b48..86b38b175e1 100644 --- a/op-challenger/config/config_test.go +++ b/op-challenger/config/config_test.go @@ -150,6 +150,10 @@ func applyValidConfigForSuperAsteriscKona(t *testing.T, cfg *Config) { applyValidConfigForAsteriscKona(t, cfg) } +func applyValidConfigForOptimisticZK(cfg *Config) { + cfg.RollupRpc = validRollupRpc +} + func validConfig(t *testing.T, gameType gameTypes.GameType) Config { cfg := NewConfig(validGameFactoryAddress, validL1EthRpc, validL1BeaconUrl, validRollupRpc, validL2Rpc, validDatadir, gameType) if gameType == gameTypes.SuperCannonGameType || gameType == gameTypes.SuperPermissionedGameType { @@ -173,6 +177,9 @@ func validConfig(t *testing.T, gameType gameTypes.GameType) Config { if gameType == gameTypes.SuperAsteriscKonaGameType { applyValidConfigForSuperAsteriscKona(t, &cfg) } + if gameType == gameTypes.OptimisticZKGameType { + applyValidConfigForOptimisticZK(&cfg) + } return cfg } diff --git a/op-challenger/flags/flags.go b/op-challenger/flags/flags.go index 1a9c4c2efd2..d5626fab855 100644 --- a/op-challenger/flags/flags.go +++ b/op-challenger/flags/flags.go @@ -88,7 +88,7 @@ var ( Aliases: []string{"trace-type"}, // For backwards compatibility Usage: "The game types to support. Valid options: " + openum.EnumStringer(gameTypes.SupportedGameTypes), EnvVars: prefixEnvVars("GAME_TYPES", "TRACE_TYPE"), - Value: cli.NewStringSlice(gameTypes.CannonGameType.String(), gameTypes.AsteriscKonaGameType.String(), gameTypes.CannonKonaGameType.String()), + Value: cli.NewStringSlice(gameTypes.CannonGameType.String(), gameTypes.CannonKonaGameType.String()), } DatadirFlag = &cli.StringFlag{ Name: "datadir", @@ -589,7 +589,7 @@ func CheckRequired(ctx *cli.Context, types []gameTypes.GameType) error { if err := CheckSuperAsteriscKonaFlags(ctx); err != nil { return err } - case gameTypes.AlphabetGameType, gameTypes.FastGameType: + case gameTypes.OptimisticZKGameType, gameTypes.AlphabetGameType, gameTypes.FastGameType: if err := checkOutputProviderFlags(ctx); err != nil { return err } diff --git a/op-challenger/game/fault/contracts/disputegame.go b/op-challenger/game/fault/contracts/disputegame.go index 5dd756d5099..611e0e0cf60 100644 --- a/op-challenger/game/fault/contracts/disputegame.go +++ b/op-challenger/game/fault/contracts/disputegame.go @@ -23,6 +23,7 @@ type GenericGameMetadata struct { } type DisputeGameContract interface { + Addr() common.Address GetL1Head(ctx context.Context) (common.Hash, error) GetStatus(ctx context.Context) (gameTypes.GameStatus, error) GetGameRange(ctx context.Context) (prestateBlock uint64, poststateBlock uint64, retErr error) diff --git a/op-challenger/game/fault/contracts/faultdisputegame.go b/op-challenger/game/fault/contracts/faultdisputegame.go index 8d5f3fa2baa..d6239633c71 100644 --- a/op-challenger/game/fault/contracts/faultdisputegame.go +++ b/op-challenger/game/fault/contracts/faultdisputegame.go @@ -162,6 +162,10 @@ func mustParseAbi(json []byte) *abi.ABI { return &loaded } +func (f *FaultDisputeGameContractLatest) Addr() common.Address { + return f.contract.Addr() +} + // GetBalanceAndDelay returns the total amount of ETH controlled by this contract. // Note that the ETH is actually held by the DelayedWETH contract which may be shared by multiple games. // Returns the balance and the address of the contract that actually holds the balance. diff --git a/op-challenger/game/fault/contracts/gamefactory.go b/op-challenger/game/fault/contracts/gamefactory.go index 4727d0b9cc5..29097433a80 100644 --- a/op-challenger/game/fault/contracts/gamefactory.go +++ b/op-challenger/game/fault/contracts/gamefactory.go @@ -106,15 +106,29 @@ func (f *DisputeGameFactoryContract) GetGameCount(ctx context.Context, blockHash return result.GetBigInt(0).Uint64(), nil } -func (f *DisputeGameFactoryContract) GetGame(ctx context.Context, idx uint64, blockHash common.Hash) (gameTypes.GameMetadata, error) { +func (f *DisputeGameFactoryContract) GetGame(ctx context.Context, idx uint64, block rpcblock.Block) (gameTypes.GameMetadata, error) { defer f.metrics.StartContractRequest("GetGame")() - result, err := f.multiCaller.SingleCall(ctx, rpcblock.ByHash(blockHash), f.contract.Call(methodGameAtIndex, new(big.Int).SetUint64(idx))) + result, err := f.multiCaller.SingleCall(ctx, block, f.contract.Call(methodGameAtIndex, new(big.Int).SetUint64(idx))) if err != nil { return gameTypes.GameMetadata{}, fmt.Errorf("failed to load game %v: %w", idx, err) } return f.decodeGame(idx, result), nil } +func (f *DisputeGameFactoryContract) GetGameStatus(ctx context.Context, idx uint64) (gameTypes.GameStatus, error) { + defer f.metrics.StartContractRequest("GetGameStatus")() + game, err := f.GetGame(ctx, idx, rpcblock.Latest) + if err != nil { + return 0, fmt.Errorf("failed to load game status: %w", err) + } + + gameContract, err := NewDisputeGameContract(ctx, f.metrics, f.multiCaller, gameTypes.GameType(game.GameType), game.Proxy) + if err != nil { + return 0, fmt.Errorf("failed to create contract bindings for game %s: %w", game.Proxy, err) + } + return gameContract.GetStatus(ctx) +} + func (f *DisputeGameFactoryContract) getGameImpl(ctx context.Context, gameType gameTypes.GameType) (common.Address, error) { defer f.metrics.StartContractRequest("GetGameImpl")() result, err := f.multiCaller.SingleCall(ctx, rpcblock.Latest, f.contract.Call(methodGameImpls, gameType)) diff --git a/op-challenger/game/fault/contracts/gamefactory_test.go b/op-challenger/game/fault/contracts/gamefactory_test.go index bdd468c49f4..b451628ef0b 100644 --- a/op-challenger/game/fault/contracts/gamefactory_test.go +++ b/op-challenger/game/fault/contracts/gamefactory_test.go @@ -123,8 +123,8 @@ func TestLoadGame(t *testing.T) { } expectedGames := []gameTypes.GameMetadata{game0, game1, game2} for idx, expected := range expectedGames { - expectGetGame(stubRpc, idx, blockHash, expected) - actual, err := factory.GetGame(context.Background(), uint64(idx), blockHash) + expectGetGame(stubRpc, idx, rpcblock.ByHash(blockHash), expected) + actual, err := factory.GetGame(context.Background(), uint64(idx), rpcblock.ByHash(blockHash)) require.NoError(t, err) require.Equal(t, expected, actual) } @@ -132,6 +132,28 @@ func TestLoadGame(t *testing.T) { } } +func TestGetGameStatus(t *testing.T) { + for _, version := range factoryVersions { + t.Run(version.String(), func(t *testing.T) { + stubRpc, factory := setupDisputeGameFactoryTest(t, version) + game0 := gameTypes.GameMetadata{ + Index: 0, + GameType: 0, + Timestamp: 1234, + Proxy: common.Address{0xaa}, + } + expectGetGame(stubRpc, 0, rpcblock.Latest, game0) + stubRpc.AddContract(game0.Proxy, snapshots.LoadFaultDisputeGameABI()) + expectedStatus := gameTypes.GameStatusChallengerWon + stubRpc.SetResponse(game0.Proxy, methodVersion, rpcblock.Latest, nil, []interface{}{versLatest}) + stubRpc.SetResponse(game0.Proxy, methodStatus, rpcblock.Latest, nil, []interface{}{expectedStatus}) + actual, err := factory.GetGameStatus(context.Background(), 0) + require.NoError(t, err) + require.Equal(t, expectedStatus, actual) + }) + } +} + func TestGetAllGames(t *testing.T) { for _, version := range factoryVersions { t.Run(version.String(), func(t *testing.T) { @@ -159,7 +181,7 @@ func TestGetAllGames(t *testing.T) { expectedGames := []gameTypes.GameMetadata{game0, game1, game2} stubRpc.SetResponse(factoryAddr, methodGameCount, rpcblock.ByHash(blockHash), nil, []interface{}{big.NewInt(int64(len(expectedGames)))}) for idx, expected := range expectedGames { - expectGetGame(stubRpc, idx, blockHash, expected) + expectGetGame(stubRpc, idx, rpcblock.ByHash(blockHash), expected) } actualGames, err := factory.GetAllGames(context.Background(), blockHash) require.NoError(t, err) @@ -201,7 +223,7 @@ func TestGetAllGamesAtOrAfter(t *testing.T) { stubRpc.SetResponse(factoryAddr, methodGameCount, rpcblock.ByHash(blockHash), nil, []interface{}{big.NewInt(int64(len(allGames)))}) for idx, expected := range allGames { - expectGetGame(stubRpc, idx, blockHash, expected) + expectGetGame(stubRpc, idx, rpcblock.ByHash(blockHash), expected) } // Set an earliest timestamp that's in the middle of a batch earliestTimestamp := uint64(test.earliestGameIdx) @@ -427,11 +449,11 @@ func TestDecodeDisputeGameCreatedLog(t *testing.T) { } } -func expectGetGame(stubRpc *batchingTest.AbiBasedRpc, idx int, blockHash common.Hash, game gameTypes.GameMetadata) { +func expectGetGame(stubRpc *batchingTest.AbiBasedRpc, idx int, block rpcblock.Block, game gameTypes.GameMetadata) { stubRpc.SetResponse( factoryAddr, methodGameAtIndex, - rpcblock.ByHash(blockHash), + block, []interface{}{big.NewInt(int64(idx))}, []interface{}{ game.GameType, diff --git a/op-challenger/game/fault/contracts/optimisticzkdisputegame.go b/op-challenger/game/fault/contracts/optimisticzkdisputegame.go index 0a497d99ed2..2a898daa57c 100644 --- a/op-challenger/game/fault/contracts/optimisticzkdisputegame.go +++ b/op-challenger/game/fault/contracts/optimisticzkdisputegame.go @@ -3,6 +3,7 @@ package contracts import ( "context" "fmt" + "math/big" "time" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts/metrics" @@ -14,8 +15,38 @@ import ( "github.com/ethereum/go-ethereum/common" ) +type ProposalStatus uint8 + +const ( + ProposalStatusUnchallenged ProposalStatus = iota + ProposalStatusChallenged + ProposalStatusUnchallengedAndValidProofProvided + ProposalStatusChallengedAndValidProofProvided + ProposalStatusResolved +) + +var ( + methodChallenge = "challenge" + methodChallengerBond = "challengerBond" + methodClaimData = "claimData" +) + +type claimData struct { + ParentIndex uint32 + CounteredBy common.Address + Prover common.Address + Claim common.Hash + Status ProposalStatus + Deadline uint64 +} + type OptimisticZKDisputeGameContract interface { DisputeGameContract + ChallengeTx(ctx context.Context) (txmgr.TxCandidate, error) + GetProposal(ctx context.Context) (common.Hash, uint64, error) + GetChallengerMetadata(ctx context.Context, block rpcblock.Block) (ChallengerMetadata, error) + GetCredit(ctx context.Context, recipient common.Address) (*big.Int, gameTypes.GameStatus, error) + ClaimCreditTx(ctx context.Context, recipient common.Address) (txmgr.TxCandidate, error) } type OptimisticZKDisputeGameContractLatest struct { @@ -24,6 +55,37 @@ type OptimisticZKDisputeGameContractLatest struct { contract *batching.BoundContract } +func (g *OptimisticZKDisputeGameContractLatest) GetCredit(ctx context.Context, recipient common.Address) (*big.Int, gameTypes.GameStatus, error) { + defer g.metrics.StartContractRequest("GetCredit")() + results, err := g.multiCaller.Call(ctx, rpcblock.Latest, + g.contract.Call(methodCredit, recipient), + g.contract.Call(methodStatus)) + if err != nil { + return nil, gameTypes.GameStatusInProgress, err + } + if len(results) != 2 { + return nil, gameTypes.GameStatusInProgress, fmt.Errorf("expected 2 results but got %v", len(results)) + } + credit := results[0].GetBigInt(0) + status, err := gameTypes.GameStatusFromUint8(results[1].GetUint8(0)) + if err != nil { + return nil, gameTypes.GameStatusInProgress, fmt.Errorf("invalid game status %v: %w", status, err) + } + return credit, status, nil +} + +func (g *OptimisticZKDisputeGameContractLatest) ClaimCreditTx(ctx context.Context, recipient common.Address) (txmgr.TxCandidate, error) { + defer g.metrics.StartContractRequest("ClaimCredit")() + call := g.contract.Call(methodClaimCredit, recipient) + _, err := g.multiCaller.SingleCall(ctx, rpcblock.Latest, call) + if err != nil { + return txmgr.TxCandidate{}, fmt.Errorf("%w: %w", ErrSimulationFailed, err) + } + return call.ToTxCandidate() +} + +var _ OptimisticZKDisputeGameContract = (*OptimisticZKDisputeGameContractLatest)(nil) + func NewOptimisticZKDisputeGameContract( m metrics.ContractMetricer, addr common.Address, @@ -37,6 +99,10 @@ func NewOptimisticZKDisputeGameContract( }, nil } +func (g *OptimisticZKDisputeGameContractLatest) Addr() common.Address { + return g.contract.Addr() +} + // GetMetadata returns the basic game metadata func (g *OptimisticZKDisputeGameContractLatest) GetMetadata(ctx context.Context, block rpcblock.Block) (GenericGameMetadata, error) { defer g.metrics.StartContractRequest("GetMetadata")() @@ -103,6 +169,61 @@ func (g *OptimisticZKDisputeGameContractLatest) GetGameRange(ctx context.Context return } +type ChallengerMetadata struct { + ParentIndex uint32 + ProposalStatus ProposalStatus + ProposedRoot common.Hash + L2SequenceNumber uint64 + Deadline time.Time +} + +func (g *OptimisticZKDisputeGameContractLatest) GetChallengerMetadata(ctx context.Context, block rpcblock.Block) (ChallengerMetadata, error) { + defer g.metrics.StartContractRequest("GetChallengerMetadata")() + results, err := g.multiCaller.Call(ctx, block, + g.contract.Call(methodClaimData), + g.contract.Call(methodL2SequenceNumber)) + if err != nil { + return ChallengerMetadata{}, fmt.Errorf("failed to retrieve challenger metadata: %w", err) + } + if len(results) != 2 { + return ChallengerMetadata{}, fmt.Errorf("expected 2 results but got %v", len(results)) + } + data := g.decodeClaimData(results[0]) + l2SeqNum := results[1].GetBigInt(0).Uint64() + return ChallengerMetadata{ + ParentIndex: data.ParentIndex, + ProposalStatus: data.Status, + ProposedRoot: data.Claim, + L2SequenceNumber: l2SeqNum, + Deadline: time.Unix(int64(data.Deadline), 0), + }, nil +} + +func (g *OptimisticZKDisputeGameContractLatest) ChallengeTx(ctx context.Context) (txmgr.TxCandidate, error) { + tx, err := g.contract.Call(methodChallenge).ToTxCandidate() + if err != nil { + return txmgr.TxCandidate{}, fmt.Errorf("failed to create challenge tx: %w", err) + } + + result, err := g.multiCaller.SingleCall(ctx, rpcblock.Latest, g.contract.Call(methodChallengerBond)) + if err != nil { + return txmgr.TxCandidate{}, fmt.Errorf("failed to retrieve challenger bond: %w", err) + } + tx.Value = result.GetBigInt(0) + return tx, nil +} + +func (g *OptimisticZKDisputeGameContractLatest) GetProposal(ctx context.Context) (common.Hash, uint64, error) { + results, err := g.multiCaller.Call(ctx, rpcblock.Latest, g.contract.Call(methodRootClaim), g.contract.Call(methodL2SequenceNumber)) + if err != nil { + return common.Hash{}, 0, fmt.Errorf("failed to retrieve proposal: %w", err) + } + if len(results) != 2 { + return common.Hash{}, 0, fmt.Errorf("expected 2 results but got %v", len(results)) + } + return results[0].GetHash(0), results[1].GetBigInt(0).Uint64(), nil +} + func (g *OptimisticZKDisputeGameContractLatest) GetResolvedAt(ctx context.Context, block rpcblock.Block) (time.Time, error) { defer g.metrics.StartContractRequest("GetResolvedAt")() result, err := g.multiCaller.SingleCall(ctx, block, g.contract.Call(methodResolvedAt)) @@ -132,4 +253,21 @@ func (g *OptimisticZKDisputeGameContractLatest) resolveCall() *batching.Contract return g.contract.Call(methodResolve) } +func (g *OptimisticZKDisputeGameContractLatest) decodeClaimData(result *batching.CallResult) claimData { + parentIndex := result.GetUint32(0) + counteredBy := result.GetAddress(1) + prover := result.GetAddress(2) + claim := result.GetHash(3) + status := result.GetUint8(4) + deadline := result.GetUint64(5) + return claimData{ + ParentIndex: parentIndex, + CounteredBy: counteredBy, + Prover: prover, + Claim: claim, + Status: ProposalStatus(status), + Deadline: deadline, + } +} + var _ DisputeGameContract = (*OptimisticZKDisputeGameContractLatest)(nil) diff --git a/op-challenger/game/fault/contracts/optimisticzkdisputegame_test.go b/op-challenger/game/fault/contracts/optimisticzkdisputegame_test.go index 9c9a393f2fc..0e994941cc8 100644 --- a/op-challenger/game/fault/contracts/optimisticzkdisputegame_test.go +++ b/op-challenger/game/fault/contracts/optimisticzkdisputegame_test.go @@ -2,6 +2,7 @@ package contracts import ( "context" + "errors" "math/big" "testing" "time" @@ -11,6 +12,7 @@ import ( "github.com/ethereum-optimism/optimism/op-service/sources/batching" "github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock" batchingTest "github.com/ethereum-optimism/optimism/op-service/sources/batching/test" + "github.com/ethereum-optimism/optimism/op-service/txmgr" "github.com/ethereum-optimism/optimism/packages/contracts-bedrock/snapshots" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" @@ -20,6 +22,10 @@ const ( versZKLatest = "0.0.0" ) +var ( + zkGameAddr = common.Address{0x45, 0x44, 0x43} +) + var zkVersions = []contractVersion{ { version: versZKLatest, @@ -82,7 +88,7 @@ func TestZKSimpleGetters(t *testing.T) { t.Skip("Skipping for this version") } stubRpc, game := setupZKDisputeGameTest(t, version) - stubRpc.SetResponse(fdgAddr, test.method, rpcblock.Latest, nil, []interface{}{test.result}) + stubRpc.SetResponse(zkGameAddr, test.method, rpcblock.Latest, nil, []interface{}{test.result}) status, err := test.call(game) require.NoError(t, err) expected := test.expected @@ -106,10 +112,10 @@ func TestZKGetMetadata(t *testing.T) { expectedRootClaim := common.Hash{0x01, 0x02} expectedStatus := gameTypes.GameStatusChallengerWon block := rpcblock.ByNumber(889) - stubRpc.SetResponse(fdgAddr, methodL1Head, block, nil, []interface{}{expectedL1Head}) - stubRpc.SetResponse(fdgAddr, methodL2SequenceNumber, block, nil, []interface{}{new(big.Int).SetUint64(expectedL2BlockNumber)}) - stubRpc.SetResponse(fdgAddr, methodRootClaim, block, nil, []interface{}{expectedRootClaim}) - stubRpc.SetResponse(fdgAddr, methodStatus, block, nil, []interface{}{expectedStatus}) + stubRpc.SetResponse(zkGameAddr, methodL1Head, block, nil, []interface{}{expectedL1Head}) + stubRpc.SetResponse(zkGameAddr, methodL2SequenceNumber, block, nil, []interface{}{new(big.Int).SetUint64(expectedL2BlockNumber)}) + stubRpc.SetResponse(zkGameAddr, methodRootClaim, block, nil, []interface{}{expectedRootClaim}) + stubRpc.SetResponse(zkGameAddr, methodStatus, block, nil, []interface{}{expectedStatus}) actual, err := contract.GetMetadata(context.Background(), block) expected := GenericGameMetadata{ L1Head: expectedL1Head, @@ -130,8 +136,8 @@ func TestZKGetGameRange(t *testing.T) { stubRpc, contract := setupZKDisputeGameTest(t, version) expectedStart := uint64(65) expectedEnd := uint64(102) - stubRpc.SetResponse(fdgAddr, methodStartingBlockNumber, rpcblock.Latest, nil, []interface{}{new(big.Int).SetUint64(expectedStart)}) - stubRpc.SetResponse(fdgAddr, methodL2SequenceNumber, rpcblock.Latest, nil, []interface{}{new(big.Int).SetUint64(expectedEnd)}) + stubRpc.SetResponse(zkGameAddr, methodStartingBlockNumber, rpcblock.Latest, nil, []interface{}{new(big.Int).SetUint64(expectedStart)}) + stubRpc.SetResponse(zkGameAddr, methodL2SequenceNumber, rpcblock.Latest, nil, []interface{}{new(big.Int).SetUint64(expectedEnd)}) start, end, err := contract.GetGameRange(context.Background()) require.NoError(t, err) require.Equal(t, expectedStart, start) @@ -145,7 +151,7 @@ func TestZKResolveTx(t *testing.T) { version := version t.Run(version.String(), func(t *testing.T) { stubRpc, game := setupZKDisputeGameTest(t, version) - stubRpc.SetResponse(fdgAddr, methodResolve, rpcblock.Latest, nil, nil) + stubRpc.SetResponse(zkGameAddr, methodResolve, rpcblock.Latest, nil, nil) tx, err := game.ResolveTx() require.NoError(t, err) stubRpc.VerifyTxCandidate(tx) @@ -153,21 +159,133 @@ func TestZKResolveTx(t *testing.T) { } } +func TestZKGetChallengerMetadata(t *testing.T) { + for _, version := range zkVersions { + version := version + t.Run(version.String(), func(t *testing.T) { + stubRpc, contract := setupZKDisputeGameTest(t, version) + expectedParentIndex := uint32(525) + expectedProposalStatus := ProposalStatusChallengedAndValidProofProvided + counteredBy := common.Address{0xad} + prover := common.Address{0xac} + expectedL2BlockNumber := uint64(123) + expectedRootClaim := common.Hash{0x01, 0x02} + expectedDeadline := time.Unix(84928429020, 0) + block := rpcblock.ByNumber(889) + stubRpc.SetResponse(zkGameAddr, methodClaimData, block, nil, []interface{}{ + expectedParentIndex, counteredBy, prover, expectedRootClaim, expectedProposalStatus, uint64(expectedDeadline.Unix()), + }) + stubRpc.SetResponse(zkGameAddr, methodL2SequenceNumber, block, nil, []interface{}{new(big.Int).SetUint64(expectedL2BlockNumber)}) + actual, err := contract.GetChallengerMetadata(context.Background(), block) + expected := ChallengerMetadata{ + ParentIndex: expectedParentIndex, + ProposalStatus: expectedProposalStatus, + ProposedRoot: expectedRootClaim, + L2SequenceNumber: expectedL2BlockNumber, + Deadline: expectedDeadline, + } + require.NoError(t, err) + require.Equal(t, expected, actual) + }) + } +} + +func TestZKChallengeTx(t *testing.T) { + for _, version := range zkVersions { + version := version + t.Run(version.String(), func(t *testing.T) { + bond := big.NewInt(97592472) + + stubRpc, game := setupZKDisputeGameTest(t, version) + stubRpc.SetResponse(zkGameAddr, methodChallengerBond, rpcblock.Latest, nil, []interface{}{bond}) + stubRpc.SetResponse(zkGameAddr, methodChallenge, rpcblock.Latest, nil, nil) + + tx, err := game.ChallengeTx(context.Background()) + require.NoError(t, err) + stubRpc.VerifyTxCandidate(tx) + }) + } +} + +func TestZKGetProposal(t *testing.T) { + for _, version := range zkVersions { + version := version + t.Run(version.String(), func(t *testing.T) { + rootClaim := common.Hash{0xaa} + l2SequenceNumber := big.NewInt(1236) + stubRpc, game := setupZKDisputeGameTest(t, version) + stubRpc.SetResponse(zkGameAddr, methodRootClaim, rpcblock.Latest, nil, []interface{}{rootClaim}) + stubRpc.SetResponse(zkGameAddr, methodL2SequenceNumber, rpcblock.Latest, nil, []interface{}{l2SequenceNumber}) + + actualClaim, actualSeqNum, err := game.GetProposal(context.Background()) + require.NoError(t, err) + require.Equal(t, rootClaim, actualClaim) + require.Equal(t, l2SequenceNumber.Uint64(), actualSeqNum) + }) + } +} + +func TestZKGame_GetCredit(t *testing.T) { + for _, version := range zkVersions { + version := version + t.Run(version.String(), func(t *testing.T) { + stubRpc, game := setupZKDisputeGameTest(t, version) + addr := common.Address{0x01} + expectedCredit := big.NewInt(4284) + expectedStatus := gameTypes.GameStatusChallengerWon + stubRpc.SetResponse(zkGameAddr, methodCredit, rpcblock.Latest, []interface{}{addr}, []interface{}{expectedCredit}) + stubRpc.SetResponse(zkGameAddr, methodStatus, rpcblock.Latest, nil, []interface{}{expectedStatus}) + + actualCredit, actualStatus, err := game.GetCredit(context.Background(), addr) + require.NoError(t, err) + require.Equal(t, expectedCredit, actualCredit) + require.Equal(t, expectedStatus, actualStatus) + }) + } +} + +func TestZKGame_ClaimCreditTx(t *testing.T) { + for _, version := range zkVersions { + version := version + t.Run(version.String(), func(t *testing.T) { + t.Run("Success", func(t *testing.T) { + stubRpc, game := setupZKDisputeGameTest(t, version) + addr := common.Address{0xaa} + + stubRpc.SetResponse(zkGameAddr, methodClaimCredit, rpcblock.Latest, []interface{}{addr}, nil) + tx, err := game.ClaimCreditTx(context.Background(), addr) + require.NoError(t, err) + stubRpc.VerifyTxCandidate(tx) + }) + + t.Run("SimulationFails", func(t *testing.T) { + stubRpc, game := setupZKDisputeGameTest(t, version) + addr := common.Address{0xaa} + + stubRpc.SetError(zkGameAddr, methodClaimCredit, rpcblock.Latest, []interface{}{addr}, errors.New("still locked")) + tx, err := game.ClaimCreditTx(context.Background(), addr) + require.ErrorIs(t, err, ErrSimulationFailed) + require.Equal(t, txmgr.TxCandidate{}, tx) + }) + }) + } +} + func setupZKDisputeGameTest(t *testing.T, version contractVersion) (*batchingTest.AbiBasedRpc, OptimisticZKDisputeGameContract) { fdgAbi := version.loadAbi() vmAbi := snapshots.LoadMIPSABI() oracleAbi := snapshots.LoadPreimageOracleABI() - stubRpc := batchingTest.NewAbiBasedRpc(t, fdgAddr, fdgAbi) + stubRpc := batchingTest.NewAbiBasedRpc(t, zkGameAddr, fdgAbi) stubRpc.AddContract(vmAddr, vmAbi) stubRpc.AddContract(oracleAddr, oracleAbi) caller := batching.NewMultiCaller(stubRpc, batching.DefaultBatchSize) - stubRpc.SetResponse(fdgAddr, methodGameType, rpcblock.Latest, nil, []interface{}{uint32(version.gameType)}) - stubRpc.SetResponse(fdgAddr, methodVersion, rpcblock.Latest, nil, []interface{}{version.version}) + stubRpc.SetResponse(zkGameAddr, methodGameType, rpcblock.Latest, nil, []interface{}{uint32(version.gameType)}) + stubRpc.SetResponse(zkGameAddr, methodVersion, rpcblock.Latest, nil, []interface{}{version.version}) stubRpc.SetResponse(oracleAddr, methodVersion, rpcblock.Latest, nil, []interface{}{oracleLatest}) - game, err := NewOptimisticZKDisputeGameContract(contractMetrics.NoopContractMetrics, fdgAddr, caller) + game, err := NewOptimisticZKDisputeGameContract(contractMetrics.NoopContractMetrics, zkGameAddr, caller) require.NoError(t, err) return stubRpc, game } diff --git a/op-challenger/game/service.go b/op-challenger/game/service.go index eb7be17c098..8188667f5ed 100644 --- a/op-challenger/game/service.go +++ b/op-challenger/game/service.go @@ -10,6 +10,7 @@ import ( challengerClient "github.com/ethereum-optimism/optimism/op-challenger/game/client" "github.com/ethereum-optimism/optimism/op-challenger/game/keccak" "github.com/ethereum-optimism/optimism/op-challenger/game/keccak/fetcher" + "github.com/ethereum-optimism/optimism/op-challenger/game/zk" "github.com/ethereum-optimism/optimism/op-challenger/sender" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" @@ -217,6 +218,10 @@ func (s *Service) registerGameTypes(ctx context.Context, cfg *config.Config) err if err != nil { return err } + err = zk.RegisterGameTypes(ctx, s.l1Clock, s.logger, s.metrics, cfg, gameTypeRegistry, s.txSender, s.clientProvider, s.factoryContract) + if err != nil { + return err + } s.registry = gameTypeRegistry s.oracles = oracles return nil diff --git a/op-challenger/game/types/game_type.go b/op-challenger/game/types/game_type.go index aec84d80aa0..9e9943d0575 100644 --- a/op-challenger/game/types/game_type.go +++ b/op-challenger/game/types/game_type.go @@ -21,7 +21,7 @@ const ( SuperAsteriscKonaGameType GameType = 7 CannonKonaGameType GameType = 8 SuperCannonKonaGameType GameType = 9 - OptimisticZKGameType GameType = 10 // Not (yet) supported by op-challenger + OptimisticZKGameType GameType = 10 FastGameType GameType = 254 AlphabetGameType GameType = 255 KailuaGameType GameType = 1337 // Not supported by op-challenger @@ -42,6 +42,7 @@ var SupportedGameTypes = []GameType{ SuperCannonKonaGameType, SuperPermissionedGameType, SuperAsteriscKonaGameType, + OptimisticZKGameType, } // Set implements the Set method required by the [cli.Generic] interface. diff --git a/op-challenger/game/zk/actor.go b/op-challenger/game/zk/actor.go new file mode 100644 index 00000000000..2a8ec7a9f63 --- /dev/null +++ b/op-challenger/game/zk/actor.go @@ -0,0 +1,172 @@ +package zk + +import ( + "context" + "errors" + "fmt" + "math" + "strings" + + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts" + "github.com/ethereum-optimism/optimism/op-challenger/game/generic" + gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock" + "github.com/ethereum-optimism/optimism/op-service/txmgr" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rpc" +) + +var ( + errNoChallengeRequired = errors.New("no challenge required") + errNoResolutionRequired = errors.New("no resolution required") +) + +type RootProvider interface { + OutputAtBlock(ctx context.Context, blockNum uint64) (*eth.OutputResponse, error) +} + +type GameStatusProvider interface { + GetGameStatus(ctx context.Context, idx uint64) (gameTypes.GameStatus, error) +} + +type ChallengableContract interface { + Addr() common.Address + ChallengeTx(ctx context.Context) (txmgr.TxCandidate, error) + GetProposal(ctx context.Context) (common.Hash, uint64, error) + GetChallengerMetadata(ctx context.Context, block rpcblock.Block) (contracts.ChallengerMetadata, error) + ResolveTx() (txmgr.TxCandidate, error) +} + +type Actor struct { + logger log.Logger + l1Clock ClockReader + rootProvider RootProvider + gameStatusProvider GameStatusProvider + contract ChallengableContract + txSender TxSender + l1Head eth.BlockID +} + +func ActorCreator(l1Clock ClockReader, rootProvider RootProvider, gameStatusProvider GameStatusProvider, contract ChallengableContract, txSender TxSender) generic.ActorCreator { + return func(ctx context.Context, logger log.Logger, l1Head eth.BlockID) (generic.Actor, error) { + return &Actor{ + logger: logger, + l1Clock: l1Clock, + rootProvider: rootProvider, + gameStatusProvider: gameStatusProvider, + contract: contract, + txSender: txSender, + l1Head: l1Head, + }, nil + } +} + +func (a *Actor) Act(ctx context.Context) error { + gameState, err := a.contract.GetChallengerMetadata(ctx, rpcblock.Latest) + if err != nil { + return fmt.Errorf("failed to get zk game state: %w", err) + } + + var txs []txmgr.TxCandidate + if tx, err := a.createChallengeTx(ctx, gameState); errors.Is(err, errNoChallengeRequired) { + a.logger.Debug("No challenge required") + } else if err != nil { + return err + } else { + txs = append(txs, tx) + } + if tx, err := a.createResolveTx(ctx, gameState); errors.Is(err, errNoResolutionRequired) { + a.logger.Debug("No resolution required") + } else if err != nil { + return err + } else { + txs = append(txs, tx) + } + + if len(txs) == 0 { + return nil + } + if err := a.txSender.SendAndWaitSimple(fmt.Sprintf("respond to game %v", a.contract.Addr()), txs...); err != nil { + return fmt.Errorf("failed to send transactions for game %v: %w", a.contract.Addr(), err) + } + return nil +} + +func (a *Actor) createChallengeTx(ctx context.Context, gameState contracts.ChallengerMetadata) (txmgr.TxCandidate, error) { + if gameState.ProposalStatus != contracts.ProposalStatusUnchallenged || gameState.Deadline.Before(a.l1Clock.Now()) { + a.logger.Trace("Skipping unchallengeable zk game") + return txmgr.TxCandidate{}, errNoChallengeRequired + } + if valid, err := a.isValidProposal(ctx); err != nil { + return txmgr.TxCandidate{}, fmt.Errorf("failed to check if proposal is valid: %w", err) + } else if valid { + a.logger.Trace("Not challenging valid zk game") + return txmgr.TxCandidate{}, errNoChallengeRequired + } + + a.logger.Info("Challenging game") + return a.contract.ChallengeTx(ctx) +} + +func (a *Actor) isValidProposal(ctx context.Context) (bool, error) { + proposalHash, proposalSeqNum, err := a.contract.GetProposal(ctx) + if err != nil { + return false, fmt.Errorf("failed to get zk game proposal: %w", err) + } + canonicalOutput, err := a.rootProvider.OutputAtBlock(ctx, proposalSeqNum) + if err != nil { + var rpcErr rpc.Error + if errors.As(err, &rpcErr) { + if strings.Contains(strings.ToLower(rpcErr.Error()), "not found") { + // There is no valid output at the proposal sequence number (it's in the future) + return false, nil + } + } + return false, fmt.Errorf("failed to get canonical output at block %v: %w", proposalSeqNum, err) + } + if common.Hash(canonicalOutput.OutputRoot) != proposalHash { + // Output root doesn't match so can't be valid + return false, nil + } + return true, nil +} + +func (a *Actor) createResolveTx(ctx context.Context, gameState contracts.ChallengerMetadata) (txmgr.TxCandidate, error) { + if gameState.ProposalStatus == contracts.ProposalStatusResolved { + a.logger.Trace("Skipping resolution of resolved zk game") + return txmgr.TxCandidate{}, errNoResolutionRequired + } + deadlineExpired := gameState.Deadline.Before(a.l1Clock.Now()) + + if gameState.ParentIndex != math.MaxUint32 { + parentStatus, err := a.gameStatusProvider.GetGameStatus(ctx, uint64(gameState.ParentIndex)) + if err != nil { + return txmgr.TxCandidate{}, fmt.Errorf("failed to get parent game status: %w", err) + } + if parentStatus == gameTypes.GameStatusInProgress { + a.logger.Trace("Skipping resolution of zk game with parent in progress") + return txmgr.TxCandidate{}, errNoResolutionRequired + } + if parentStatus == gameTypes.GameStatusChallengerWon { + // Resolve if the parent game is invalid + return a.contract.ResolveTx() + } + } + + if gameState.ProposalStatus == contracts.ProposalStatusChallengedAndValidProofProvided || + gameState.ProposalStatus == contracts.ProposalStatusUnchallengedAndValidProofProvided { + // Resolve if a valid proof is provided + return a.contract.ResolveTx() + } + if deadlineExpired { + // Resolve if the deadline has expired (either for challenging or proving) + return a.contract.ResolveTx() + } + return txmgr.TxCandidate{}, errNoResolutionRequired +} + +func (a *Actor) AdditionalStatus(_ context.Context) ([]any, error) { + return nil, nil +} diff --git a/op-challenger/game/zk/actor_test.go b/op-challenger/game/zk/actor_test.go new file mode 100644 index 00000000000..ac141d4e026 --- /dev/null +++ b/op-challenger/game/zk/actor_test.go @@ -0,0 +1,354 @@ +package zk + +import ( + "context" + "errors" + "math" + "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts" + "github.com/ethereum-optimism/optimism/op-challenger/game/types" + "github.com/ethereum-optimism/optimism/op-service/clock" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock" + "github.com/ethereum-optimism/optimism/op-service/testlog" + "github.com/ethereum-optimism/optimism/op-service/txmgr" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rpc" + "github.com/stretchr/testify/require" +) + +var ( + challengeData = "challenge" + resolveData = "resolve" + l1Time = time.Unix(9892842, 0) +) + +type zkTestStubs struct { + rootProvider *stubRootProvider + contract *stubContract + sender *stubTxSender +} + +func TestActor(t *testing.T) { + // Output root: Valid, Invalid + // Safety: Safe, Unsafe, Beyond unsafe + // In challenge period, ChallengePeriodExpired, In proof period, ProvenWithoutChallenge, ProvenAfterChallenge, ProofPeriodExpired, Resolved + // No parent, parent in progress, parent valid, parent invalid + tests := []struct { + name string + setup func(t *testing.T, stubs *zkTestStubs) + challenge bool + resolve bool + }{ + { + name: "DoNotChallengeCorrectProposal", + setup: func(t *testing.T, stubs *zkTestStubs) { + stubs.contract.setDeadlineNotReached() + stubs.contract.proposalHash = stubs.rootProvider.root + stubs.contract.l2SequenceNumber = stubs.rootProvider.rootBlockNum + }, + }, + { + name: "ChallengeIncorrectProposal", + setup: func(t *testing.T, stubs *zkTestStubs) { + stubs.contract.proposalHash = common.Hash{0xba, 0xd0} + }, + challenge: true, + }, + { + name: "DoNothingIfAlreadyChallenged", + setup: func(t *testing.T, stubs *zkTestStubs) { + stubs.rootProvider.root = common.Hash{0xba, 0xd0} // Disagree but already challenged + stubs.contract.challenge(t) + }, + }, + { + name: "ChallengeProposalBeyondCurrentUnsafeHead", + setup: func(t *testing.T, stubs *zkTestStubs) { + stubs.rootProvider.root = common.Hash{0xba, 0xd0} + stubs.rootProvider.outputErr = mockNotFoundRPCError() + stubs.contract.proposalHash = stubs.rootProvider.root + stubs.contract.l2SequenceNumber = stubs.rootProvider.rootBlockNum + }, + challenge: true, + }, + { + name: "ChallengeUnresolvableGameWithNoParent", + setup: func(t *testing.T, stubs *zkTestStubs) { + stubs.contract.proposalHash = common.Hash{0xba, 0xd0} + stubs.contract.parentIndex = math.MaxUint32 + }, + challenge: true, + }, + { + name: "ResolveGameWithNoParent", + setup: func(t *testing.T, stubs *zkTestStubs) { + stubs.contract.setDeadlineExpired() + stubs.contract.proposalHash = common.Hash{0xba, 0xd0} + stubs.contract.parentIndex = math.MaxUint32 + }, + resolve: true, + }, + { + name: "DoNothingWhenDeadlineExpiredButParentNotResolved", + setup: func(t *testing.T, stubs *zkTestStubs) { + stubs.contract.setDeadlineExpired() + // Proposal is invalid but can't challenge because the deadline is expired + stubs.contract.proposalHash = common.Hash{0xba, 0xd0} + // And can't resolve because the parent is still unresolved + stubs.contract.setParentStatus(types.GameStatusInProgress) + }, + }, + { + name: "InChallengePeriodWithInvalidParent", + setup: func(t *testing.T, stubs *zkTestStubs) { + // Game should be challenged + stubs.contract.proposalHash = common.Hash{0xba, 0xd0} + stubs.contract.setDeadlineNotReached() + // And is immediately resolvable because the parent is invalid + stubs.contract.setParentStatus(types.GameStatusChallengerWon) + }, + challenge: true, + resolve: true, + }, + { + name: "UnchallengedWithDeadlineExpired", + setup: func(t *testing.T, stubs *zkTestStubs) { + stubs.contract.setDeadlineExpired() + }, + resolve: true, + }, + { + name: "ChallengedWithDeadlineExpired", + setup: func(t *testing.T, stubs *zkTestStubs) { + stubs.contract.setDeadlineExpired() + stubs.contract.challenge(t) + }, + resolve: true, + }, + { + name: "ChallengedAndProvenWithDeadlineExpired", + setup: func(t *testing.T, stubs *zkTestStubs) { + stubs.contract.setDeadlineExpired() + stubs.contract.challenge(t) + stubs.contract.prove(t) + }, + resolve: true, + }, + { + name: "ChallengedAndProvenWithDeadlineNotReached", + setup: func(t *testing.T, stubs *zkTestStubs) { + stubs.contract.setDeadlineNotReached() + stubs.contract.challenge(t) + stubs.contract.prove(t) + }, + resolve: true, + }, + { + name: "UnchallengedAndProvenWithDeadlineExpired", + setup: func(t *testing.T, stubs *zkTestStubs) { + stubs.contract.setDeadlineExpired() + stubs.contract.prove(t) + }, + resolve: true, + }, + { + name: "UnchallengedAndProvenWithDeadlineNotReached", + setup: func(t *testing.T, stubs *zkTestStubs) { + stubs.contract.setDeadlineNotReached() + stubs.contract.prove(t) + }, + resolve: true, + }, + { + name: "AlreadyResolved", + setup: func(t *testing.T, stubs *zkTestStubs) { + stubs.contract.setDeadlineNotReached() + stubs.contract.markResolved() + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actor, stubs := setupActorTest(t) + if tt.setup != nil { + tt.setup(t, stubs) + } + err := actor.Act(context.Background()) + require.NoError(t, err) + expectedTxCount := 0 + if tt.challenge { + require.Contains(t, stubs.sender.sentData, challengeData) + expectedTxCount++ + } + if tt.resolve { + require.Contains(t, stubs.sender.sentData, resolveData) + expectedTxCount++ + } + require.Len(t, stubs.sender.sentData, expectedTxCount) + }) + } +} + +func setupActorTest(t *testing.T) (*Actor, *zkTestStubs) { + logger := testlog.Logger(t, log.LvlInfo) + l1Head := eth.BlockID{ + Hash: common.Hash{0x12}, + Number: 785, + } + rootBlockNum := uint64(28492) + rootProvider := &stubRootProvider{ + root: common.Hash{0x11}, + rootBlockNum: rootBlockNum, + } + // Default to a valid proposal + contract := &stubContract{ + proposalHash: rootProvider.root, + l2SequenceNumber: rootProvider.rootBlockNum, + parentStatus: types.GameStatusDefenderWon, + parentIndex: 482, + } + contract.setDeadlineNotReached() + txSender := &stubTxSender{} + l1Clock := clock.NewDeterministicClock(l1Time) + // Simplify the tests by using the same stub for the game and the dispute game factory + creator := ActorCreator(l1Clock, rootProvider, contract, contract, txSender) + genericActor, err := creator(context.Background(), logger, l1Head) + require.NoError(t, err, "failed to create actor") + actor, ok := genericActor.(*Actor) + require.True(t, ok, "actor is not of expected type") + return actor, &zkTestStubs{ + rootProvider: rootProvider, + contract: contract, + sender: txSender, + } +} + +type stubRootProvider struct { + outputErr error + rootBlockNum uint64 + root common.Hash +} + +func (s *stubRootProvider) OutputAtBlock(_ context.Context, blockNum uint64) (*eth.OutputResponse, error) { + if s.outputErr != nil { + return nil, s.outputErr + } + if blockNum != s.rootBlockNum { + return nil, errors.New("unexpected output request") + } + return ð.OutputResponse{ + OutputRoot: eth.Bytes32(s.root), + }, nil +} + +type stubContract struct { + parentIndex uint32 + parentStatus types.GameStatus + proposalStatus contracts.ProposalStatus + deadline time.Time + txCreated bool + proposalHash common.Hash + l2SequenceNumber uint64 +} + +func (s *stubContract) Addr() common.Address { + return common.Address{0x67, 0x67, 0x67} +} + +func (s *stubContract) challenge(t *testing.T) { + require.Equal(t, contracts.ProposalStatusUnchallenged, s.proposalStatus, "game not in challengable state") + s.proposalStatus = contracts.ProposalStatusChallenged +} + +func (s *stubContract) prove(t *testing.T) { + if s.proposalStatus == contracts.ProposalStatusUnchallenged { + s.proposalStatus = contracts.ProposalStatusUnchallengedAndValidProofProvided + return + } + require.Equal(t, contracts.ProposalStatusChallenged, s.proposalStatus, "game not in provable state") + s.proposalStatus = contracts.ProposalStatusChallengedAndValidProofProvided +} + +func (s *stubContract) setDeadlineExpired() { + s.deadline = l1Time.Add(-1 * time.Second) +} + +func (s *stubContract) setDeadlineNotReached() { + s.deadline = l1Time.Add(1 * time.Second) +} + +func (s *stubContract) markResolved() { + s.proposalStatus = contracts.ProposalStatusResolved +} + +func (s *stubContract) setParentStatus(status types.GameStatus) { + s.parentStatus = status +} + +func (s *stubContract) GetGameStatus(_ context.Context, idx uint64) (types.GameStatus, error) { + if idx != uint64(s.parentIndex) { + return 0, errors.New("unexpected parent index") + } + if idx == math.MaxUint32 { + return 0, errors.New("execution reverted") // no such game + } + return s.parentStatus, nil +} + +func (s *stubContract) GetChallengerMetadata(_ context.Context, _ rpcblock.Block) (contracts.ChallengerMetadata, error) { + return contracts.ChallengerMetadata{ + ParentIndex: s.parentIndex, + ProposalStatus: s.proposalStatus, + ProposedRoot: s.proposalHash, + L2SequenceNumber: s.l2SequenceNumber, + Deadline: s.deadline, + }, nil +} + +func (s *stubContract) ChallengeTx(_ context.Context) (txmgr.TxCandidate, error) { + s.txCreated = true + return txmgr.TxCandidate{ + TxData: []byte(challengeData), + }, nil +} + +func (s *stubContract) ResolveTx() (txmgr.TxCandidate, error) { + return txmgr.TxCandidate{ + TxData: []byte(resolveData), + }, nil +} + +func (s *stubContract) GetProposal(_ context.Context) (common.Hash, uint64, error) { + return s.proposalHash, s.l2SequenceNumber, nil +} + +type stubTxSender struct { + sentData []string + sendErr error +} + +func (s *stubTxSender) SendAndWaitSimple(_ string, candidates ...txmgr.TxCandidate) error { + for _, candidate := range candidates { + s.sentData = append(s.sentData, string(candidate.TxData)) + } + if s.sendErr != nil { + return s.sendErr + } + return nil +} + +// mockNotFoundRPCError creates a minimal rpc.Error that reports a "not found" message +// to exercise the JSON-RPC application error path in the enricher. +func mockNotFoundRPCError() rpc.Error { return testRPCError{msg: "not found", code: -32000} } + +type testRPCError struct { + msg string + code int +} + +func (e testRPCError) Error() string { return e.msg } +func (e testRPCError) ErrorCode() int { return e.code } diff --git a/op-challenger/game/zk/register.go b/op-challenger/game/zk/register.go new file mode 100644 index 00000000000..0676472e779 --- /dev/null +++ b/op-challenger/game/zk/register.go @@ -0,0 +1,70 @@ +package zk + +import ( + "context" + "fmt" + "time" + + "github.com/ethereum-optimism/optimism/op-challenger/config" + "github.com/ethereum-optimism/optimism/op-challenger/game/client" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/claims" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts" + "github.com/ethereum-optimism/optimism/op-challenger/game/generic" + "github.com/ethereum-optimism/optimism/op-challenger/game/scheduler" + gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types" + "github.com/ethereum-optimism/optimism/op-challenger/metrics" + "github.com/ethereum-optimism/optimism/op-service/txmgr" + "github.com/ethereum/go-ethereum/log" +) + +type ClockReader interface { + Now() time.Time +} + +type Registry interface { + RegisterGameType(gameType gameTypes.GameType, creator scheduler.PlayerCreator) + RegisterBondContract(gameType gameTypes.GameType, creator claims.BondContractCreator) +} + +type TxSender interface { + SendAndWaitSimple(txPurpose string, txs ...txmgr.TxCandidate) error +} + +func RegisterGameTypes( + ctx context.Context, + l1Clock ClockReader, + logger log.Logger, + m metrics.Metricer, + cfg *config.Config, + registry Registry, + txSender TxSender, + clients *client.Provider, + gameStatusProvider GameStatusProvider, +) error { + if cfg.GameTypeEnabled(gameTypes.OptimisticZKGameType) { + registry.RegisterGameType(gameTypes.OptimisticZKGameType, func(game gameTypes.GameMetadata, dir string) (scheduler.GamePlayer, error) { + rollupClient, syncValidator, err := clients.RollupClients() + if err != nil { + return nil, fmt.Errorf("failed to create rollup clients: %w", err) + } + contract, err := contracts.NewOptimisticZKDisputeGameContract(m, game.Proxy, clients.MultiCaller()) + if err != nil { + return nil, fmt.Errorf("failed to create optimistic zk dispute game bindings: %w", err) + } + return generic.NewGenericGamePlayer( + ctx, + logger, + game.Proxy, + contract, + syncValidator, + nil, + clients.L1Client(), + ActorCreator(l1Clock, rollupClient, gameStatusProvider, contract, txSender), + ) + }) + registry.RegisterBondContract(gameTypes.OptimisticZKGameType, func(game gameTypes.GameMetadata) (claims.BondContract, error) { + return contracts.NewOptimisticZKDisputeGameContract(m, game.Proxy, clients.MultiCaller()) + }) + } + return nil +} diff --git a/op-conductor/rpc/ws/flashblocks_handler.go b/op-conductor/rpc/ws/flashblocks_handler.go index c58a86bfb2f..b4b159c5eb2 100644 --- a/op-conductor/rpc/ws/flashblocks_handler.go +++ b/op-conductor/rpc/ws/flashblocks_handler.go @@ -10,6 +10,7 @@ import ( "github.com/coder/websocket" "github.com/ethereum-optimism/optimism/op-conductor/metrics" + opclient "github.com/ethereum-optimism/optimism/op-service/client" "github.com/ethereum-optimism/optimism/op-service/httputil" "github.com/ethereum-optimism/optimism/op-service/retry" "github.com/ethereum/go-ethereum/log" @@ -54,7 +55,7 @@ type Handler struct { log log.Logger isLeaderFn func(context.Context) bool metrics metrics.Metricer - rollupBoostConn *websocket.Conn + rollupBoostConn *opclient.WSClient rollupBoostCtx context.Context rollupBoostWsCancel context.CancelFunc httpServer *httputil.HTTPServer @@ -83,18 +84,14 @@ func NewHandler(cfg Config, log log.Logger, isLeaderFn func(context.Context) boo // Try to establish initial connection to rollup boost WebSocket maxConnectionAttempts := 5 - var err error - handler.rollupBoostConn, err = retry.Do(context.Background(), maxConnectionAttempts, retry.Fixed(reconnectDelay), func() (*websocket.Conn, error) { - log.Info("attempting to connect to rollup boost WebSocket", "url", cfg.RollupBoostWsURL) - dialCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - conn, resp, err := websocket.Dial(dialCtx, cfg.RollupBoostWsURL, nil) - if resp != nil && resp.Body != nil { - resp.Body.Close() - } - return conn, err + conn, err := opclient.DialWS(context.Background(), opclient.WSConfig{ + URL: cfg.RollupBoostWsURL, + DialTimeout: 5 * time.Second, + MaxAttempts: maxConnectionAttempts, + Backoff: retry.Fixed(reconnectDelay), + Log: log, }) - + handler.rollupBoostConn = conn if err != nil { return nil, fmt.Errorf("failed to connect to rollup boost WebSocket: %w", err) } @@ -204,13 +201,12 @@ func (h *Handler) listenToRollupBoost(ctx context.Context) { h.log.Info("reconnecting to rollup boost WebSocket", "url", h.cfg.RollupBoostWsURL) // Connect with timeout - dialCtx, cancel := context.WithTimeout(ctx, 5*time.Second) - defer cancel() - conn, resp, err := websocket.Dial(dialCtx, h.cfg.RollupBoostWsURL, nil) - if resp != nil && resp.Body != nil { - resp.Body.Close() - } - + conn, err := opclient.DialWS(ctx, opclient.WSConfig{ + URL: h.cfg.RollupBoostWsURL, + DialTimeout: 5 * time.Second, + MaxAttempts: 1, + Log: h.log, + }) if err != nil { h.log.Warn("failed to connect to rollup boost WebSocket, will retry", "err", err, "retryIn", reconnectDelay) @@ -227,8 +223,8 @@ func (h *Handler) listenToRollupBoost(ctx context.Context) { // Read with timeout readCtx, cancel := context.WithTimeout(ctx, 30*time.Second) - defer cancel() _, message, err := h.rollupBoostConn.Read(readCtx) + cancel() if err != nil { h.log.Warn("error reading from rollup boost WebSocket", "err", err) diff --git a/op-deployer/pkg/deployer/devfeatures.go b/op-deployer/pkg/deployer/devfeatures.go index 6a8867d43a2..fefd390e06b 100644 --- a/op-deployer/pkg/deployer/devfeatures.go +++ b/op-deployer/pkg/deployer/devfeatures.go @@ -17,9 +17,6 @@ var ( // DeployV2DisputeGamesDevFlag enables deployment of V2 dispute game contracts. DeployV2DisputeGamesDevFlag = common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000100") - - // CustomGasTokenDevFlag enables the custom gas token. - CustomGasTokenDevFlag = common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000001000") ) // IsDevFeatureEnabled checks if a specific development feature is enabled in a feature bitmap. diff --git a/op-deployer/pkg/deployer/integration_test/apply_test.go b/op-deployer/pkg/deployer/integration_test/apply_test.go index bd5dc5c88af..b028b09f82e 100644 --- a/op-deployer/pkg/deployer/integration_test/apply_test.go +++ b/op-deployer/pkg/deployer/integration_test/apply_test.go @@ -321,10 +321,6 @@ func TestEndToEndApply(t *testing.T) { Symbol: "CGT", InitialLiquidity: (*hexutil.Big)(amount), } - // CGT config for OPCM - intent.GlobalDeployOverrides = map[string]interface{}{ - "devFeatureBitmap": deployer.CustomGasTokenDevFlag, - } require.NoError(t, deployer.ApplyPipeline(ctx, deployer.ApplyPipelineOpts{ DeploymentTarget: deployer.DeploymentTargetLive, diff --git a/op-devstack/dsl/el.go b/op-devstack/dsl/el.go index 5d5856a67eb..d0245b11ba0 100644 --- a/op-devstack/dsl/el.go +++ b/op-devstack/dsl/el.go @@ -38,7 +38,7 @@ func (el *elNode) WaitForBlock() eth.BlockRef { func (el *elNode) WaitForLabel(label eth.BlockLabel, predicate func(eth.BlockInfo) (bool, error)) eth.BlockInfo { var block eth.BlockInfo - err := wait.For(el.ctx, 500*time.Millisecond, func() (bool, error) { + err := wait.For(el.ctx, 200*time.Millisecond, func() (bool, error) { var err error block, err = el.inner.EthClient().InfoByLabel(el.ctx, label) if err != nil { diff --git a/op-devstack/dsl/l2_network.go b/op-devstack/dsl/l2_network.go index dfb6ef0a83b..d2ae3f694a0 100644 --- a/op-devstack/dsl/l2_network.go +++ b/op-devstack/dsl/l2_network.go @@ -1,7 +1,9 @@ package dsl import ( + "bytes" "fmt" + "io" "math" "time" @@ -12,9 +14,11 @@ import ( "github.com/ethereum-optimism/optimism/op-devstack/stack/match" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait" "github.com/ethereum-optimism/optimism/op-node/rollup" + "github.com/ethereum-optimism/optimism/op-node/rollup/derive" "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/retry" "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" ) // L2Network wraps a stack.L2Network interface for DSL operations @@ -261,3 +265,262 @@ func (n *L2Network) DisputeGameFactoryProxyAddr() common.Address { func (n *L2Network) DepositContractAddr() common.Address { return n.inner.RollupConfig().DepositContractAddress } + +func (n *L2Network) DeriveData(blocks int) (channels []derive.ChannelID, channelFrames map[derive.ChannelID][]derive.Frame, l2Txs map[common.Address][]*ethtypes.Transaction) { + l := n.log + ctx := n.ctx + + channelFrames = make(map[derive.ChannelID][]derive.Frame) + channels = make([]derive.ChannelID, 0) + l2Txs = make(map[common.Address][]*ethtypes.Transaction) + + rollupCfg := n.inner.RollupConfig() + batchInboxAddr := rollupCfg.BatchInboxAddress + + l1EC := n.inner.L1().L1ELNode(match.FirstL1EL).EthClient() + + // Get current L1 block number before starting to monitor + startBlockRef, err := l1EC.BlockRefByLabel(ctx, eth.Unsafe) + n.require.NoError(err, "Failed to get start block number") + + seenChannels := make(map[derive.ChannelID]bool) + lastBlockRef := startBlockRef + + // Monitor L1 blocks for batch transactions + for range blocks { + NewL1ELNode(n.inner.L1().L1ELNode(match.FirstL1EL)).WaitForBlock() + + // Get current block number + currentBlockRef, err := l1EC.BlockRefByLabel(ctx, eth.Unsafe) + n.require.NoError(err, "Failed to get current block number") + blockNum := currentBlockRef.Number + lastBlockRef = currentBlockRef + + _, txs, err := l1EC.InfoAndTxsByNumber(ctx, blockNum) + n.require.NoError(err, "Failed to get block %d", blockNum) + + // Process transactions in this block + for _, tx := range txs { + // Check if transaction is targeted to BatchInbox + if tx.To() != nil && *tx.To() == batchInboxAddr { + // Get transaction sender + chainID := n.inner.L1().ChainID() + chainIDBig := chainID.ToBig() + signer := ethtypes.LatestSignerForChainID(chainIDBig) + sender, err := signer.Sender(tx) + n.require.NoError(err, "Failed to get transaction sender") + + l.Debug("Found batch transaction", + "txHash", tx.Hash(), + "block", blockNum, + "sender", sender) + + var datas [][]byte + if tx.Type() != ethtypes.BlobTxType { + // Regular transaction - data is in tx.Data() + datas = append(datas, tx.Data()) + } else { + // Blob transaction - need to fetch blobs from beacon + // For now, log that we found a blob tx but skip detailed parsing + // as it requires beacon API access + l.Error("Found blob transaction (skipping blob fetch for now)", + "txHash", tx.Hash(), + "blobHashes", tx.BlobHashes()) + continue + } + + // Parse frames from transaction data + for _, data := range datas { + frames, err := derive.ParseFrames(data) + if err != nil { + l.Warn("Failed to parse frames from transaction", + "txHash", tx.Hash(), + "error", err) + n.require.NoError(err) + } + + l.Debug("Parsed frames from transaction", + "txHash", tx.Hash(), + "frameCount", len(frames)) + + // Process each frame + for _, frame := range frames { + channelID := frame.ID + if !seenChannels[channelID] { + seenChannels[channelID] = true + l.Debug("Found new channel", + "channelID", channelID.String(), + "txHash", tx.Hash(), + "block", blockNum) + channels = append(channels, channelID) + } + channelFrames[channelID] = append(channelFrames[channelID], frame) + l.Debug("Frame added to channel", + "channelID", channelID.String(), + "frameNumber", frame.FrameNumber, + "dataLength", len(frame.Data), + "isLast", frame.IsLast, + "txHash", tx.Hash()) + } + } + } + } + } + + // Reassemble channels and extract batches + for channelID, frames := range channelFrames { + l.Debug("Processing channel", + "channelID", channelID.String(), + "frameCount", len(frames)) + + // Sort frames by frame number + sortedFrames := make([]derive.Frame, len(frames)) + copy(sortedFrames, frames) + for i := 0; i < len(sortedFrames); i++ { + for j := i + 1; j < len(sortedFrames); j++ { + if sortedFrames[i].FrameNumber > sortedFrames[j].FrameNumber { + sortedFrames[i], sortedFrames[j] = sortedFrames[j], sortedFrames[i] + } + } + } + + // Create a channel and add frames to it + // We need an L1 block ref for the channel - use the last processed block as the origin + originBlock := lastBlockRef + ch := derive.NewChannel(channelID, originBlock, false) + + for _, frame := range sortedFrames { + err := ch.AddFrame(frame, originBlock) + if err != nil { + l.Warn("Failed to add frame to channel", + "channelID", channelID.String(), + "frameNumber", frame.FrameNumber, + "error", err) + continue + } + } + + l.Debug("Channel is ready, extracting batches", + "channelID", channelID.String(), + "size", ch.Size()) + + channelReader := ch.Reader() + channelData, err := io.ReadAll(channelReader) + if err != nil { + l.Warn("Failed to read channel data", + "channelID", channelID.String(), + "error", err) + continue + } + + l.Debug("Read channel data", + "channelID", channelID.String(), + "dataLength", len(channelData)) + + spec := rollup.NewChainSpec(rollupCfg) + maxRLPBytes := spec.MaxRLPBytesPerChannel(originBlock.Time) + isFjord := rollupCfg.IsFjord(originBlock.Time) + batchReader, err := derive.BatchReader(bytes.NewReader(channelData), maxRLPBytes, isFjord) + if err != nil { + l.Warn("Failed to create batch reader", + "channelID", channelID.String(), + "error", err) + continue + } + + // Read all batches from the channel + batchCount := 0 + for { + batchData, err := batchReader() + if err == io.EOF { + break + } + if err != nil { + l.Warn("Failed to read batch from channel", + "channelID", channelID.String(), + "batchCount", batchCount, + "error", err) + break + } + + batchCount++ + batchType := batchData.GetBatchType() + + l.Debug("Found batch in channel", + "channelID", channelID.String(), + "batchNumber", batchCount, + "batchType", batchType, + "compressionAlgo", batchData.ComprAlgo) + + // Decode the batch based on type + if batchType == derive.SingularBatchType { + singularBatch, err := derive.GetSingularBatch(batchData) + if err != nil { + l.Warn("Failed to decode singular batch", + "channelID", channelID.String(), + "batchNumber", batchCount, + "error", err) + n.require.NoError(err) + } + + for _, txData := range singularBatch.Transactions { + var tx ethtypes.Transaction + n.require.NoError(tx.UnmarshalBinary(txData)) + + signer := ethtypes.LatestSignerForChainID(rollupCfg.L2ChainID) + fromAddr, err := signer.Sender(&tx) + n.require.NoError(err) + + l2Txs[fromAddr] = append(l2Txs[fromAddr], &tx) + } + + } else if batchType == derive.SpanBatchType { + spanBatch, err := derive.DeriveSpanBatch( + batchData, + rollupCfg.BlockTime, + rollupCfg.Genesis.L2Time, + rollupCfg.L2ChainID, + ) + if err != nil { + l.Warn("Failed to decode span batch", + "channelID", channelID.String(), + "batchNumber", batchCount, + "error", err) + continue + } + + for blockIdx, batchElement := range spanBatch.Batches { + l.Debug("L2 block in span batch", + "channelID", channelID.String(), + "batchNumber", batchCount, + "blockIndex", blockIdx, + "epochNum", batchElement.EpochNum, + "timestamp", batchElement.Timestamp, + "txCount", len(batchElement.Transactions)) + + for _, txData := range batchElement.Transactions { + var tx ethtypes.Transaction + n.require.NoError(tx.UnmarshalBinary(txData)) + + signer := ethtypes.LatestSignerForChainID(rollupCfg.L2ChainID) + fromAddr, err := signer.Sender(&tx) + n.require.NoError(err) + + l2Txs[fromAddr] = append(l2Txs[fromAddr], &tx) + } + } + } else { + l.Warn("Unknown batch type", + "channelID", channelID.String(), + "batchNumber", batchCount, + "batchType", batchType) + } + } + + l.Debug("Finished processing channel", + "channelID", channelID.String(), + "totalBatches", batchCount) + } + + return channels, channelFrames, l2Txs +} diff --git a/op-devstack/dsl/l2_op_rbuilder.go b/op-devstack/dsl/l2_op_rbuilder.go index 78f395c51e1..fef26ccfde5 100644 --- a/op-devstack/dsl/l2_op_rbuilder.go +++ b/op-devstack/dsl/l2_op_rbuilder.go @@ -1,11 +1,9 @@ package dsl import ( - "context" - "time" + opclient "github.com/ethereum-optimism/optimism/op-service/client" "github.com/ethereum-optimism/optimism/op-devstack/stack" - "github.com/ethereum/go-ethereum/log" ) type OPRBuilderNodeSet []*OPRBuilderNode @@ -21,7 +19,7 @@ func NewOPRBuilderNodeSet(inner []stack.OPRBuilderNode, control stack.ControlPla type OPRBuilderNode struct { commonImpl inner stack.OPRBuilderNode - wsClient *FlashblocksWSClient + wsClient *opclient.WSClient control stack.ControlPlane } @@ -29,7 +27,7 @@ func NewOPRBuilderNode(inner stack.OPRBuilderNode, control stack.ControlPlane) * return &OPRBuilderNode{ commonImpl: commonFromT(inner.T()), inner: inner, - wsClient: NewFlashblocksWSClient(inner.FlashblocksClient()), + wsClient: inner.FlashblocksClient(), control: control, } } @@ -42,8 +40,8 @@ func (c *OPRBuilderNode) Escape() stack.OPRBuilderNode { return c.inner } -func (c *OPRBuilderNode) ListenFor(ctx context.Context, logger log.Logger, duration time.Duration, output chan<- []byte, done chan<- struct{}) error { - return c.wsClient.ListenFor(ctx, logger, duration, output, done) +func (c *OPRBuilderNode) FlashblocksClient() *opclient.WSClient { + return c.wsClient } func (el *OPRBuilderNode) Stop() { @@ -54,7 +52,3 @@ func (el *OPRBuilderNode) Stop() { func (el *OPRBuilderNode) Start() { el.control.OPRBuilderNodeState(el.inner.ID(), stack.Start) } - -func (el *OPRBuilderNode) FlashblocksClient() *FlashblocksWSClient { - return NewFlashblocksWSClient(el.inner.FlashblocksClient()) -} diff --git a/op-devstack/dsl/rollup_boost.go b/op-devstack/dsl/rollup_boost.go index 1638944c1bb..37c98803d38 100644 --- a/op-devstack/dsl/rollup_boost.go +++ b/op-devstack/dsl/rollup_boost.go @@ -1,6 +1,10 @@ package dsl -import "github.com/ethereum-optimism/optimism/op-devstack/stack" +import ( + opclient "github.com/ethereum-optimism/optimism/op-service/client" + + "github.com/ethereum-optimism/optimism/op-devstack/stack" +) type RollupBoostNodesSet []*RollupBoostNode @@ -30,6 +34,6 @@ func NewRollupBoostNode(inner stack.RollupBoostNode, control stack.ControlPlane) } } -func (r *RollupBoostNode) FlashblocksClient() *FlashblocksWSClient { - return NewFlashblocksWSClient(r.inner.FlashblocksClient()) +func (r *RollupBoostNode) FlashblocksClient() *opclient.WSClient { + return r.inner.FlashblocksClient() } diff --git a/op-devstack/dsl/sequencer.go b/op-devstack/dsl/sequencer.go index b68db6ce573..e3b20605496 100644 --- a/op-devstack/dsl/sequencer.go +++ b/op-devstack/dsl/sequencer.go @@ -1,7 +1,12 @@ package dsl import ( + "github.com/ethereum-optimism/optimism/op-devstack/devtest" "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-test-sequencer/sequencer/seqtypes" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" ) type TestSequencer struct { @@ -24,3 +29,10 @@ func (s *TestSequencer) String() string { func (s *TestSequencer) Escape() stack.TestSequencer { return s.inner } + +func (s *TestSequencer) SequenceBlock(t devtest.T, chainID eth.ChainID, parent common.Hash) { + ca := s.Escape().ControlAPI(chainID) + + require.NoError(t, ca.New(t.Ctx(), seqtypes.BuildOpts{Parent: parent})) + require.NoError(t, ca.Next(t.Ctx())) +} diff --git a/op-devstack/shim/op_rbuilder.go b/op-devstack/shim/op_rbuilder.go index 8e931142136..a1fca59679f 100644 --- a/op-devstack/shim/op_rbuilder.go +++ b/op-devstack/shim/op_rbuilder.go @@ -6,21 +6,22 @@ import ( "github.com/ethereum-optimism/optimism/op-devstack/stack" "github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-service/apis" + opclient "github.com/ethereum-optimism/optimism/op-service/client" "github.com/ethereum-optimism/optimism/op-service/sources" ) type OPRBuilderNodeConfig struct { ELNodeConfig - RollupCfg *rollup.Config - ID stack.OPRBuilderNodeID - FlashblocksWsClient stack.FlashblocksWSClient + RollupCfg *rollup.Config + ID stack.OPRBuilderNodeID + FlashblocksClient *opclient.WSClient } type OPRBuilderNode struct { rpcELNode - id stack.OPRBuilderNodeID - engineClient *sources.EngineClient - flashblocksWsClient stack.FlashblocksWSClient + id stack.OPRBuilderNodeID + engineClient *sources.EngineClient + flashblocksClient *opclient.WSClient } var _ stack.OPRBuilderNode = (*OPRBuilderNode)(nil) @@ -33,10 +34,10 @@ func NewOPRBuilderNode(cfg OPRBuilderNodeConfig) *OPRBuilderNode { require.NoError(cfg.T, err) return &OPRBuilderNode{ - rpcELNode: newRpcELNode(cfg.ELNodeConfig), - engineClient: l2EngineClient, - id: cfg.ID, - flashblocksWsClient: cfg.FlashblocksWsClient, + rpcELNode: newRpcELNode(cfg.ELNodeConfig), + engineClient: l2EngineClient, + id: cfg.ID, + flashblocksClient: cfg.FlashblocksClient, } } @@ -48,8 +49,8 @@ func (r *OPRBuilderNode) L2EthClient() apis.L2EthClient { return r.engineClient.L2Client } -func (r *OPRBuilderNode) FlashblocksClient() stack.FlashblocksWSClient { - return r.flashblocksWsClient +func (r *OPRBuilderNode) FlashblocksClient() *opclient.WSClient { + return r.flashblocksClient } func (r *OPRBuilderNode) L2EngineClient() apis.EngineClient { diff --git a/op-devstack/shim/rollup_boost.go b/op-devstack/shim/rollup_boost.go index 6b2024f4f13..be4fa4ef6e7 100644 --- a/op-devstack/shim/rollup_boost.go +++ b/op-devstack/shim/rollup_boost.go @@ -1,18 +1,20 @@ package shim import ( + "github.com/stretchr/testify/require" + "github.com/ethereum-optimism/optimism/op-devstack/stack" "github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-service/apis" + opclient "github.com/ethereum-optimism/optimism/op-service/client" "github.com/ethereum-optimism/optimism/op-service/sources" - "github.com/stretchr/testify/require" ) type RollupBoostNodeConfig struct { ELNodeConfig - RollupCfg *rollup.Config - ID stack.RollupBoostNodeID - FlashblocksWsClient stack.FlashblocksWSClient + RollupCfg *rollup.Config + ID stack.RollupBoostNodeID + FlashblocksClient *opclient.WSClient } type RollupBoostNode struct { @@ -21,7 +23,7 @@ type RollupBoostNode struct { id stack.RollupBoostNodeID - flashblocksWsClient stack.FlashblocksWSClient + flashblocksClient *opclient.WSClient } var _ stack.RollupBoostNode = (*RollupBoostNode)(nil) @@ -34,10 +36,10 @@ func NewRollupBoostNode(cfg RollupBoostNodeConfig) *RollupBoostNode { require.NoError(cfg.T, err) return &RollupBoostNode{ - rpcELNode: newRpcELNode(cfg.ELNodeConfig), - engineClient: l2EngineClient, - id: cfg.ID, - flashblocksWsClient: cfg.FlashblocksWsClient, + rpcELNode: newRpcELNode(cfg.ELNodeConfig), + engineClient: l2EngineClient, + id: cfg.ID, + flashblocksClient: cfg.FlashblocksClient, } } @@ -49,8 +51,8 @@ func (r *RollupBoostNode) L2EthClient() apis.L2EthClient { return r.engineClient.L2Client } -func (r *RollupBoostNode) FlashblocksClient() stack.FlashblocksWSClient { - return r.flashblocksWsClient +func (r *RollupBoostNode) FlashblocksClient() *opclient.WSClient { + return r.flashblocksClient } func (r *RollupBoostNode) L2EngineClient() apis.EngineClient { diff --git a/op-devstack/stack/op_rbuilder.go b/op-devstack/stack/op_rbuilder.go index 7c655e32237..03926167f40 100644 --- a/op-devstack/stack/op_rbuilder.go +++ b/op-devstack/stack/op_rbuilder.go @@ -4,6 +4,7 @@ import ( "log/slog" "github.com/ethereum-optimism/optimism/op-service/apis" + "github.com/ethereum-optimism/optimism/op-service/client" "github.com/ethereum-optimism/optimism/op-service/eth" ) @@ -72,7 +73,7 @@ type OPRBuilderNode interface { ID() OPRBuilderNodeID L2EthClient() apis.L2EthClient L2EngineClient() apis.EngineClient - FlashblocksClient() FlashblocksWSClient + FlashblocksClient() *client.WSClient ELNode } diff --git a/op-devstack/stack/rollup_boost.go b/op-devstack/stack/rollup_boost.go index 92fdd6e760f..8c44e3d79d3 100644 --- a/op-devstack/stack/rollup_boost.go +++ b/op-devstack/stack/rollup_boost.go @@ -4,6 +4,7 @@ import ( "log/slog" "github.com/ethereum-optimism/optimism/op-service/apis" + "github.com/ethereum-optimism/optimism/op-service/client" "github.com/ethereum-optimism/optimism/op-service/eth" ) @@ -72,7 +73,7 @@ type RollupBoostNode interface { ID() RollupBoostNodeID L2EthClient() apis.L2EthClient L2EngineClient() apis.EngineClient - FlashblocksClient() FlashblocksWSClient + FlashblocksClient() *client.WSClient ELNode } diff --git a/op-devstack/sysext/l2.go b/op-devstack/sysext/l2.go index 05a12fb0bd7..5ec2be41558 100644 --- a/op-devstack/sysext/l2.go +++ b/op-devstack/sysext/l2.go @@ -206,6 +206,13 @@ func (o *Orchestrator) hydrateRollupBoostNodeMaybe(node *descriptors.Node, l2Net flashblocksWsUrl, flashblocksWsHeaders, err := o.findProtocolService(rollupBoostService, WebsocketFlashblocksProtocol) require.NoError(err, "failed to find websocket service for rollup-boost") + wsClient, err := client.DialWS(l2Net.T().Ctx(), client.WSConfig{ + URL: flashblocksWsUrl, + Headers: flashblocksWsHeaders, + Log: l2Net.Logger(), + }) + require.NoError(err, "failed to create rollup-boost websocket client") + rollupBoost := shim.NewRollupBoostNode(shim.RollupBoostNodeConfig{ ID: stack.NewRollupBoostNodeID(rollupBoostService.Name, l2ID.ChainID()), ELNodeConfig: shim.ELNodeConfig{ @@ -213,13 +220,8 @@ func (o *Orchestrator) hydrateRollupBoostNodeMaybe(node *descriptors.Node, l2Net Client: o.rpcClient(l2Net.T(), rollupBoostService, RPCProtocol, "/", opts...), ChainID: l2ID.ChainID(), }, - RollupCfg: l2Net.RollupConfig(), - FlashblocksWsClient: shim.NewFlashblocksWSClient(shim.FlashblocksWSClientConfig{ - CommonConfig: shim.NewCommonConfig(l2Net.T()), - ID: stack.NewFlashblocksWSClientID(rollupBoostService.Name, l2ID.ChainID()), - WsUrl: flashblocksWsUrl, - WsHeaders: flashblocksWsHeaders, - }), + RollupCfg: l2Net.RollupConfig(), + FlashblocksClient: wsClient, }) l2Net.AddRollupBoostNode(rollupBoost) @@ -264,6 +266,13 @@ func (o *Orchestrator) hydrateOPRBuilderMaybe(node *descriptors.Node, l2Net stac flashblocksWsUrl, flashblocksWsHeaders, err := o.findProtocolService(rbuilderService, WebsocketFlashblocksProtocol) require.NoError(err, "failed to find websocket service for rbuilder") + wsClient, err := client.DialWS(l2Net.T().Ctx(), client.WSConfig{ + URL: flashblocksWsUrl, + Headers: flashblocksWsHeaders, + Log: l2Net.Logger(), + }) + require.NoError(err, "failed to create rbuilder websocket client") + flashblocksBuilder := shim.NewOPRBuilderNode(shim.OPRBuilderNodeConfig{ ID: stack.NewOPRBuilderNodeID(rbuilderService.Name, l2ID.ChainID()), ELNodeConfig: shim.ELNodeConfig{ @@ -271,12 +280,7 @@ func (o *Orchestrator) hydrateOPRBuilderMaybe(node *descriptors.Node, l2Net stac Client: o.rpcClient(l2Net.T(), rbuilderService, RPCProtocol, "/", opts...), ChainID: l2ID.ChainID(), }, - FlashblocksWsClient: shim.NewFlashblocksWSClient(shim.FlashblocksWSClientConfig{ - CommonConfig: shim.NewCommonConfig(l2Net.T()), - ID: stack.NewFlashblocksWSClientID(rbuilderService.Name, l2ID.ChainID()), - WsUrl: flashblocksWsUrl, - WsHeaders: flashblocksWsHeaders, - }), + FlashblocksClient: wsClient, }) l2Net.AddOPRBuilderNode(flashblocksBuilder) diff --git a/op-devstack/sysgo/l2_batcher.go b/op-devstack/sysgo/l2_batcher.go index 643858c18d1..46e0db27a4f 100644 --- a/op-devstack/sysgo/l2_batcher.go +++ b/op-devstack/sysgo/l2_batcher.go @@ -89,7 +89,7 @@ func WithBatcher(batcherID stack.L2BatcherID, l1ELID stack.L1ELNodeID, l2CLID st L1EthRpc: l1EL.UserRPC(), L2EthRpc: []string{l2EL.UserRPC()}, RollupRpc: []string{l2CL.UserRPC()}, - MaxPendingTransactions: 1, + MaxPendingTransactions: 7, MaxChannelDuration: 1, MaxL1TxSize: 120_000, TestUseMaxTxSizeForBlobs: false, diff --git a/op-devstack/sysgo/l2_cl_opnode.go b/op-devstack/sysgo/l2_cl_opnode.go index ddc037f3a7d..2831a4666e2 100644 --- a/op-devstack/sysgo/l2_cl_opnode.go +++ b/op-devstack/sysgo/l2_cl_opnode.go @@ -28,6 +28,7 @@ import ( "github.com/ethereum-optimism/optimism/op-node/rollup/interop" nodeSync "github.com/ethereum-optimism/optimism/op-node/rollup/sync" "github.com/ethereum-optimism/optimism/op-service/client" + "github.com/ethereum-optimism/optimism/op-service/clock" "github.com/ethereum-optimism/optimism/op-service/eth" opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics" "github.com/ethereum-optimism/optimism/op-service/oppprof" @@ -51,6 +52,7 @@ type OpNode struct { el *stack.L2ELNodeID // Optional: nil when using SyncTester userProxy *tcpproxy.Proxy interopProxy *tcpproxy.Proxy + clock clock.Clock } var _ L2CLNode = (*OpNode)(nil) @@ -130,7 +132,7 @@ func (n *OpNode) Start() { n.interopEndpoint = "ws://" + n.interopProxy.Addr() } n.logger.Info("Starting op-node") - opNode, err := opnode.NewOpnode(n.logger, n.cfg, func(err error) { + opNode, err := opnode.NewOpnode(n.logger, n.cfg, n.clock, func(err error) { n.p.Require().NoError(err, "op-node critical error") }) n.p.Require().NoError(err, "op-node failed to start") @@ -339,6 +341,10 @@ func WithOpNode(l2CLID stack.L2CLNodeID, l1CLID stack.L1CLNodeID, l1ELID stack.L p: p, } + if orch.timeTravelClock != nil { + l2CLNode.clock = orch.timeTravelClock + } + // Set the EL field to link to the L2EL node l2CLNode.el = &l2ELID require.True(orch.l2CLs.SetIfMissing(l2CLID, l2CLNode), fmt.Sprintf("must not already exist: %s", l2CLID)) diff --git a/op-devstack/sysgo/op_rbuilder.go b/op-devstack/sysgo/op_rbuilder.go index 4bab3d0002e..fc92a5aba2d 100644 --- a/op-devstack/sysgo/op_rbuilder.go +++ b/op-devstack/sysgo/op_rbuilder.go @@ -312,6 +312,13 @@ func (b *OPRBuilderNode) hydrate(system stack.ExtensibleSystem) { system.T().Require().NoError(err) system.T().Cleanup(elRPC.Close) + // Create a shared websocket client for flashblocks traffic over the proxy. + wsClient, err := client.DialWS(system.T().Ctx(), client.WSConfig{ + URL: b.wsProxyURL, + Log: system.Logger(), + }) + system.T().Require().NoError(err) + node := shim.NewOPRBuilderNode(shim.OPRBuilderNodeConfig{ ID: b.id, ELNodeConfig: shim.ELNodeConfig{ @@ -319,13 +326,8 @@ func (b *OPRBuilderNode) hydrate(system stack.ExtensibleSystem) { Client: elRPC, ChainID: b.id.ChainID(), }, - RollupCfg: b.rollupCfg, - FlashblocksWsClient: shim.NewFlashblocksWSClient(shim.FlashblocksWSClientConfig{ - CommonConfig: shim.NewCommonConfig(system.T()), - ID: stack.NewFlashblocksWSClientID(b.id.Key(), b.id.ChainID()), - WsUrl: b.wsProxyURL, - WsHeaders: nil, - }), + RollupCfg: b.rollupCfg, + FlashblocksClient: wsClient, }) system.L2Network(stack.L2NetworkID(b.id.ChainID())).(stack.ExtensibleL2Network).AddOPRBuilderNode(node) } diff --git a/op-devstack/sysgo/rollup_boost.go b/op-devstack/sysgo/rollup_boost.go index 90258e40a21..ece089e00e4 100644 --- a/op-devstack/sysgo/rollup_boost.go +++ b/op-devstack/sysgo/rollup_boost.go @@ -21,7 +21,7 @@ import ( // RollupBoostNode is a lightweight sysgo-managed process wrapper around a rollup-boost // WebSocket stream source. It exposes a stable proxied ws URL and hydrates the L2 -// network with a FlashblocksWSClient shim that points at it. +// network with a shared WSClient that points at it. type RollupBoostNode struct { mu sync.Mutex @@ -51,6 +51,14 @@ func (r *RollupBoostNode) hydrate(system stack.ExtensibleSystem) { system.T().Require().NoError(err) system.T().Cleanup(elRPC.Close) + // Create a shared websocket client for flashblocks traffic over the proxy. + wsClient, err := client.DialWS(system.T().Ctx(), client.WSConfig{ + URL: r.wsProxyURL, + Headers: r.header, + Log: system.Logger(), + }) + system.T().Require().NoError(err) + node := shim.NewRollupBoostNode(shim.RollupBoostNodeConfig{ ID: r.id, ELNodeConfig: shim.ELNodeConfig{ @@ -58,13 +66,8 @@ func (r *RollupBoostNode) hydrate(system stack.ExtensibleSystem) { Client: elRPC, ChainID: r.id.ChainID(), }, - RollupCfg: system.L2Network(stack.L2NetworkID(r.id.ChainID())).RollupConfig(), - FlashblocksWsClient: shim.NewFlashblocksWSClient(shim.FlashblocksWSClientConfig{ - CommonConfig: shim.NewCommonConfig(system.T()), - ID: stack.NewFlashblocksWSClientID(r.id.Key(), r.id.ChainID()), - WsUrl: r.wsProxyURL, - WsHeaders: r.header, - }), + RollupCfg: system.L2Network(stack.L2NetworkID(r.id.ChainID())).RollupConfig(), + FlashblocksClient: wsClient, }) system.L2Network(stack.L2NetworkID(r.id.ChainID())).(stack.ExtensibleL2Network).AddRollupBoostNode(node) } @@ -149,7 +152,7 @@ func (r *RollupBoostNode) Stop() { } // WithRollupBoost starts a rollup-boost process using the provided options -// and registers a FlashblocksWSClient on the target L2 chain. +// and registers a WSClient on the target L2 chain. // l2ELID is required to link the proxy to the L2 EL it serves. func WithRollupBoost(id stack.RollupBoostNodeID, l2ELID stack.L2ELNodeID, opts ...RollupBoostOption) stack.Option[*Orchestrator] { return stack.AfterDeploy(func(orch *Orchestrator) { diff --git a/op-devstack/sysgo/system.go b/op-devstack/sysgo/system.go index ef5e17a8a91..a925d4703e2 100644 --- a/op-devstack/sysgo/system.go +++ b/op-devstack/sysgo/system.go @@ -623,7 +623,7 @@ func singleChainSystemWithFlashblocksOpts(ids *SingleChainSystemWithFlashblocksI opt := stack.Combine[*Orchestrator]() // Precompute deterministic P2P identity and peering between sequencer EL and op-rbuilder EL. seqID := NewELNodeIdentity("127.0.0.1", 0) - builderID := NewELNodeIdentity("127.0.0.1", 30303) // use default reth p2p port + builderID := NewELNodeIdentity("127.0.0.1", 0) // allocate dynamic port for builder var missingEnv []string if os.Getenv("OP_RBUILDER_EXEC_PATH") == "" { diff --git a/op-devstack/sysgo/util.go b/op-devstack/sysgo/util.go index 68fb9d0eedd..893d9f17051 100644 --- a/op-devstack/sysgo/util.go +++ b/op-devstack/sysgo/util.go @@ -11,8 +11,8 @@ import ( "sync" "time" - "github.com/coder/websocket" "github.com/ethereum-optimism/optimism/op-devstack/devtest" + opclient "github.com/ethereum-optimism/optimism/op-service/client" "github.com/stretchr/testify/assert" ) @@ -79,14 +79,8 @@ func waitWSReady(p devtest.P, rawURL string, timeout time.Duration) { waitWSMsg := fmt.Sprintf("WebSocket endpoint %s not ready within %v", rawURL, timeout) p.Require().EventuallyWithT(func(c *assert.CollectT) { ctx, cancel := context.WithTimeout(context.Background(), 750*time.Millisecond) - conn, resp, err := websocket.Dial(ctx, rawURL, nil) + err := opclient.ProbeWS(ctx, rawURL) cancel() - if resp != nil && resp.Body != nil { - _ = resp.Body.Close() - } - if conn != nil { - _ = conn.Close(websocket.StatusNormalClosure, "") - } assert.NoError(c, err, "WebSocket handshake to %s should succeed", rawURL) }, timeout, 100*time.Millisecond, waitWSMsg) } diff --git a/op-dispute-mon/mon/extract/output_agreement_enricher_test.go b/op-dispute-mon/mon/extract/output_agreement_enricher_test.go index d0f665383c5..420f15d5754 100644 --- a/op-dispute-mon/mon/extract/output_agreement_enricher_test.go +++ b/op-dispute-mon/mon/extract/output_agreement_enricher_test.go @@ -40,7 +40,7 @@ func TestOutputAgreementEnricher(t *testing.T) { }) t.Run("SkipNonOutputRootGameTypes", func(t *testing.T) { - gameTypes := []uint32{4, 5, 7, 8, 10, 49812} + gameTypes := []uint32{4, 5, 7, 9, 11, 49812} for _, gameType := range gameTypes { gameType := gameType t.Run(fmt.Sprintf("GameType_%d", gameType), func(t *testing.T) { diff --git a/op-dispute-mon/mon/extract/super_agreement_enricher_test.go b/op-dispute-mon/mon/extract/super_agreement_enricher_test.go index 5ad80217a89..7fa06027862 100644 --- a/op-dispute-mon/mon/extract/super_agreement_enricher_test.go +++ b/op-dispute-mon/mon/extract/super_agreement_enricher_test.go @@ -61,7 +61,7 @@ func TestDetector_CheckSuperRootAgreement(t *testing.T) { }) t.Run("FetchAllNonOutputRootGameTypes", func(t *testing.T) { - gameTypes := []uint32{4, 5, 7, 8, 10, 49812} // Treat unknown game types as using super roots + gameTypes := []uint32{4, 5, 7, 9, 11, 49812} // Treat unknown game types as using super roots for _, gameType := range gameTypes { gameType := gameType t.Run(fmt.Sprintf("GameType_%d", gameType), func(t *testing.T) { diff --git a/op-dispute-mon/mon/types/types.go b/op-dispute-mon/mon/types/types.go index e05d2fd509b..7b1874516c8 100644 --- a/op-dispute-mon/mon/types/types.go +++ b/op-dispute-mon/mon/types/types.go @@ -13,7 +13,25 @@ import ( // outputRootGameTypes lists the set of legacy game types that use output roots // It is assumed that all other game types use super roots -var outputRootGameTypes = []uint32{0, 1, 2, 3, 6, 254, 255, 1337} +var outputRootGameTypes = []types.GameType{ + types.CannonGameType, + types.PermissionedGameType, + types.AsteriscGameType, + types.AsteriscKonaGameType, + types.OPSuccinctGameType, + types.CannonKonaGameType, + types.OptimisticZKGameType, + types.FastGameType, + types.AlphabetGameType, + types.KailuaGameType, +} + +var superRootGameTypes = []types.GameType{ + types.SuperCannonGameType, + types.SuperPermissionedGameType, + types.SuperAsteriscKonaGameType, + types.SuperCannonKonaGameType, +} // EnrichedClaim extends the faultTypes.Claim with additional context. type EnrichedClaim struct { @@ -87,7 +105,7 @@ type EnrichedGameData struct { // UsesOutputRoots returns true if the game type is one of the known types that use output roots as proposals. func (g EnrichedGameData) UsesOutputRoots() bool { - return slices.Contains(outputRootGameTypes, g.GameType) + return slices.Contains(outputRootGameTypes, types.GameType(g.GameType)) } // HasMixedAvailability returns true if some rollup endpoints returned "not found" while others succeeded diff --git a/op-dispute-mon/mon/types/types_test.go b/op-dispute-mon/mon/types/types_test.go index 99df937ea8e..1addef9c9a0 100644 --- a/op-dispute-mon/mon/types/types_test.go +++ b/op-dispute-mon/mon/types/types_test.go @@ -13,7 +13,7 @@ func TestEnrichedGameData_UsesOutputRoots(t *testing.T) { gameType := gameType t.Run(fmt.Sprintf("GameType-%v", gameType), func(t *testing.T) { data := EnrichedGameData{ - GameMetadata: types.GameMetadata{GameType: gameType}, + GameMetadata: types.GameMetadata{GameType: uint32(gameType)}, } require.True(t, data.UsesOutputRoots()) }) @@ -177,3 +177,22 @@ func TestEnrichedGameData_HasMixedSafety(t *testing.T) { }) } } + +func TestAllSupportedGameTypesAreOutputOrSuperRootType(t *testing.T) { + for _, gameType := range types.SupportedGameTypes { + t.Run(gameType.String(), func(t *testing.T) { + data := EnrichedGameData{ + GameMetadata: types.GameMetadata{ + GameType: uint32(gameType), + }, + } + if data.UsesOutputRoots() { + require.Contains(t, outputRootGameTypes, gameType) + require.NotContains(t, superRootGameTypes, gameType) + } else { + require.Contains(t, superRootGameTypes, gameType) + require.NotContains(t, outputRootGameTypes, gameType) + } + }) + } +} diff --git a/op-e2e/e2eutils/opnode/opnode.go b/op-e2e/e2eutils/opnode/opnode.go index 3a8cf3b6963..db5b7f5695f 100644 --- a/op-e2e/e2eutils/opnode/opnode.go +++ b/op-e2e/e2eutils/opnode/opnode.go @@ -12,6 +12,7 @@ import ( "github.com/ethereum-optimism/optimism/op-node/node/runcfg" "github.com/ethereum-optimism/optimism/op-node/p2p" "github.com/ethereum-optimism/optimism/op-service/cliapp" + "github.com/ethereum-optimism/optimism/op-service/clock" "github.com/ethereum-optimism/optimism/op-service/endpoint" "github.com/ethereum-optimism/optimism/op-service/eth" ) @@ -54,7 +55,7 @@ func (o *Opnode) P2P() p2p.Node { var _ services.RollupNode = (*Opnode)(nil) -func NewOpnode(l log.Logger, c *config.Config, errFn func(error)) (*Opnode, error) { +func NewOpnode(l log.Logger, c *config.Config, clk clock.Clock, errFn func(error)) (*Opnode, error) { if err := c.Check(); err != nil { return nil, err } @@ -70,7 +71,7 @@ func NewOpnode(l log.Logger, c *config.Config, errFn func(error)) (*Opnode, erro l.Warn("closed op-node!") }() } - node, err := rollupNode.New(context.Background(), c, l, "", metrics.NewMetrics("")) + node, err := rollupNode.New(context.Background(), c, l, "", metrics.NewMetrics(""), clk) if err != nil { return nil, err } diff --git a/op-e2e/interop/interop_test.go b/op-e2e/interop/interop_test.go index ca03e511274..080b06fda1c 100644 --- a/op-e2e/interop/interop_test.go +++ b/op-e2e/interop/interop_test.go @@ -13,6 +13,7 @@ import ( "github.com/ethereum-optimism/optimism/op-service/dial" "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/sources/batching" + "github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock" "github.com/ethereum-optimism/optimism/op-service/testlog" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" @@ -442,7 +443,7 @@ func TestProposals(t *testing.T) { head, err := ethClient.BlockByNumber(context.Background(), nil) require.NoError(t, err) - game, err := factory.GetGame(context.Background(), 0, head.Hash()) + game, err := factory.GetGame(context.Background(), 0, rpcblock.ByHash(head.Hash())) require.NoError(t, err) require.Equal(t, uint32(4) /* super permissionless */, game.GameType) } diff --git a/op-e2e/interop/supersystem_l2.go b/op-e2e/interop/supersystem_l2.go index fc3fb282a58..3d7b0fa7c04 100644 --- a/op-e2e/interop/supersystem_l2.go +++ b/op-e2e/interop/supersystem_l2.go @@ -22,6 +22,7 @@ import ( "github.com/ethereum-optimism/optimism/op-node/rollup/sync" l2os "github.com/ethereum-optimism/optimism/op-proposer/proposer" "github.com/ethereum-optimism/optimism/op-service/client" + "github.com/ethereum-optimism/optimism/op-service/clock" "github.com/ethereum-optimism/optimism/op-service/dial" "github.com/ethereum-optimism/optimism/op-service/endpoint" oplog "github.com/ethereum-optimism/optimism/op-service/log" @@ -198,7 +199,7 @@ func (s *interopE2ESystem) newNodeForL2( ConfigPersistence: config.DisabledConfigPersistence{}, } opNode, err := opnode.NewOpnode(logger.New("service", "op-node"), - nodeCfg, func(err error) { + nodeCfg, clock.SystemClock, func(err error) { s.t.Error(err) }) require.NoError(s.t, err) diff --git a/op-e2e/system/e2esys/setup.go b/op-e2e/system/e2esys/setup.go index 3c19a379a91..baf0b52854b 100644 --- a/op-e2e/system/e2esys/setup.go +++ b/op-e2e/system/e2esys/setup.go @@ -612,10 +612,10 @@ func (cfg SystemConfig) Start(t *testing.T, startOpts ...StartOption) (*System, // Automatically stop the system at the end of the test t.Cleanup(sys.Close) - c := clock.SystemClock + clk := clock.SystemClock if cfg.SupportL1TimeTravel { sys.TimeTravelClock = clock.NewAdvancingClock(100 * time.Millisecond) - c = sys.TimeTravelClock + clk = sys.TimeTravelClock } if err := cfg.DeployConfig.Check(testlog.Logger(t, log.LevelInfo)); err != nil { @@ -747,7 +747,7 @@ func (cfg SystemConfig) Start(t *testing.T, startOpts ...StartOption) (*System, // Initialize nodes l1Geth, _, err := geth.InitL1( - cfg.DeployConfig.L1BlockTime, cfg.L1FinalizedDistance, l1Genesis, c, + cfg.DeployConfig.L1BlockTime, cfg.L1FinalizedDistance, l1Genesis, clk, path.Join(cfg.BlobsPath, "l1_el"), bcn, cfg.GethOptions[RoleL1]...) if err != nil { return nil, err @@ -887,7 +887,7 @@ func (cfg SystemConfig) Start(t *testing.T, startOpts ...StartOption) (*System, c.Rollup.LogDescription(cfg.Loggers[name], chaincfg.L2ChainIDToNetworkDisplayName) l := cfg.Loggers[name] - n, err := opnode.NewOpnode(l, &c, func(err error) { + n, err := opnode.NewOpnode(l, &c, clk, func(err error) { t.Error(err) }) require.NoError(t, err) diff --git a/op-e2e/system/p2p/reqresp_test.go b/op-e2e/system/p2p/reqresp_test.go index 2680f9fb910..068977c1722 100644 --- a/op-e2e/system/p2p/reqresp_test.go +++ b/op-e2e/system/p2p/reqresp_test.go @@ -157,7 +157,7 @@ func TestSystemP2PAltSync(t *testing.T) { // Ensure L1 chain configuration is provided for the sync node syncNodeCfg.L1ChainConfig = sys.L1GenesisCfg.Config - syncerNode, err := rollupNode.New(ctx, syncNodeCfg, cfg.Loggers["syncer"], "", metrics.NewMetrics("")) + syncerNode, err := rollupNode.New(ctx, syncNodeCfg, cfg.Loggers["syncer"], "", metrics.NewMetrics(""), nil) require.NoError(t, err) err = syncerNode.Start(ctx) require.NoError(t, err) diff --git a/op-node/cmd/main.go b/op-node/cmd/main.go index 68af603022f..deacab727fb 100644 --- a/op-node/cmd/main.go +++ b/op-node/cmd/main.go @@ -94,7 +94,7 @@ func RollupNodeMain(ctx *cli.Context, closeApp context.CancelCauseFunc) (cliapp. cfg.Rollup.LogDescription(log, chaincfg.L2ChainIDToNetworkDisplayName) } - n, err := node.New(ctx.Context, cfg, log, VersionWithMeta, m) + n, err := node.New(ctx.Context, cfg, log, VersionWithMeta, m, nil) if err != nil { return nil, fmt.Errorf("unable to create the rollup node: %w", err) } diff --git a/op-node/node/node.go b/op-node/node/node.go index c81085a4268..fadc303b727 100644 --- a/op-node/node/node.go +++ b/op-node/node/node.go @@ -34,6 +34,7 @@ import ( "github.com/ethereum-optimism/optimism/op-node/rollup/sequencing" "github.com/ethereum-optimism/optimism/op-node/rollup/sync" "github.com/ethereum-optimism/optimism/op-service/client" + "github.com/ethereum-optimism/optimism/op-service/clock" "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/event" "github.com/ethereum-optimism/optimism/op-service/httputil" @@ -103,6 +104,7 @@ type OpNode struct { // Retain the config to test for active features rather than test for runtime state. cfg *config.Config log log.Logger + clock clock.Clock appVersion string metrics *metrics.Metrics @@ -153,15 +155,15 @@ type OpNode struct { // New creates a new OpNode instance. // The provided ctx argument is for the span of initialization only; // the node will immediately Stop(ctx) before finishing initialization if the context is canceled during initialization. -func New(ctx context.Context, cfg *config.Config, log log.Logger, appVersion string, m *metrics.Metrics) (*OpNode, error) { - return NewWithOverride(ctx, cfg, log, appVersion, m, InitializationOverrides{}) +func New(ctx context.Context, cfg *config.Config, log log.Logger, appVersion string, m *metrics.Metrics, clk clock.Clock) (*OpNode, error) { + return NewWithOverride(ctx, cfg, log, appVersion, m, clk, InitializationOverrides{}) } // NewWithOverride creates a new OpNode instance with optional initialization overrides. // This allows callers to override specific initialization steps, enabling resource sharing // (e.g., shared L1Client across multiple nodes) without duplicating connections or caches. // If override is nil or any of its fields are nil, the default initialization is used for those steps. -func NewWithOverride(ctx context.Context, cfg *config.Config, log log.Logger, appVersion string, m *metrics.Metrics, override InitializationOverrides) (*OpNode, error) { +func NewWithOverride(ctx context.Context, cfg *config.Config, log log.Logger, appVersion string, m *metrics.Metrics, clk clock.Clock, override InitializationOverrides) (*OpNode, error) { if err := cfg.Check(); err != nil { return nil, err } @@ -169,6 +171,7 @@ func NewWithOverride(ctx context.Context, cfg *config.Config, log log.Logger, ap n := &OpNode{ cfg: cfg, log: log, + clock: clk, appVersion: appVersion, metrics: m, rollupHalt: cfg.RollupHalt, @@ -712,7 +715,7 @@ func initP2P(cfg *config.Config, node *OpNode) (*p2p.NodeP2P, error) { } // embed syncDeriver and tracer(optional) to the blockReceiver to handle unsafe payloads via p2p rec := p2p.NewBlockReceiver(node.log, node.metrics, node.l2Driver.SyncDeriver, node.cfg.Tracer) - p2pNode, err := p2p.NewNodeP2P(node.resourcesCtx, &cfg.Rollup, node.log, cfg.P2P, rec, node.l2Source, node.runCfg, node.metrics) + p2pNode, err := p2p.NewNodeP2P(node.resourcesCtx, &cfg.Rollup, node.log, cfg.P2P, rec, node.l2Source, node.runCfg, node.metrics, node.clock) if err != nil { return nil, err } diff --git a/op-node/p2p/gossip.go b/op-node/p2p/gossip.go index 2a8442a9d23..3ac14c39363 100644 --- a/op-node/p2p/gossip.go +++ b/op-node/p2p/gossip.go @@ -21,6 +21,7 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum-optimism/optimism/op-node/rollup" + "github.com/ethereum-optimism/optimism/op-service/clock" "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/ptr" opsigner "github.com/ethereum-optimism/optimism/op-service/signer" @@ -264,7 +265,7 @@ func (sb *seenBlocks) markSeen(h common.Hash) { sb.blockHashes = append(sb.blockHashes, h) } -func BuildBlocksValidator(log log.Logger, cfg *rollup.Config, runCfg GossipRuntimeConfig, blockVersion eth.BlockVersion, gossipConf GossipSetupConfigurables) pubsub.ValidatorEx { +func BuildBlocksValidator(log log.Logger, cfg *rollup.Config, runCfg GossipRuntimeConfig, blockVersion eth.BlockVersion, gossipConf GossipSetupConfigurables, clk clock.Clock) pubsub.ValidatorEx { // Seen block hashes per block height // uint64 -> *seenBlocks blockHeightLRU, err := lru.New[uint64, *seenBlocks](1000) @@ -331,6 +332,9 @@ func BuildBlocksValidator(log log.Logger, cfg *rollup.Config, runCfg GossipRunti // rounding down to seconds is fine here. now := uint64(time.Now().Unix()) + if clk != nil { + now = uint64(clk.Now().Unix()) + } // [REJECT] if the `payload.timestamp` is older than the configured threshold threshold := uint64(gossipConf.GetGossipTimestampThreshold().Seconds()) @@ -630,11 +634,11 @@ func (p *publisher) Close() error { return errors.Join(e1, e2) } -func JoinGossip(self peer.ID, ps *pubsub.PubSub, log log.Logger, cfg *rollup.Config, runCfg GossipRuntimeConfig, gossipIn GossipIn, gossipConf GossipSetupConfigurables) (GossipOut, error) { +func JoinGossip(self peer.ID, ps *pubsub.PubSub, log log.Logger, cfg *rollup.Config, runCfg GossipRuntimeConfig, gossipIn GossipIn, gossipConf GossipSetupConfigurables, clk clock.Clock) (GossipOut, error) { p2pCtx, p2pCancel := context.WithCancel(context.Background()) v1Logger := log.New("topic", "blocksV1") - blocksV1Validator := guardGossipValidator(log, logValidationResult(self, "validated blockv1", v1Logger, BuildBlocksValidator(v1Logger, cfg, runCfg, eth.BlockV1, gossipConf))) + blocksV1Validator := guardGossipValidator(log, logValidationResult(self, "validated blockv1", v1Logger, BuildBlocksValidator(v1Logger, cfg, runCfg, eth.BlockV1, gossipConf, clk))) blocksV1, err := newBlockTopic(p2pCtx, blocksTopicV1(cfg), ps, v1Logger, gossipIn, blocksV1Validator) if err != nil { p2pCancel() @@ -642,7 +646,7 @@ func JoinGossip(self peer.ID, ps *pubsub.PubSub, log log.Logger, cfg *rollup.Con } v2Logger := log.New("topic", "blocksV2") - blocksV2Validator := guardGossipValidator(log, logValidationResult(self, "validated blockv2", v2Logger, BuildBlocksValidator(v2Logger, cfg, runCfg, eth.BlockV2, gossipConf))) + blocksV2Validator := guardGossipValidator(log, logValidationResult(self, "validated blockv2", v2Logger, BuildBlocksValidator(v2Logger, cfg, runCfg, eth.BlockV2, gossipConf, clk))) blocksV2, err := newBlockTopic(p2pCtx, blocksTopicV2(cfg), ps, v2Logger, gossipIn, blocksV2Validator) if err != nil { p2pCancel() @@ -650,7 +654,7 @@ func JoinGossip(self peer.ID, ps *pubsub.PubSub, log log.Logger, cfg *rollup.Con } v3Logger := log.New("topic", "blocksV3") - blocksV3Validator := guardGossipValidator(log, logValidationResult(self, "validated blockv3", v3Logger, BuildBlocksValidator(v3Logger, cfg, runCfg, eth.BlockV3, gossipConf))) + blocksV3Validator := guardGossipValidator(log, logValidationResult(self, "validated blockv3", v3Logger, BuildBlocksValidator(v3Logger, cfg, runCfg, eth.BlockV3, gossipConf, clk))) blocksV3, err := newBlockTopic(p2pCtx, blocksTopicV3(cfg), ps, v3Logger, gossipIn, blocksV3Validator) if err != nil { p2pCancel() @@ -658,7 +662,7 @@ func JoinGossip(self peer.ID, ps *pubsub.PubSub, log log.Logger, cfg *rollup.Con } v4Logger := log.New("topic", "blocksV4") - blocksV4Validator := guardGossipValidator(log, logValidationResult(self, "validated blockv4", v4Logger, BuildBlocksValidator(v4Logger, cfg, runCfg, eth.BlockV4, gossipConf))) + blocksV4Validator := guardGossipValidator(log, logValidationResult(self, "validated blockv4", v4Logger, BuildBlocksValidator(v4Logger, cfg, runCfg, eth.BlockV4, gossipConf, clk))) blocksV4, err := newBlockTopic(p2pCtx, blocksTopicV4(cfg), ps, v4Logger, gossipIn, blocksV4Validator) if err != nil { p2pCancel() diff --git a/op-node/p2p/gossip_test.go b/op-node/p2p/gossip_test.go index c98cb2ae440..10eb6fe4983 100644 --- a/op-node/p2p/gossip_test.go +++ b/op-node/p2p/gossip_test.go @@ -12,6 +12,7 @@ import ( "github.com/golang/snappy" "github.com/ethereum-optimism/optimism/op-node/rollup" + "github.com/ethereum-optimism/optimism/op-service/clock" "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/ptr" opsigner "github.com/ethereum-optimism/optimism/op-service/signer" @@ -156,14 +157,14 @@ func TestBlockValidator(t *testing.T) { // Create a mock gossip configuration for testing mockGossipConf := &mockGossipSetupConfigurablesWithThreshold{threshold: 60 * time.Second} - v2Validator := BuildBlocksValidator(testlog.Logger(t, log.LevelCrit), cfg, runCfg, eth.BlockV2, mockGossipConf) - v3Validator := BuildBlocksValidator(testlog.Logger(t, log.LevelCrit), cfg, runCfg, eth.BlockV3, mockGossipConf) - v4Validator := BuildBlocksValidator(testlog.Logger(t, log.LevelDebug), cfg, runCfg, eth.BlockV4, mockGossipConf) + v2Validator := BuildBlocksValidator(testlog.Logger(t, log.LevelCrit), cfg, runCfg, eth.BlockV2, mockGossipConf, clock.SystemClock) + v3Validator := BuildBlocksValidator(testlog.Logger(t, log.LevelCrit), cfg, runCfg, eth.BlockV3, mockGossipConf, clock.SystemClock) + v4Validator := BuildBlocksValidator(testlog.Logger(t, log.LevelDebug), cfg, runCfg, eth.BlockV4, mockGossipConf, clock.SystemClock) jovianCfg := &rollup.Config{ L2ChainID: big.NewInt(100), JovianTime: ptr.New(uint64(0)), } - v4JovianValidator := BuildBlocksValidator(testlog.Logger(t, log.LevelCrit), jovianCfg, runCfg, eth.BlockV4, mockGossipConf) + v4JovianValidator := BuildBlocksValidator(testlog.Logger(t, log.LevelCrit), jovianCfg, runCfg, eth.BlockV4, mockGossipConf, clock.SystemClock) zero, one := uint64(0), uint64(1) beaconHash, withdrawalsRoot := common.HexToHash("0x1234"), common.HexToHash("0x9876") @@ -263,7 +264,7 @@ func TestGossipTimestampThreshold(t *testing.T) { mockConfig := &mockGossipSetupConfigurablesWithThreshold{threshold: tc.threshold} // Create validator with the mock config - validator := BuildBlocksValidator(testlog.Logger(t, log.LevelCrit), cfg, runCfg, eth.BlockV2, mockConfig) + validator := BuildBlocksValidator(testlog.Logger(t, log.LevelCrit), cfg, runCfg, eth.BlockV2, mockConfig, clock.SystemClock) // Create payload with the specific timestamp payload := createExecutionPayload(types.Withdrawals{}, nil, nil, nil) diff --git a/op-node/p2p/host_test.go b/op-node/p2p/host_test.go index 9f461b2fed7..621fc20747d 100644 --- a/op-node/p2p/host_test.go +++ b/op-node/p2p/host_test.go @@ -27,6 +27,7 @@ import ( "github.com/ethereum-optimism/optimism/op-node/metrics" "github.com/ethereum-optimism/optimism/op-node/p2p/store" "github.com/ethereum-optimism/optimism/op-node/rollup" + "github.com/ethereum-optimism/optimism/op-service/clock" "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/testlog" "github.com/ethereum-optimism/optimism/op-service/testutils" @@ -120,7 +121,7 @@ func TestP2PFull(t *testing.T) { runCfgB := &testutils.MockRuntimeConfig{P2PSeqAddress: common.Address{0x42}} logA := testlog.Logger(t, log.LevelError).New("host", "A") - nodeA, err := NewNodeP2P(context.Background(), &rollup.Config{}, logA, &confA, &mockGossipIn{}, nil, runCfgA, metrics.NoopMetrics) + nodeA, err := NewNodeP2P(context.Background(), &rollup.Config{}, logA, &confA, &mockGossipIn{}, nil, runCfgA, metrics.NoopMetrics, clock.SystemClock) require.NoError(t, err) defer nodeA.Close() @@ -149,7 +150,7 @@ func TestP2PFull(t *testing.T) { logB := testlog.Logger(t, log.LevelError).New("host", "B") - nodeB, err := NewNodeP2P(context.Background(), &rollup.Config{}, logB, &confB, &mockGossipIn{}, nil, runCfgB, metrics.NoopMetrics) + nodeB, err := NewNodeP2P(context.Background(), &rollup.Config{}, logB, &confB, &mockGossipIn{}, nil, runCfgB, metrics.NoopMetrics, clock.SystemClock) require.NoError(t, err) defer nodeB.Close() hostB := nodeB.Host() @@ -321,7 +322,7 @@ func TestDiscovery(t *testing.T) { resourcesCtx, resourcesCancel := context.WithCancel(context.Background()) defer resourcesCancel() - nodeA, err := NewNodeP2P(context.Background(), rollupCfg, logA, &confA, &mockGossipIn{}, nil, runCfgA, metrics.NoopMetrics) + nodeA, err := NewNodeP2P(context.Background(), rollupCfg, logA, &confA, &mockGossipIn{}, nil, runCfgA, metrics.NoopMetrics, clock.SystemClock) require.NoError(t, err) defer nodeA.Close() hostA := nodeA.Host() @@ -336,7 +337,7 @@ func TestDiscovery(t *testing.T) { confB.DiscoveryDB = discDBC // Start B - nodeB, err := NewNodeP2P(context.Background(), rollupCfg, logB, &confB, &mockGossipIn{}, nil, runCfgB, metrics.NoopMetrics) + nodeB, err := NewNodeP2P(context.Background(), rollupCfg, logB, &confB, &mockGossipIn{}, nil, runCfgB, metrics.NoopMetrics, clock.SystemClock) require.NoError(t, err) defer nodeB.Close() hostB := nodeB.Host() @@ -351,7 +352,7 @@ func TestDiscovery(t *testing.T) { }}) // Start C - nodeC, err := NewNodeP2P(context.Background(), rollupCfg, logC, &confC, &mockGossipIn{}, nil, runCfgC, metrics.NoopMetrics) + nodeC, err := NewNodeP2P(context.Background(), rollupCfg, logC, &confC, &mockGossipIn{}, nil, runCfgC, metrics.NoopMetrics, clock.SystemClock) require.NoError(t, err) defer nodeC.Close() hostC := nodeC.Host() diff --git a/op-node/p2p/node.go b/op-node/p2p/node.go index 149eec5a8f8..4c60f8b46df 100644 --- a/op-node/p2p/node.go +++ b/op-node/p2p/node.go @@ -61,6 +61,7 @@ func NewNodeP2P( l2Chain L2Chain, runCfg GossipRuntimeConfig, metrics metrics.Metricer, + clock clock.Clock, ) (*NodeP2P, error) { if setup == nil { return nil, errors.New("p2p node cannot be created without setup") @@ -69,7 +70,7 @@ func NewNodeP2P( return nil, errors.New("SetupP2P.Disabled is true") } var n NodeP2P - if err := n.init(resourcesCtx, rollupCfg, log, setup, gossipIn, l2Chain, runCfg, metrics); err != nil { + if err := n.init(resourcesCtx, rollupCfg, log, setup, gossipIn, l2Chain, runCfg, metrics, clock); err != nil { closeErr := n.Close() if closeErr != nil { log.Error("failed to close p2p after starting with err", "closeErr", closeErr, "err", err) @@ -93,6 +94,7 @@ func (n *NodeP2P) init( l2Chain L2Chain, runCfg GossipRuntimeConfig, metrics metrics.Metricer, + clk clock.Clock, ) error { bwc := p2pmetrics.NewBandwidthCounter() @@ -162,7 +164,7 @@ func (n *NodeP2P) init( if err != nil { return fmt.Errorf("failed to start gossipsub router: %w", err) } - n.gsOut, err = JoinGossip(n.host.ID(), n.gs, log, rollupCfg, runCfg, gossipIn, setup) + n.gsOut, err = JoinGossip(n.host.ID(), n.gs, log, rollupCfg, runCfg, gossipIn, setup, clk) if err != nil { return fmt.Errorf("failed to join blocks gossip topic: %w", err) } 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 // diff --git a/op-service/client/ws.go b/op-service/client/ws.go new file mode 100644 index 00000000000..1f875560330 --- /dev/null +++ b/op-service/client/ws.go @@ -0,0 +1,200 @@ +package client + +import ( + "context" + "errors" + "net/http" + "time" + + "github.com/coder/websocket" + "github.com/ethereum/go-ethereum/log" + + "github.com/ethereum-optimism/optimism/op-service/retry" +) + +// WSConfig configures a websocket connection. +// This is the shared configuration type for all outbound websocket clients in the codebase. +// Higher level users can build additional behavior on top, but should prefer DialWS / WSClient +// instead of constructing websocket.Dial calls directly. +type WSConfig struct { + // URL is the websocket endpoint, e.g. wss://example:8546/ws. + URL string + // Headers are optional HTTP headers included in the websocket handshake. + Headers http.Header + + // DialTimeout bounds the initial websocket dial. + DialTimeout time.Duration + // ReadTimeout bounds individual Read calls when a context without deadline is used. + ReadTimeout time.Duration + // WriteTimeout bounds individual Write calls when a context without deadline is used. + WriteTimeout time.Duration + + // MaxAttempts configures how many dial attempts are made with backoff. + // Defaults to 1 if zero. + MaxAttempts int + // Backoff is the backoff strategy used between dial attempts. + // Defaults to retry.Exponential() if nil. + Backoff retry.Strategy + + // Log is used for connection level logging. + // If nil, logging is disabled. + Log log.Logger +} + +// applyDefaults fills empty WSConfig fields with conservative defaults. +func (c *WSConfig) applyDefaults() { + if c.DialTimeout == 0 { + c.DialTimeout = 10 * time.Second + } + if c.ReadTimeout == 0 { + c.ReadTimeout = 30 * time.Second + } + if c.WriteTimeout == 0 { + c.WriteTimeout = 30 * time.Second + } + if c.MaxAttempts < 1 { + c.MaxAttempts = 1 + } + if c.Backoff == nil { + c.Backoff = retry.Exponential() + } +} + +// WSClient is the canonical outbound websocket client for the monorepo. +// It wraps a coder/websocket connection, handles dialing with backoff, and exposes +// context-aware read/write helpers. New outbound websocket integrations should go +// through this type (or helpers built on top of it) rather than using websocket.Dial +// directly. +type WSClient struct { + conn *websocket.Conn + config WSConfig +} + +// DialWS establishes a websocket connection using the given configuration. +// It performs MaxAttempts connection attempts with the configured backoff. +func DialWS(ctx context.Context, cfg WSConfig) (*WSClient, error) { + cfg.applyDefaults() + + if cfg.Log != nil { + cfg.Log.Info("Dialing websocket", "url", cfg.URL) + } + + conn, err := retry.Do(ctx, cfg.MaxAttempts, cfg.Backoff, func() (*websocket.Conn, error) { + dialCtx, cancel := context.WithTimeout(ctx, cfg.DialTimeout) + defer cancel() + conn, resp, err := websocket.Dial(dialCtx, cfg.URL, &websocket.DialOptions{ + HTTPHeader: cfg.Headers, + }) + if resp != nil && resp.Body != nil { + _ = resp.Body.Close() + } + return conn, err + }) + if err != nil { + if cfg.Log != nil { + cfg.Log.Warn("Failed to dial websocket", "url", cfg.URL, "err", err) + } + return nil, err + } + + if cfg.Log != nil { + cfg.Log.Info("Websocket connection established", "url", cfg.URL) + } + + return &WSClient{ + conn: conn, + config: cfg, + }, nil +} + +// Close closes the websocket connection with the given status and reason. +func (c *WSClient) Close(status websocket.StatusCode, reason string) error { + if c == nil || c.conn == nil { + return nil + } + return c.conn.Close(status, reason) +} + +// Read reads the next message from the websocket connection. +// If the context has no deadline, a default read timeout from the config is applied. +func (c *WSClient) Read(ctx context.Context) (websocket.MessageType, []byte, error) { + if deadline, ok := ctx.Deadline(); !ok || time.Until(deadline) <= 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, c.config.ReadTimeout) + defer cancel() + } + return c.conn.Read(ctx) +} + +// Write writes a message to the websocket connection. +// If the context has no deadline, a default write timeout from the config is applied. +func (c *WSClient) Write(ctx context.Context, msgType websocket.MessageType, data []byte) error { + if deadline, ok := ctx.Deadline(); !ok || time.Until(deadline) <= 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, c.config.WriteTimeout) + defer cancel() + } + return c.conn.Write(ctx, msgType, data) +} + +// ReadAll streams all websocket messages for the given duration into the provided output channel. +// It closes the done channel (when provided) after finishing the read loop. +func (c *WSClient) ReadAll(ctx context.Context, logger log.Logger, duration time.Duration, output chan<- []byte, done chan<- struct{}) error { + if c == nil { + return errors.New("ws client is nil") + } + if logger == nil { + return errors.New("logger is nil") + } + if done != nil { + defer close(done) + } + + logger.Info("Listening on WebSocket client", "duration", duration) + + ctx, cancel := context.WithTimeout(ctx, duration) + defer cancel() + + for { + _, message, err := c.Read(ctx) + if err != nil { + if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { + logger.Info("WebSocket read finished") + return nil + } + logger.Error("Error reading WebSocket message", "error", err) + return err + } + logger.Debug("Received WebSocket message", "message_length", len(message)) + + select { + case output <- message: + case <-ctx.Done(): + logger.Info("Context done while sending message") + return nil + } + } +} + +// ProbeWS performs a lightweight websocket handshake against the given URL and closes immediately. +// It can be used in readiness checks to verify that the endpoint accepts websocket connections. +func ProbeWS(ctx context.Context, url string) error { + cfg := WSConfig{ + URL: url, + DialTimeout: 5 * time.Second, + MaxAttempts: 1, + } + cfg.applyDefaults() + + conn, err := DialWS(ctx, cfg) + if err != nil { + return err + } + // Close without waiting for the peer's close frame. Some flashblocks endpoints + // immediately drop the connection after a successful handshake which makes the + // full close handshake fail spuriously even though the endpoint is healthy. + if conn.conn == nil { + return nil + } + return conn.conn.CloseNow() +} diff --git a/op-service/client/ws_test.go b/op-service/client/ws_test.go new file mode 100644 index 00000000000..697b0604746 --- /dev/null +++ b/op-service/client/ws_test.go @@ -0,0 +1,99 @@ +package client + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/coder/websocket" + "github.com/ethereum/go-ethereum/log" +) + +// newTestLogger returns a no-op logger suitable for tests. +func newTestLogger(t *testing.T) log.Logger { + t.Helper() + // Root logger with no handlers is effectively silent. + return log.New() +} + +// wsEchoServer starts a simple websocket echo server for tests. +func wsEchoServer(t *testing.T) (*httptest.Server, string) { + t.Helper() + ctx := t.Context() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{ + CompressionMode: websocket.CompressionDisabled, + }) + if err != nil { + return + } + defer conn.Close(websocket.StatusNormalClosure, "test complete") + + for { + msgType, data, err := conn.Read(ctx) + if err != nil { + return + } + if err := conn.Write(ctx, msgType, data); err != nil { + return + } + } + }) + + srv := httptest.NewServer(handler) + + // Convert http://127.0.0.1:port to ws://127.0.0.1:port. + wsURL := "ws" + srv.URL[len("http"):] + return srv, wsURL +} + +func TestDialWSAndEcho(t *testing.T) { + ctx := t.Context() + srv, wsURL := wsEchoServer(t) + defer srv.Close() + + opCtx, cancelOp := context.WithTimeout(ctx, 5*time.Second) + defer cancelOp() + + client, err := DialWS(opCtx, WSConfig{ + URL: wsURL, + Log: newTestLogger(t), + }) + if err != nil { + t.Fatalf("DialWS failed: %v", err) + } + defer client.Close(websocket.StatusNormalClosure, "test complete") + + const payload = "hello over websocket" + + if err := client.Write(opCtx, websocket.MessageText, []byte(payload)); err != nil { + t.Fatalf("Write failed: %v", err) + } + + msgType, data, err := client.Read(opCtx) + if err != nil { + t.Fatalf("Read failed: %v", err) + } + if msgType != websocket.MessageText { + t.Fatalf("unexpected message type: %v", msgType) + } + if string(data) != payload { + t.Fatalf("unexpected payload: got %q, want %q", string(data), payload) + } +} + +func TestProbeWS(t *testing.T) { + ctx := t.Context() + srv, wsURL := wsEchoServer(t) + defer srv.Close() + + opCtx, cancelOp := context.WithTimeout(ctx, 5*time.Second) + defer cancelOp() + + if err := ProbeWS(opCtx, wsURL); err != nil { + t.Fatalf("ProbeWS failed: %v", err) + } +} diff --git a/op-supernode/supernode/chain_container/virtual_node/virtual_node.go b/op-supernode/supernode/chain_container/virtual_node/virtual_node.go index af0cd88e637..f5677ec6425 100644 --- a/op-supernode/supernode/chain_container/virtual_node/virtual_node.go +++ b/op-supernode/supernode/chain_container/virtual_node/virtual_node.go @@ -20,7 +20,7 @@ func defaultInnerNodeFactory(ctx context.Context, cfg *opnodecfg.Config, log get if initOverload != nil { overrides = *initOverload } - return rollupNode.NewWithOverride(ctx, cfg, log, appVersion, m, overrides) + return rollupNode.NewWithOverride(ctx, cfg, log, appVersion, m, nil, overrides) } var ( diff --git a/ops/docker/op-stack-go/Dockerfile b/ops/docker/op-stack-go/Dockerfile index c0a77a6b22e..57052e7101b 100644 --- a/ops/docker/op-stack-go/Dockerfile +++ b/ops/docker/op-stack-go/Dockerfile @@ -16,10 +16,6 @@ ARG UBUNTU_TARGET_BASE_IMAGE=ubuntu:22.04 # The only build that uses this is `op-challenger-target`. ARG KONA_VERSION=none -# The version of asterisc to use. -# The only build that uses this is `op-challenger-target`. -ARG ASTERISC_VERSION=none - # builder_foundry does not need to be built on $BUILDPLATFORM, as foundry produces static binaries. FROM alpine:3.21 AS builder_foundry @@ -222,25 +218,20 @@ CMD ["op-node"] # Make the kona docker image published by upstream available as a source to copy kona from. FROM ghcr.io/op-rs/kona/kona-host:$KONA_VERSION AS kona -# Make the asterisc docker image published by upstream available as a source to copy asterisc from. -FROM us-docker.pkg.dev/oplabs-tools-artifacts/images/asterisc:$ASTERISC_VERSION AS asterisc -# Also produce an op-challenger loaded with kona and asterisc using ubuntu +# Also produce an op-challenger loaded with kona using ubuntu FROM $UBUNTU_TARGET_BASE_IMAGE AS op-challenger-target RUN apt-get update && apt-get install -y --no-install-recommends musl openssl ca-certificates COPY --from=op-challenger-builder /app/op-challenger/bin/op-challenger /usr/local/bin/ # Copy in op-program and cannon COPY --from=op-program-builder /app/op-program/bin/op-program /usr/local/bin/ -ENV OP_CHALLENGER_ASTERISC_SERVER=/usr/local/bin/op-program ENV OP_CHALLENGER_CANNON_SERVER=/usr/local/bin/op-program COPY --from=cannon-builder /app/cannon/bin/cannon /usr/local/bin/ ENV OP_CHALLENGER_CANNON_BIN=/usr/local/bin/cannon -# Copy in kona and asterisc +# Copy in kona COPY --from=kona /usr/local/bin/kona-host /usr/local/bin/ ENV OP_CHALLENGER_ASTERISC_KONA_SERVER=/usr/local/bin/kona-host ENV OP_CHALLENGER_CANNON_KONA_SERVER=/usr/local/bin/kona-host -COPY --from=asterisc /usr/local/bin/asterisc /usr/local/bin/ -ENV OP_CHALLENGER_ASTERISC_BIN=/usr/local/bin/asterisc CMD ["op-challenger"] FROM $TARGET_BASE_IMAGE AS op-dispute-mon-target diff --git a/packages/contracts-bedrock/interfaces/safe/ILivenessModule2.sol b/packages/contracts-bedrock/interfaces/safe/ILivenessModule2.sol index 921d1a794c7..6a40033d7ea 100644 --- a/packages/contracts-bedrock/interfaces/safe/ILivenessModule2.sol +++ b/packages/contracts-bedrock/interfaces/safe/ILivenessModule2.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +interface ISafe { } + /// @title ILivenessModule2 /// @notice Interface for LivenessModule2, a singleton module for challenge-based ownership transfer interface ILivenessModule2 { @@ -10,18 +12,34 @@ interface ILivenessModule2 { address fallbackOwner; } + event ChallengeCancelled(address indexed safe); + event ChallengeStarted(address indexed safe, uint256 challengeStartTime); + event ChallengeSucceeded(address indexed safe, address fallbackOwner); + event ModuleCleared(address indexed safe); + event ModuleConfigured(address indexed safe, uint256 livenessResponsePeriod, address fallbackOwner); + + error LivenessModule2_ChallengeAlreadyExists(); + error LivenessModule2_ChallengeDoesNotExist(); + error LivenessModule2_InvalidFallbackOwner(); + error LivenessModule2_InvalidResponsePeriod(); + error LivenessModule2_InvalidVersion(); + error LivenessModule2_ModuleNotConfigured(); + error LivenessModule2_ModuleNotEnabled(); + error LivenessModule2_ModuleStillEnabled(); + error LivenessModule2_OwnershipTransferFailed(); + error LivenessModule2_ResponsePeriodActive(); + error LivenessModule2_ResponsePeriodEnded(); + error LivenessModule2_UnauthorizedCaller(); + error SemverComp_InvalidSemverParts(); + /// @notice Returns the configuration for a Safe - /// @return livenessResponsePeriod The response period - /// @return fallbackOwner The fallback owner address - function livenessSafeConfiguration(address) external view returns (uint256 livenessResponsePeriod, address fallbackOwner); + /// @param _safe The Safe to query + /// @return The ModuleConfig for the Safe + function livenessSafeConfiguration(ISafe _safe) external view returns (ModuleConfig memory); /// @notice Returns the challenge start time for a Safe (0 if no challenge) /// @return The challenge start timestamp - function challengeStartTime(address) external view returns (uint256); - - /// @notice Semantic version - /// @return version The contract version - function version() external view returns (string memory); + function challengeStartTime(ISafe) external view returns (uint256); /// @notice Configures the module for a Safe that has already enabled it /// @param _config The configuration parameters for the module @@ -33,16 +51,16 @@ interface ILivenessModule2 { /// @notice Returns challenge_start_time + liveness_response_period if there is a challenge, or 0 if not /// @param _safe The Safe address to query /// @return The challenge end timestamp, or 0 if no challenge - function getChallengePeriodEnd(address _safe) external view returns (uint256); + function getChallengePeriodEnd(ISafe _safe) external view returns (uint256); /// @notice Challenges an enabled safe /// @param _safe The Safe to challenge - function challenge(address _safe) external; + function challenge(ISafe _safe) external; /// @notice Responds to a challenge for an enabled safe, canceling it function respond() external; /// @notice Removes all current owners from an enabled safe and appoints fallback as sole owner /// @param _safe The Safe to transfer ownership of - function changeOwnershipToFallback(address _safe) external; + function changeOwnershipToFallback(ISafe _safe) external; } diff --git a/packages/contracts-bedrock/interfaces/safe/ISaferSafes.sol b/packages/contracts-bedrock/interfaces/safe/ISaferSafes.sol index 7fc50d05eda..27044452cf3 100644 --- a/packages/contracts-bedrock/interfaces/safe/ISaferSafes.sol +++ b/packages/contracts-bedrock/interfaces/safe/ISaferSafes.sol @@ -1,68 +1,28 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.15; +pragma solidity ^0.8.0; -import {GnosisSafe} from "safe-contracts/GnosisSafe.sol"; -import {Enum} from "safe-contracts/common/Enum.sol"; -import {ISemver} from "interfaces/universal/ISemver.sol"; +import { ITimelockGuard, IEnum, ISafe } from "interfaces/safe/ITimelockGuard.sol"; +import { ILivenessModule2 } from "interfaces/safe/ILivenessModule2.sol"; +import { ISemver } from "interfaces/universal/ISemver.sol"; interface ISaferSafes is ISemver { - struct ModuleConfig { - uint256 livenessResponsePeriod; - address fallbackOwner; - } - - struct ExecTransactionParams { - address to; - uint256 value; - bytes data; - Enum.Operation operation; - uint256 safeTxGas; - uint256 baseGas; - uint256 gasPrice; - address gasToken; - address payable refundReceiver; - } - - enum TransactionState { - PENDING, - CANCELLED, - EXECUTED - } - - struct ScheduledTransaction { - uint256 executionTime; - TransactionState state; - ExecTransactionParams params; - } - - event CancellationThresholdUpdated( - GnosisSafe indexed safe, - uint256 oldThreshold, - uint256 newThreshold - ); + event CancellationThresholdUpdated(ISafe indexed safe, uint256 oldThreshold, uint256 newThreshold); event ChallengeCancelled(address indexed safe); event ChallengeStarted(address indexed safe, uint256 challengeStartTime); event ChallengeSucceeded(address indexed safe, address fallbackOwner); - event GuardConfigured(GnosisSafe indexed safe, uint256 timelockDelay); + event GuardConfigured(ISafe indexed safe, uint256 timelockDelay); event Message(string message); event ModuleCleared(address indexed safe); - event ModuleConfigured( - address indexed safe, - uint256 livenessResponsePeriod, - address fallbackOwner - ); - event TransactionCancelled(GnosisSafe indexed safe, bytes32 indexed txHash); - event TransactionExecuted(GnosisSafe indexed safe, bytes32 txHash); - event TransactionScheduled( - GnosisSafe indexed safe, - bytes32 indexed txHash, - uint256 executionTime - ); + event ModuleConfigured(address indexed safe, uint256 livenessResponsePeriod, address fallbackOwner); + event TransactionCancelled(ISafe indexed safe, bytes32 indexed txHash); + event TransactionExecuted(ISafe indexed safe, bytes32 indexed txHash); + event TransactionScheduled(ISafe indexed safe, bytes32 indexed txHash, uint256 executionTime); error LivenessModule2_ChallengeAlreadyExists(); error LivenessModule2_ChallengeDoesNotExist(); error LivenessModule2_InvalidFallbackOwner(); error LivenessModule2_InvalidResponsePeriod(); + error LivenessModule2_InvalidVersion(); error LivenessModule2_ModuleNotConfigured(); error LivenessModule2_ModuleNotEnabled(); error LivenessModule2_ModuleStillEnabled(); @@ -74,8 +34,10 @@ interface ISaferSafes is ISemver { error SemverComp_InvalidSemverParts(); error TimelockGuard_GuardNotConfigured(); error TimelockGuard_GuardNotEnabled(); + error TimelockGuard_GuardStillEnabled(); error TimelockGuard_InvalidTimelockDelay(); error TimelockGuard_InvalidVersion(); + error TimelockGuard_NotOwner(); error TimelockGuard_TransactionAlreadyCancelled(); error TimelockGuard_TransactionAlreadyExecuted(); error TimelockGuard_TransactionAlreadyScheduled(); @@ -83,21 +45,20 @@ interface ISaferSafes is ISemver { error TimelockGuard_TransactionNotScheduled(); function cancelTransaction( - GnosisSafe _safe, + ISafe _safe, bytes32 _txHash, uint256 _nonce, bytes calldata _signatures - ) external; + ) + external; - function cancellationThreshold( - GnosisSafe _safe - ) external view returns (uint256); + function cancellationThreshold(ISafe _safe) external view returns (uint256); - function challenge(address _safe) external; + function challenge(ISafe _safe) external; - function challengeStartTime(address _safe) external view returns (uint256); + function challengeStartTime(ISafe) external view returns (uint256); - function changeOwnershipToFallback(address _safe) external; + function changeOwnershipToFallback(ISafe _safe) external; function checkAfterExecution(bytes32 _txHash, bool _success) external; @@ -105,60 +66,56 @@ interface ISaferSafes is ISemver { address _to, uint256 _value, bytes calldata _data, - Enum.Operation _operation, + IEnum.Operation _operation, uint256 _safeTxGas, uint256 _baseGas, uint256 _gasPrice, address _gasToken, address payable _refundReceiver, bytes calldata, - address - ) external view; + address _msgSender + ) + external; function clearLivenessModule() external; - function configureLivenessModule(ModuleConfig calldata _config) external; + function clearTimelockGuard() external; + + function configureLivenessModule(ILivenessModule2.ModuleConfig calldata _config) external; function configureTimelockGuard(uint256 _timelockDelay) external; - function getChallengePeriodEnd( - address _safe - ) external view returns (uint256); + function getChallengePeriodEnd(ISafe _safe) external view returns (uint256); - function livenessSafeConfiguration( - address _safe - ) - external - view - returns (uint256 livenessResponsePeriod, address fallbackOwner); + function livenessSafeConfiguration(ISafe _safe) external view returns (ILivenessModule2.ModuleConfig memory); - function maxCancellationThreshold( - GnosisSafe _safe - ) external view returns (uint256); + function maxCancellationThreshold(ISafe _safe) external view returns (uint256); - function pendingTransactions( - GnosisSafe _safe - ) external view returns (ScheduledTransaction[] memory); + function pendingTransactions(ISafe _safe) external view returns (ITimelockGuard.ScheduledTransaction[] memory); function respond() external; function scheduleTransaction( - GnosisSafe _safe, + ISafe _safe, uint256 _nonce, - ExecTransactionParams calldata _params, + ITimelockGuard.ExecTransactionParams calldata _params, bytes calldata _signatures - ) external; + ) + external; function scheduledTransaction( - GnosisSafe _safe, + ISafe _safe, bytes32 _txHash - ) external view returns (ScheduledTransaction memory); + ) + external + view + returns (ITimelockGuard.ScheduledTransaction memory); + + function signCancellation(bytes32) external; - function signCancellation(bytes32 _txHash) external; + function supportsInterface(bytes4 interfaceId) external view returns (bool); - function timelockConfiguration( - GnosisSafe _safe - ) external view returns (uint256); + function timelockDelay(ISafe _safe) external view returns (uint256); function version() external pure returns (string memory); } diff --git a/packages/contracts-bedrock/interfaces/safe/ITimelockGuard.sol b/packages/contracts-bedrock/interfaces/safe/ITimelockGuard.sol index 6ab35526a74..86871be9ecc 100644 --- a/packages/contracts-bedrock/interfaces/safe/ITimelockGuard.sol +++ b/packages/contracts-bedrock/interfaces/safe/ITimelockGuard.sol @@ -1,8 +1,13 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.4; +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; -library Enum { - type Operation is uint8; +interface ISafe { } + +interface IEnum { + enum Operation { + Call, + DelegateCall + } } interface ITimelockGuard { @@ -25,7 +30,7 @@ interface ITimelockGuard { address to; uint256 value; bytes data; - Enum.Operation operation; + IEnum.Operation operation; uint256 safeTxGas; uint256 baseGas; uint256 gasPrice; @@ -37,58 +42,60 @@ interface ITimelockGuard { error TimelockGuard_GuardNotEnabled(); error TimelockGuard_GuardStillEnabled(); error TimelockGuard_InvalidTimelockDelay(); + error TimelockGuard_NotOwner(); error TimelockGuard_TransactionAlreadyCancelled(); error TimelockGuard_TransactionAlreadyScheduled(); error TimelockGuard_TransactionNotScheduled(); error TimelockGuard_TransactionNotReady(); error TimelockGuard_TransactionAlreadyExecuted(); error TimelockGuard_InvalidVersion(); + error SemverComp_InvalidSemverParts(); - event CancellationThresholdUpdated(address indexed safe, uint256 oldThreshold, uint256 newThreshold); - event GuardConfigured(address indexed safe, uint256 timelockDelay); - event TransactionCancelled(address indexed safe, bytes32 indexed txHash); - event TransactionScheduled(address indexed safe, bytes32 indexed txHash, uint256 executionTime); - event TransactionExecuted(address indexed safe, bytes32 indexed txHash); + event CancellationThresholdUpdated(ISafe indexed safe, uint256 oldThreshold, uint256 newThreshold); + event GuardConfigured(ISafe indexed safe, uint256 timelockDelay); + event TransactionCancelled(ISafe indexed safe, bytes32 indexed txHash); + event TransactionScheduled(ISafe indexed safe, bytes32 indexed txHash, uint256 executionTime); + event TransactionExecuted(ISafe indexed safe, bytes32 indexed txHash); event Message(string message); - event TransactionsNotCancelled(address indexed safe, uint256 uncancelledCount); - function cancelTransaction(address _safe, bytes32 _txHash, uint256 _nonce, bytes memory _signatures) external; - function signCancellation(bytes32 _txHash) external; - function cancellationThreshold(address _safe) external view returns (uint256); + function cancelTransaction(ISafe _safe, bytes32 _txHash, uint256 _nonce, bytes memory _signatures) external; + function signCancellation(bytes32) external; + function cancellationThreshold(ISafe _safe) external view returns (uint256); + function supportsInterface(bytes4 interfaceId) external view returns (bool); function checkTransaction( address _to, uint256 _value, bytes memory _data, - Enum.Operation _operation, + IEnum.Operation _operation, uint256 _safeTxGas, uint256 _baseGas, uint256 _gasPrice, address _gasToken, address payable _refundReceiver, - bytes memory _signatures, + bytes memory, address _msgSender ) external; - function checkAfterExecution(bytes32, bool) external; + function checkAfterExecution(bytes32 _txHash, bool _success) external; function configureTimelockGuard(uint256 _timelockDelay) external; + function clearTimelockGuard() external; function scheduledTransaction( - address _safe, + ISafe _safe, bytes32 _txHash ) external view returns (ScheduledTransaction memory); - function safeConfigs(address) external view returns (uint256 timelockDelay); function scheduleTransaction( - address _safe, + ISafe _safe, uint256 _nonce, ExecTransactionParams memory _params, bytes memory _signatures ) external; - function timelockConfiguration(address _safe) external view returns (uint256 timelockDelay); - function maxCancellationThreshold(address _safe) external view returns (uint256); - function pendingTransactions(address _safe) + function timelockDelay(ISafe _safe) external view returns (uint256); + function maxCancellationThreshold(ISafe _safe) external view returns (uint256); + function pendingTransactions(ISafe _safe) external view returns (ScheduledTransaction[] memory); diff --git a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol index abe36f4cef9..b70d7ade169 100644 --- a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol @@ -169,6 +169,11 @@ contract Deploy is Deployer { deploySuperchain(); } + // Override useCustomGasToken config if system feature flag is set + if (Config.sysFeatureCustomGasToken()) { + cfg.setUseCustomGasToken(true); + } + deployImplementations({ _isInterop: cfg.useInterop() }); // Deploy Current OPChain Contracts diff --git a/packages/contracts-bedrock/scripts/deploy/DeploySaferSafes.s.sol b/packages/contracts-bedrock/scripts/deploy/DeploySaferSafes.s.sol new file mode 100644 index 00000000000..3c411baed94 --- /dev/null +++ b/packages/contracts-bedrock/scripts/deploy/DeploySaferSafes.s.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +// Forge +import { Script } from "forge-std/Script.sol"; + +// Scripts +import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; + +// Interfaces +import { ISaferSafes } from "interfaces/safe/ISaferSafes.sol"; + +// Libraries +import { SemverComp } from "src/libraries/SemverComp.sol"; + +/// @title DeploySaferSafes +/// @notice Deploys the SaferSafes singleton contract using CREATE2 with the default salt. +contract DeploySaferSafes is Script { + struct Output { + ISaferSafes saferSafesSingleton; + } + + /// @notice Deploys SaferSafes and returns the output struct. + function run() public returns (Output memory output_) { + output_ = _deploy(); + assertValidOutput(output_); + } + + /// @notice Deploys SaferSafes without broadcasting (for use by other scripts). + function _deploy() internal returns (Output memory output_) { + output_.saferSafesSingleton = ISaferSafes( + DeployUtils.createDeterministic({ + _name: "SaferSafes", + _args: DeployUtils.encodeConstructor(bytes("")), + _salt: DeployUtils.DEFAULT_SALT + }) + ); + vm.label(address(output_.saferSafesSingleton), "SaferSafesSingleton"); + } + + /// @notice Validates the deployment output. + function assertValidOutput(Output memory _output) public view { + DeployUtils.assertValidContractAddress(address(_output.saferSafesSingleton)); + + require(SemverComp.eq(_output.saferSafesSingleton.version(), "1.10.1"), "DeploySaferSafes: unexpected version"); + } +} diff --git a/packages/contracts-bedrock/scripts/libraries/Config.sol b/packages/contracts-bedrock/scripts/libraries/Config.sol index 60653866f7c..4b5a2ed6339 100644 --- a/packages/contracts-bedrock/scripts/libraries/Config.sol +++ b/packages/contracts-bedrock/scripts/libraries/Config.sol @@ -286,13 +286,13 @@ library Config { return vm.envOr("DEV_FEATURE__DEPLOY_V2_DISPUTE_GAMES", false); } - /// @notice Returns true if the development feature custom gas token is enabled. - function devFeatureCustomGasToken() internal view returns (bool) { - return vm.envOr("DEV_FEATURE__CUSTOM_GAS_TOKEN", false); - } - /// @notice Returns true if the development feature opcm_v2 is enabled. function devFeatureOpcmV2() internal view returns (bool) { return vm.envOr("DEV_FEATURE__OPCM_V2", false); } + + /// @notice Returns true if the system feature custom_gas_token is enabled. + function sysFeatureCustomGasToken() internal view returns (bool) { + return vm.envOr("SYS_FEATURE__CUSTOM_GAS_TOKEN", false); + } } diff --git a/packages/contracts-bedrock/src/libraries/DevFeatures.sol b/packages/contracts-bedrock/src/libraries/DevFeatures.sol index 33a372c63b0..ed46b8c8d2b 100644 --- a/packages/contracts-bedrock/src/libraries/DevFeatures.sol +++ b/packages/contracts-bedrock/src/libraries/DevFeatures.sol @@ -22,10 +22,6 @@ library DevFeatures { bytes32 public constant DEPLOY_V2_DISPUTE_GAMES = bytes32(0x0000000000000000000000000000000000000000000000000000000000000100); - /// @notice The feature that enables the custom gas token. - bytes32 public constant CUSTOM_GAS_TOKEN = - bytes32(0x0000000000000000000000000000000000000000000000000000000000001000); - /// @notice The feature that enables the OPContractsManagerV2 contract. bytes32 public constant OPCM_V2 = bytes32(0x0000000000000000000000000000000000000000000000000000000000010000); diff --git a/packages/contracts-bedrock/test/L1/FeesDepositor.t.sol b/packages/contracts-bedrock/test/L1/FeesDepositor.t.sol index 43b28d21150..e329f243869 100644 --- a/packages/contracts-bedrock/test/L1/FeesDepositor.t.sol +++ b/packages/contracts-bedrock/test/L1/FeesDepositor.t.sol @@ -9,7 +9,6 @@ import { FeesDepositor } from "src/L1/FeesDepositor.sol"; import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; import { Proxy } from "src/universal/Proxy.sol"; import { Features } from "src/libraries/Features.sol"; -import { DevFeatures } from "src/libraries/DevFeatures.sol"; /// @title FeesDepositor_TestInit /// @notice Base test contract with initialization for `FeesDepositor` tests. @@ -98,7 +97,7 @@ contract FeesDepositor_Receive_Test is FeesDepositor_TestInit { } function testFuzz_receive_atOrAboveThreshold_succeeds(uint256 _sendAmount) external { - skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); // Handling the fork tests scenario case for the fork tests uint256 depositFeesRecipientBalanceBefore = depositFeesRecipient.balance; @@ -126,7 +125,7 @@ contract FeesDepositor_Receive_Test is FeesDepositor_TestInit { } function testFuzz_receive_multipleDeposits_succeeds(uint256 _firstAmount, uint256 _secondAmount) external { - skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); // Handling the fork tests scenario uint256 depositFeesRecipientBalanceBefore = depositFeesRecipient.balance; diff --git a/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol b/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol index e26b938a9c2..abec06bf78b 100644 --- a/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol +++ b/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol @@ -15,7 +15,6 @@ import { Predeploys } from "src/libraries/Predeploys.sol"; import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import { Features } from "src/libraries/Features.sol"; -import { DevFeatures } from "src/libraries/DevFeatures.sol"; // Interfaces import { ICrossDomainMessenger } from "interfaces/universal/ICrossDomainMessenger.sol"; @@ -333,7 +332,7 @@ contract L1StandardBridge_Paused_Test is CommonTest { contract L1StandardBridge_Receive_Test is CommonTest { /// @notice Tests receive bridges ETH successfully. function test_receive_succeeds() external { - skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); uint256 portalBalanceBefore = address(optimismPortal2).balance; uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; @@ -377,6 +376,20 @@ contract L1StandardBridge_Receive_Test is CommonTest { (bool revertsAsExpected,) = address(l1StandardBridge).call{ value: 100 }(hex""); assertTrue(revertsAsExpected, "expectRevert: call did not revert"); } + + /// @notice Tests that receive reverts when custom gas token is enabled and value is sent. + function testFuzz_receive_withCustomGasToken_reverts(uint256 _value) external { + skipIfSysFeatureDisabled(Features.CUSTOM_GAS_TOKEN); + + _value = bound(_value, 1, type(uint128).max); + vm.deal(alice, _value); + + vm.prank(alice, alice); + vm.expectRevert(IOptimismPortal2.OptimismPortal_NotAllowedOnCGTMode.selector); + + (bool revertsAsExpected,) = address(l1StandardBridge).call{ value: _value }(hex""); + assertTrue(revertsAsExpected, "expectRevert: call did not revert"); + } } /// @title L1StandardBridge_DepositETH_Test @@ -388,7 +401,7 @@ contract L1StandardBridge_DepositETH_Test is L1StandardBridge_TestInit { /// Only EOA can call depositETH. /// ETH ends up in the optimismPortal. function test_depositETH_fromEOA_succeeds() external { - skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); _preBridgeETH({ isLegacy: true, value: 500 }); uint256 portalBalanceBefore = address(optimismPortal2).balance; uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; @@ -404,7 +417,7 @@ contract L1StandardBridge_DepositETH_Test is L1StandardBridge_TestInit { /// @notice Tests that depositing ETH succeeds for an EOA using 7702 delegation. function test_depositETH_fromEOA7702_succeeds() external { - skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); // Set alice to have 7702 code. vm.etch(alice, abi.encodePacked(hex"EF0100", address(0))); @@ -428,6 +441,32 @@ contract L1StandardBridge_DepositETH_Test is L1StandardBridge_TestInit { vm.prank(alice); l1StandardBridge.depositETH{ value: 1 }(300, hex""); } + + /// @notice Tests that depositETH reverts when custom gas token is enabled and value is sent. + function testFuzz_depositETH_withCustomGasToken_reverts(uint256 _value, uint32 _minGasLimit) external { + skipIfSysFeatureDisabled(Features.CUSTOM_GAS_TOKEN); + + _value = bound(_value, 1, type(uint128).max); + vm.deal(alice, _value); + + vm.prank(alice, alice); + vm.expectRevert(IOptimismPortal2.OptimismPortal_NotAllowedOnCGTMode.selector); + l1StandardBridge.depositETH{ value: _value }(_minGasLimit, hex"dead"); + } + + /// @notice Tests that depositETH reverts when custom gas token is enabled for EOA with 7702 delegation. + function testFuzz_depositETH_fromEOA7702WithCustomGasToken_reverts(uint256 _value, uint32 _minGasLimit) external { + skipIfSysFeatureDisabled(Features.CUSTOM_GAS_TOKEN); + _value = bound(_value, 1, type(uint128).max); + + // Set alice to have 7702 code. + vm.etch(alice, abi.encodePacked(hex"EF0100", address(0))); + + vm.deal(alice, _value); + vm.prank(alice, alice); + vm.expectRevert(IOptimismPortal2.OptimismPortal_NotAllowedOnCGTMode.selector); + l1StandardBridge.depositETH{ value: _value }(_minGasLimit, hex"dead"); + } } /// @title L1StandardBridge_DepositETHTo_Test @@ -439,7 +478,7 @@ contract L1StandardBridge_DepositETHTo_Test is L1StandardBridge_TestInit { /// EOA or contract can call depositETHTo. /// ETH ends up in the optimismPortal. function test_depositETHTo_succeeds() external { - skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); _preBridgeETHTo({ isLegacy: true, value: 600 }); uint256 portalBalanceBefore = address(optimismPortal2).balance; uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; @@ -457,7 +496,7 @@ contract L1StandardBridge_DepositETHTo_Test is L1StandardBridge_TestInit { /// @param _to Random recipient address /// @param _amount Random ETH amount to deposit function testFuzz_depositETHTo_randomRecipient_succeeds(address _to, uint256 _amount) external { - skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); vm.assume(_to != address(0)); _amount = bound(_amount, 1, 10 ether); @@ -475,6 +514,23 @@ contract L1StandardBridge_DepositETHTo_Test is L1StandardBridge_TestInit { assertEq(address(optimismPortal2).balance, portalBalanceBefore + _amount); } } + + /// @notice Tests that depositETHTo reverts when custom gas token is enabled and value is sent. + function testFuzz_depositETHTo_withCustomGasToken_reverts( + address _to, + uint256 _value, + uint32 _minGasLimit + ) + external + { + skipIfSysFeatureDisabled(Features.CUSTOM_GAS_TOKEN); + vm.assume(_to != address(0)); + _value = bound(_value, 1, type(uint128).max); + vm.deal(alice, _value); + vm.prank(alice); + vm.expectRevert(IOptimismPortal2.OptimismPortal_NotAllowedOnCGTMode.selector); + l1StandardBridge.depositETHTo{ value: _value }(_to, _minGasLimit, hex"dead"); + } } /// @title L1StandardBridge_DepositERC20_Test @@ -786,7 +842,7 @@ contract L1StandardBridge_Uncategorized_Test is L1StandardBridge_TestInit { /// Only EOA can call bridgeETH. /// ETH ends up in the optimismPortal. function test_bridgeETH_succeeds() external { - skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); _preBridgeETH({ isLegacy: false, value: 500 }); uint256 portalBalanceBefore = address(optimismPortal2).balance; uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; @@ -806,7 +862,7 @@ contract L1StandardBridge_Uncategorized_Test is L1StandardBridge_TestInit { /// Only EOA can call bridgeETHTo. /// ETH ends up in the optimismPortal. function test_bridgeETHTo_succeeds() external { - skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); _preBridgeETHTo({ isLegacy: false, value: 600 }); uint256 portalBalanceBefore = address(optimismPortal2).balance; uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; @@ -878,4 +934,35 @@ contract L1StandardBridge_Uncategorized_Test is L1StandardBridge_TestInit { vm.expectRevert("StandardBridge: cannot send to messenger"); l1StandardBridge.finalizeBridgeETH{ value: 100 }(alice, messenger, 100, hex""); } + + /// @notice Tests that bridgeETH reverts when custom gas token is enabled and value is sent. + function testFuzz_bridgeETH_withCustomGasToken_reverts(uint256 _value, uint32 _minGasLimit) external { + skipIfSysFeatureDisabled(Features.CUSTOM_GAS_TOKEN); + + _value = bound(_value, 1, type(uint128).max); + vm.deal(alice, _value); + + vm.prank(alice, alice); + vm.expectRevert(IOptimismPortal2.OptimismPortal_NotAllowedOnCGTMode.selector); + l1StandardBridge.bridgeETH{ value: _value }(_minGasLimit, hex"dead"); + } + + /// @notice Tests that bridgeETHTo reverts when custom gas token is enabled and value is sent. + function testFuzz_bridgeETHTo_withCustomGasToken_reverts( + address _to, + uint256 _value, + uint32 _minGasLimit + ) + external + { + skipIfSysFeatureDisabled(Features.CUSTOM_GAS_TOKEN); + + vm.assume(_to != address(0)); + _value = bound(_value, 1, type(uint128).max); + vm.deal(alice, _value); + + vm.prank(alice); + vm.expectRevert(IOptimismPortal2.OptimismPortal_NotAllowedOnCGTMode.selector); + l1StandardBridge.bridgeETHTo{ value: _value }(_to, _minGasLimit, hex"dead"); + } } diff --git a/packages/contracts-bedrock/test/L2/FeeSplitter.t.sol b/packages/contracts-bedrock/test/L2/FeeSplitter.t.sol index 0ed14418f63..35edca9a278 100644 --- a/packages/contracts-bedrock/test/L2/FeeSplitter.t.sol +++ b/packages/contracts-bedrock/test/L2/FeeSplitter.t.sol @@ -13,7 +13,6 @@ import { ReentrantMockFeeVault } from "test/mocks/ReentrantMockFeeVault.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; import { Types } from "src/libraries/Types.sol"; -import { DevFeatures } from "src/libraries/DevFeatures.sol"; // Interfaces import { IFeeSplitter } from "interfaces/L2/IFeeSplitter.sol"; @@ -42,10 +41,6 @@ contract FeeSplitter_TestInit is CommonTest { /// @notice Test setup. function setUp() public virtual override { - // Resolve features and skip whole test suite if custom gas token is enabled - resolveFeaturesFromEnv(); - skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); - // Enable revenue sharing before calling parent setUp super.enableRevenueShare(); super.setUp(); diff --git a/packages/contracts-bedrock/test/L2/FeeVault.t.sol b/packages/contracts-bedrock/test/L2/FeeVault.t.sol index fcdb1e464a9..aa8a572335b 100644 --- a/packages/contracts-bedrock/test/L2/FeeVault.t.sol +++ b/packages/contracts-bedrock/test/L2/FeeVault.t.sol @@ -8,12 +8,13 @@ import { CommonTest } from "test/setup/CommonTest.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; import { IFeeVault } from "interfaces/L2/IFeeVault.sol"; import { IL2ToL1MessagePasser } from "interfaces/L2/IL2ToL1MessagePasser.sol"; +import { IL2ToL1MessagePasserCGT } from "interfaces/L2/IL2ToL1MessagePasserCGT.sol"; // Libraries import { Hashing } from "src/libraries/Hashing.sol"; import { Types } from "src/libraries/Types.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; -import { DevFeatures } from "src/libraries/DevFeatures.sol"; +import { Features } from "src/libraries/Features.sol"; /// @title FeeVault_Uncategorized_Test /// @notice Abstract test contract for fee feeVault testing. @@ -83,7 +84,7 @@ abstract contract FeeVault_Uncategorized_Test is CommonTest { /// @notice Tests that `withdraw` successfully initiates a withdrawal to L1. function test_withdraw_toL1_succeeds() external { - skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); // Setup L1 withdrawal vm.prank(IProxyAdmin(Predeploys.PROXY_ADMIN).owner()); @@ -145,6 +146,31 @@ abstract contract FeeVault_Uncategorized_Test is CommonTest { assertEq(Predeploys.L2_TO_L1_MESSAGE_PASSER.balance, amount); } + /// @notice Tests that withdraw to L1 reverts when custom gas token is enabled and value is sent. + function testFuzz_withdraw_toL1WithCustomGasToken_reverts(uint256 _amount) external { + skipIfSysFeatureDisabled(Features.CUSTOM_GAS_TOKEN); + + // Setup L1 withdrawal + vm.prank(IProxyAdmin(Predeploys.PROXY_ADMIN).owner()); + feeVault.setWithdrawalNetwork(Types.WithdrawalNetwork.L1); + + // Set recipient + vm.prank(IProxyAdmin(Predeploys.PROXY_ADMIN).owner()); + feeVault.setRecipient(recipient); + + // Set minimum withdrawal amount + vm.prank(IProxyAdmin(Predeploys.PROXY_ADMIN).owner()); + feeVault.setMinWithdrawalAmount(minWithdrawalAmount); + + // Set the balance to be greater than the minimum withdrawal amount + _amount = bound(_amount, feeVault.minWithdrawalAmount() + 1, type(uint128).max); + vm.deal(address(feeVault), _amount); + + // Withdrawal should revert due to CGT mode + vm.expectRevert(IL2ToL1MessagePasserCGT.L2ToL1MessagePasserCGT_NotAllowedOnCGTMode.selector); + feeVault.withdraw(); + } + /// @notice Tests that `withdraw` successfully initiates a withdrawal to L2. function test_withdraw_toL2_succeeds() public { _setupL2Withdrawal(); diff --git a/packages/contracts-bedrock/test/L2/L1Block.t.sol b/packages/contracts-bedrock/test/L2/L1Block.t.sol index 444587bb120..5d36bb88631 100644 --- a/packages/contracts-bedrock/test/L2/L1Block.t.sol +++ b/packages/contracts-bedrock/test/L2/L1Block.t.sol @@ -9,7 +9,7 @@ import { stdStorage, StdStorage } from "forge-std/Test.sol"; import { Encoding } from "src/libraries/Encoding.sol"; import { Constants } from "src/libraries/Constants.sol"; import "src/libraries/L1BlockErrors.sol"; -import { DevFeatures } from "src/libraries/DevFeatures.sol"; +import { Features } from "src/libraries/Features.sol"; // Interfaces import { IL1BlockCGT } from "interfaces/L2/IL1BlockCGT.sol"; @@ -54,7 +54,7 @@ contract L1Block_GasPayingToken_Test is L1Block_TestInit { /// @notice Tests that the `gasPayingToken` function returns the correct token address and /// decimals. function test_gasPayingToken_succeeds() external { - skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); (address token, uint8 decimals) = l1Block.gasPayingToken(); assertEq(token, Constants.ETHER); assertEq(uint256(decimals), uint256(18)); @@ -62,7 +62,7 @@ contract L1Block_GasPayingToken_Test is L1Block_TestInit { /// @notice Tests that the `gasPayingToken` function reverts when custom gas token is enabled. function test_gasPayingToken_customGasToken_reverts() external { - skipIfDevFeatureDisabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureDisabled(Features.CUSTOM_GAS_TOKEN); vm.expectRevert("L1BlockCGT: deprecated"); l1Block.gasPayingToken(); } @@ -73,14 +73,14 @@ contract L1Block_GasPayingToken_Test is L1Block_TestInit { contract L1Block_GasPayingTokenName_Test is L1Block_TestInit { /// @notice Tests that the `gasPayingTokenName` function returns the correct token name. function test_gasPayingTokenName_succeeds() external { - skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); assertEq("Ether", l1Block.gasPayingTokenName()); } /// @notice Tests that the `gasPayingTokenName` function returns the correct token name when custom gas token is /// enabled. function test_gasPayingTokenName_customGasToken_succeeds() external { - skipIfDevFeatureDisabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureDisabled(Features.CUSTOM_GAS_TOKEN); assertEq(liquidityController.gasPayingTokenName(), l1Block.gasPayingTokenName()); } } @@ -90,14 +90,14 @@ contract L1Block_GasPayingTokenName_Test is L1Block_TestInit { contract L1Block_GasPayingTokenSymbol_Test is L1Block_TestInit { /// @notice Tests that the `gasPayingTokenSymbol` function returns the correct token symbol. function test_gasPayingTokenSymbol_succeeds() external { - skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); assertEq("ETH", l1Block.gasPayingTokenSymbol()); } /// @notice Tests that the `gasPayingTokenSymbol` function returns the correct token symbol when custom gas token is /// enabled. function test_gasPayingTokenSymbol_customGasToken_succeeds() external { - skipIfDevFeatureDisabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureDisabled(Features.CUSTOM_GAS_TOKEN); assertEq(liquidityController.gasPayingTokenSymbol(), l1Block.gasPayingTokenSymbol()); } } @@ -108,14 +108,14 @@ contract L1Block_IsCustomGasToken_Test is L1Block_TestInit { /// @notice Tests that the `isCustomGasToken` function returns false when no custom gas token /// is used. function test_isCustomGasToken_succeeds() external { - skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); assertFalse(l1Block.isCustomGasToken()); } /// @notice Tests that the `isCustomGasToken` function returns true when custom gas token /// is used. function test_isCustomGasToken_customGasToken_succeeds() external { - skipIfDevFeatureDisabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureDisabled(Features.CUSTOM_GAS_TOKEN); assertTrue(l1Block.isCustomGasToken()); } } @@ -467,7 +467,7 @@ contract L1Block_SetCustomGasToken_Test is L1Block_TestInit { function setUp() public override { super.setUp(); - skipIfDevFeatureDisabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureDisabled(Features.CUSTOM_GAS_TOKEN); l1BlockCGT = IL1BlockCGT(address(l1Block)); } diff --git a/packages/contracts-bedrock/test/L2/L1Withdrawer.t.sol b/packages/contracts-bedrock/test/L2/L1Withdrawer.t.sol index 277959e30be..865e7177f23 100644 --- a/packages/contracts-bedrock/test/L2/L1Withdrawer.t.sol +++ b/packages/contracts-bedrock/test/L2/L1Withdrawer.t.sol @@ -6,7 +6,6 @@ import { ICrossDomainMessenger } from "interfaces/universal/ICrossDomainMessenge import { Predeploys } from "src/libraries/Predeploys.sol"; import { IL1Withdrawer } from "interfaces/L2/IL1Withdrawer.sol"; import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; -import { DevFeatures } from "src/libraries/DevFeatures.sol"; /// @title L1Withdrawer_TestInit /// @notice Base test contract with initialization for `L1Withdrawer` tests. @@ -26,10 +25,6 @@ contract L1Withdrawer_TestInit is CommonTest { /// @notice Test setup. function setUp() public virtual override { - // Resolve features and skip whole test suite if custom gas token is enabled - resolveFeaturesFromEnv(); - skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); - // Enable revenue sharing before calling parent setUp super.enableRevenueShare(); super.setUp(); diff --git a/packages/contracts-bedrock/test/L2/L2StandardBridge.t.sol b/packages/contracts-bedrock/test/L2/L2StandardBridge.t.sol index 0f25b19caaa..a0f1c0aec8c 100644 --- a/packages/contracts-bedrock/test/L2/L2StandardBridge.t.sol +++ b/packages/contracts-bedrock/test/L2/L2StandardBridge.t.sol @@ -14,12 +14,13 @@ import { OptimismMintableERC20 } from "src/universal/OptimismMintableERC20.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; import { Hashing } from "src/libraries/Hashing.sol"; import { Types } from "src/libraries/Types.sol"; -import { DevFeatures } from "src/libraries/DevFeatures.sol"; +import { Features } from "src/libraries/Features.sol"; // Interfaces import { ICrossDomainMessenger } from "interfaces/universal/ICrossDomainMessenger.sol"; import { IStandardBridge } from "interfaces/universal/IStandardBridge.sol"; import { IL2ToL1MessagePasser } from "interfaces/L2/IL2ToL1MessagePasser.sol"; +import { IL2ToL1MessagePasserCGT } from "interfaces/L2/IL2ToL1MessagePasserCGT.sol"; import { IL2StandardBridge } from "interfaces/L2/IL2StandardBridge.sol"; /// @title L2StandardBridge_TestInit @@ -232,7 +233,7 @@ contract L2StandardBridge_Initialize_Test is L2StandardBridge_TestInit { contract L2StandardBridge_Receive_Test is L2StandardBridge_TestInit { /// @notice Tests that the bridge receives ETH and successfully initiates a withdrawal. function test_receive_succeeds() external { - skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); assertEq(address(l2ToL1MessagePasser).balance, 0); uint256 nonce = l2CrossDomainMessenger.messageNonce(); @@ -299,6 +300,20 @@ contract L2StandardBridge_Receive_Test is L2StandardBridge_TestInit { assertEq(success, true); assertEq(address(l2ToL1MessagePasser).balance, 100); } + + /// @notice Tests that receive reverts when custom gas token is enabled and value is sent. + function testFuzz_receive_withCustomGasToken_reverts(uint256 _value) external { + skipIfSysFeatureDisabled(Features.CUSTOM_GAS_TOKEN); + + _value = bound(_value, 1, type(uint128).max); + vm.deal(alice, _value); + + vm.prank(alice, alice); + vm.expectRevert(IL2ToL1MessagePasserCGT.L2ToL1MessagePasserCGT_NotAllowedOnCGTMode.selector); + + (bool revertsAsExpected,) = address(l2StandardBridge).call{ value: _value }(hex""); + assertTrue(revertsAsExpected, "expectRevert: call did not revert"); + } } /// @title L2StandardBridge_Withdraw_Test @@ -325,7 +340,7 @@ contract L2StandardBridge_Withdraw_Test is L2StandardBridge_TestInit { /// @notice Tests that the legacy `withdraw` interface on the L2StandardBridge sucessfully /// initiates a withdrawal. function test_withdraw_ether_succeeds() external { - skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); assertTrue(alice.balance >= 100); assertEq(Predeploys.L2_TO_L1_MESSAGE_PASSER.balance, 0); @@ -376,6 +391,18 @@ contract L2StandardBridge_Withdraw_Test is L2StandardBridge_TestInit { vm.expectRevert("StandardBridge: function can only be called from an EOA"); l2StandardBridge.withdraw(address(L2Token), 100, 1000, hex""); } + + /// @notice Tests that withdraw reverts when custom gas token is enabled and value is sent. + function testFuzz_withdraw_withCustomGasToken_reverts(uint256 _value, uint32 _minGasLimit) external { + skipIfSysFeatureDisabled(Features.CUSTOM_GAS_TOKEN); + + _value = bound(_value, 1, type(uint128).max); + vm.deal(alice, _value); + + vm.prank(alice, alice); + vm.expectRevert(IL2ToL1MessagePasserCGT.L2ToL1MessagePasserCGT_NotAllowedOnCGTMode.selector); + l2StandardBridge.withdraw{ value: _value }(Predeploys.LEGACY_ERC20_ETH, _value, _minGasLimit, hex""); + } } /// @title L2StandardBridge_WithdrawTo_Test @@ -468,7 +495,7 @@ contract L2StandardBridge_Uncategorized_Test is L2StandardBridge_TestInit { /// @notice Tests that bridging ETH succeeds. function testFuzz_bridgeETH_succeeds(uint256 _value, uint32 _minGasLimit, bytes calldata _extraData) external { - skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); uint256 nonce = l2CrossDomainMessenger.messageNonce(); bytes memory message = abi.encodeCall(IStandardBridge.finalizeBridgeETH, (alice, alice, _value, _extraData)); @@ -502,7 +529,7 @@ contract L2StandardBridge_Uncategorized_Test is L2StandardBridge_TestInit { /// @notice Tests that bridging ETH to a different address succeeds. function testFuzz_bridgeETHTo_succeeds(uint256 _value, uint32 _minGasLimit, bytes calldata _extraData) external { - skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); uint256 nonce = l2CrossDomainMessenger.messageNonce(); vm.expectCall( @@ -595,4 +622,42 @@ contract L2StandardBridge_Uncategorized_Test is L2StandardBridge_TestInit { vm.expectRevert("StandardBridge: wrong remote token for Optimism Mintable ERC20 local token"); l2StandardBridge.finalizeBridgeERC20(localToken, remoteToken, alice, alice, 100, hex""); } + + /// @notice Tests that bridgeETH reverts when custom gas token is enabled and value is sent. + function testFuzz_bridgeETH_withCustomGasToken_reverts( + uint256 _value, + uint32 _minGasLimit, + bytes calldata _extraData + ) + external + { + skipIfSysFeatureDisabled(Features.CUSTOM_GAS_TOKEN); + + _value = bound(_value, 1, type(uint128).max); + vm.deal(alice, _value); + + vm.prank(alice, alice); + vm.expectRevert(IL2ToL1MessagePasserCGT.L2ToL1MessagePasserCGT_NotAllowedOnCGTMode.selector); + l2StandardBridge.bridgeETH{ value: _value }(_minGasLimit, _extraData); + } + + /// @notice Tests that bridgeETHTo reverts when custom gas token is enabled and value is sent. + function testFuzz_bridgeETHTo_withCustomGasToken_reverts( + address _to, + uint256 _value, + uint32 _minGasLimit, + bytes calldata _extraData + ) + external + { + skipIfSysFeatureDisabled(Features.CUSTOM_GAS_TOKEN); + + vm.assume(_to != address(0)); + _value = bound(_value, 1, type(uint128).max); + vm.deal(alice, _value); + + vm.prank(alice, alice); + vm.expectRevert(IL2ToL1MessagePasserCGT.L2ToL1MessagePasserCGT_NotAllowedOnCGTMode.selector); + l2StandardBridge.bridgeETHTo{ value: _value }(_to, _minGasLimit, _extraData); + } } diff --git a/packages/contracts-bedrock/test/L2/L2ToL1MessagePasser.t.sol b/packages/contracts-bedrock/test/L2/L2ToL1MessagePasser.t.sol index c79f3af797e..e3ddd2497db 100644 --- a/packages/contracts-bedrock/test/L2/L2ToL1MessagePasser.t.sol +++ b/packages/contracts-bedrock/test/L2/L2ToL1MessagePasser.t.sol @@ -7,7 +7,7 @@ import { CommonTest } from "test/setup/CommonTest.sol"; // Libraries import { Types } from "src/libraries/Types.sol"; import { Hashing } from "src/libraries/Hashing.sol"; -import { DevFeatures } from "src/libraries/DevFeatures.sol"; +import { Features } from "src/libraries/Features.sol"; import { SemverComp } from "src/libraries/SemverComp.sol"; // Interfaces @@ -27,7 +27,7 @@ contract L2ToL1MessagePasser_Version_Test is CommonTest { contract L2ToL1MessagePasser_Receive_Test is CommonTest { /// @notice Tests that receive() initiates withdrawal with default gas limit. function testFuzz_receive_initiatesWithdrawal_succeeds(uint256 _value) external { - skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); uint256 nonce = l2ToL1MessagePasser.messageNonce(); @@ -59,7 +59,7 @@ contract L2ToL1MessagePasser_Receive_Test is CommonTest { contract L2ToL1MessagePasser_Burn_Test is CommonTest { /// @notice Tests that `burn` succeeds and destroys the ETH held in the contract. function testFuzz_burn_succeeds(uint256 _value, address _target, uint256 _gasLimit, bytes memory _data) external { - skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); vm.deal(address(this), _value); l2ToL1MessagePasser.initiateWithdrawal{ value: _value }({ _target: _target, _gasLimit: _gasLimit, _data: _data }); @@ -89,7 +89,7 @@ contract L2ToL1MessagePasser_InitiateWithdrawal_Test is CommonTest { ) external { - if (isDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN)) { + if (isSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN)) { _value = 0; } uint256 nonce = l2ToL1MessagePasser.messageNonce(); @@ -129,7 +129,7 @@ contract L2ToL1MessagePasser_InitiateWithdrawal_Test is CommonTest { ) external { - skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); bytes32 withdrawalHash = Hashing.hashWithdrawal( Types.WithdrawalTransaction({ nonce: l2ToL1MessagePasser.messageNonce(), @@ -161,7 +161,7 @@ contract L2ToL1MessagePasser_InitiateWithdrawal_Test is CommonTest { ) external { - skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); uint256 nonce = l2ToL1MessagePasser.messageNonce(); // Verify caller is an EOA (alice has no code) @@ -192,7 +192,7 @@ contract L2ToL1MessagePasser_InitiateWithdrawal_Test is CommonTest { ) external { - skipIfDevFeatureDisabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureDisabled(Features.CUSTOM_GAS_TOKEN); // Set initial state _value = bound(_value, 1, type(uint256).max); vm.deal(_randomAddress, _value); diff --git a/packages/contracts-bedrock/test/L2/LiquidityController.t.sol b/packages/contracts-bedrock/test/L2/LiquidityController.t.sol index 45375ce66ca..82208200fcc 100644 --- a/packages/contracts-bedrock/test/L2/LiquidityController.t.sol +++ b/packages/contracts-bedrock/test/L2/LiquidityController.t.sol @@ -6,7 +6,8 @@ import { CommonTest } from "test/setup/CommonTest.sol"; import { stdStorage, StdStorage } from "forge-std/Test.sol"; // Libraries -import { DevFeatures } from "src/libraries/DevFeatures.sol"; +import { Features } from "src/libraries/Features.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; // Contracts import { LiquidityController } from "src/L2/LiquidityController.sol"; @@ -38,7 +39,7 @@ contract LiquidityController_TestInit is CommonTest { /// @notice Test setup. function setUp() public virtual override { super.setUp(); - skipIfDevFeatureDisabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureDisabled(Features.CUSTOM_GAS_TOKEN); } /// @notice Helper function to authorize a minter. @@ -215,6 +216,8 @@ contract LiquidityController_Mint_Test is LiquidityController_TestInit { contract LiquidityController_Burn_Test is LiquidityController_TestInit { /// @notice Tests that the burn function can be called by an authorized minter. function testFuzz_burn_fromAuthorizedMinter_succeeds(uint256 _amount, address _minter) public { + vm.assume(_minter != Predeploys.NATIVE_ASSET_LIQUIDITY); + _authorizeMinter(_minter); _amount = bound(_amount, 0, address(nativeAssetLiquidity).balance); diff --git a/packages/contracts-bedrock/test/L2/NativeAssetLiquidity.t.sol b/packages/contracts-bedrock/test/L2/NativeAssetLiquidity.t.sol index 2b5869b5565..db3a9c6eaca 100644 --- a/packages/contracts-bedrock/test/L2/NativeAssetLiquidity.t.sol +++ b/packages/contracts-bedrock/test/L2/NativeAssetLiquidity.t.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.15; import { CommonTest } from "test/setup/CommonTest.sol"; // Libraries -import { DevFeatures } from "src/libraries/DevFeatures.sol"; +import { Features } from "src/libraries/Features.sol"; import { NativeAssetLiquidity } from "src/L2/NativeAssetLiquidity.sol"; /// @title NativeAssetLiquidity_TestInit @@ -23,7 +23,7 @@ contract NativeAssetLiquidity_TestInit is CommonTest { /// @notice Test setup. function setUp() public virtual override { super.setUp(); - skipIfDevFeatureDisabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureDisabled(Features.CUSTOM_GAS_TOKEN); } } diff --git a/packages/contracts-bedrock/test/L2/RevenueSharingIntegration.t.sol b/packages/contracts-bedrock/test/L2/RevenueSharingIntegration.t.sol index 5484029d752..777b0ffa942 100644 --- a/packages/contracts-bedrock/test/L2/RevenueSharingIntegration.t.sol +++ b/packages/contracts-bedrock/test/L2/RevenueSharingIntegration.t.sol @@ -8,7 +8,6 @@ import { IFeeVault } from "interfaces/L2/IFeeVault.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; import { Types } from "src/libraries/Types.sol"; import { ICrossDomainMessenger } from "interfaces/universal/ICrossDomainMessenger.sol"; -import { DevFeatures } from "src/libraries/DevFeatures.sol"; /// @title RevenueSharingIntegration_Test /// @notice Integration tests for the complete revenue sharing system including @@ -26,10 +25,6 @@ contract RevenueSharingIntegration_Test is CommonTest { event FundsReceived(address indexed sender, uint256 amount, uint256 newBalance); function setUp() public override { - // Resolve features and skip whole test suite if custom gas token is enabled - resolveFeaturesFromEnv(); - skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); - // Enable revenue sharing before calling parent setUp super.enableRevenueShare(); super.setUp(); diff --git a/packages/contracts-bedrock/test/L2/SuperchainRevSharesCalculator.t.sol b/packages/contracts-bedrock/test/L2/SuperchainRevSharesCalculator.t.sol index c801ae37308..9f08bdc33ac 100644 --- a/packages/contracts-bedrock/test/L2/SuperchainRevSharesCalculator.t.sol +++ b/packages/contracts-bedrock/test/L2/SuperchainRevSharesCalculator.t.sol @@ -8,9 +8,6 @@ import { CommonTest } from "test/setup/CommonTest.sol"; import { ISharesCalculator } from "interfaces/L2/ISharesCalculator.sol"; import { ISuperchainRevSharesCalculator } from "interfaces/L2/ISuperchainRevSharesCalculator.sol"; -// Libraries -import { DevFeatures } from "src/libraries/DevFeatures.sol"; - /// @notice Base setup contract for SuperchainRevSharesCalculator tests. contract SuperchainRevSharesCalculator_TestInit is CommonTest { uint256 internal constant BASIS_POINT_SCALE = 10_000; @@ -24,10 +21,6 @@ contract SuperchainRevSharesCalculator_TestInit is CommonTest { event RemainderRecipientUpdated(address indexed oldRemainderRecipient, address indexed newRemainderRecipient); function setUp() public virtual override { - // Resolve features and skip whole test suite if custom gas token is enabled - resolveFeaturesFromEnv(); - skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); - // Enable revenue sharing before calling parent setUp super.enableRevenueShare(); super.setUp(); diff --git a/packages/contracts-bedrock/test/invariants/CustomGasToken.t.sol b/packages/contracts-bedrock/test/invariants/CustomGasToken.t.sol index fe96f009e30..764a3a0236c 100644 --- a/packages/contracts-bedrock/test/invariants/CustomGasToken.t.sol +++ b/packages/contracts-bedrock/test/invariants/CustomGasToken.t.sol @@ -8,7 +8,8 @@ import { CommonTest } from "test/setup/CommonTest.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; -import { DevFeatures } from "src/libraries/DevFeatures.sol"; +import { Features } from "src/libraries/Features.sol"; +import { SafeSend } from "src/universal/SafeSend.sol"; // Contracts import { ILiquidityController } from "interfaces/L2/ILiquidityController.sol"; @@ -103,7 +104,7 @@ contract NativeAssetLiquidity_Fundooor is StdUtils { _amount = bound(_amount, 0, address(this).balance); // action: fund _amount - vm.deal(address(nativeAssetLiquidity), _amount); + new SafeSend{ value: _amount }(payable(address(nativeAssetLiquidity))); // postcondition: nil here (in the invariant tests) // update ghost variables @@ -190,8 +191,8 @@ contract CustomGasToken_Invariants_Test is CommonTest { /// @notice Test setup. function setUp() public override { - skipIfDevFeatureDisabled(DevFeatures.CUSTOM_GAS_TOKEN); super.setUp(); + skipIfSysFeatureDisabled(Features.CUSTOM_GAS_TOKEN); randomActor = new RandomActor(); actor_funder = new NativeAssetLiquidity_Fundooor(vm); diff --git a/packages/contracts-bedrock/test/invariants/FeeSplit.t.sol b/packages/contracts-bedrock/test/invariants/FeeSplit.t.sol index 0ddacddd82b..f8d36eb6b19 100644 --- a/packages/contracts-bedrock/test/invariants/FeeSplit.t.sol +++ b/packages/contracts-bedrock/test/invariants/FeeSplit.t.sol @@ -10,7 +10,6 @@ import { Predeploys } from "src/libraries/Predeploys.sol"; import { IFeeSplitter } from "interfaces/L2/IFeeSplitter.sol"; import { IL1Withdrawer } from "interfaces/L2/IL1Withdrawer.sol"; import { ISuperchainRevSharesCalculator } from "interfaces/L2/ISuperchainRevSharesCalculator.sol"; -import { DevFeatures } from "src/libraries/DevFeatures.sol"; /// @notice A struct to keep track of the state when a disburse call fails struct DisburseFailureState { @@ -215,10 +214,6 @@ contract FeeSplitter_Invariant is CommonTest { /// @notice Setup: enable the revenue share, deploy handlers and target them. function setUp() public override { - // Resolve features and skip whole test suite if custom gas token is enabled - resolveFeaturesFromEnv(); - skipIfDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN); - super.enableRevenueShare(); super.setUp(); diff --git a/packages/contracts-bedrock/test/libraries/Predeploys.t.sol b/packages/contracts-bedrock/test/libraries/Predeploys.t.sol index 590c10395cb..2a025cd5c09 100644 --- a/packages/contracts-bedrock/test/libraries/Predeploys.t.sol +++ b/packages/contracts-bedrock/test/libraries/Predeploys.t.sol @@ -9,7 +9,7 @@ import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; import { ForgeArtifacts } from "scripts/libraries/ForgeArtifacts.sol"; import { Fork } from "scripts/libraries/Config.sol"; -import { DevFeatures } from "src/libraries/DevFeatures.sol"; +import { Features } from "src/libraries/Features.sol"; /// @title Predeploys_TestInit /// @notice Reusable test initialization for `Predeploys` tests. @@ -165,7 +165,7 @@ contract Predeploys_Uncategorized_Test is Predeploys_TestInit { /// @notice Tests that the predeploy addresses are set correctly. They have code /// and the proxied accounts have the correct admin. Using custom gas token. function test_predeploys_customGasToken_succeeds() external { - skipIfDevFeatureDisabled(DevFeatures.CUSTOM_GAS_TOKEN); + skipIfSysFeatureDisabled(Features.CUSTOM_GAS_TOKEN); _test_predeploys(Fork.ISTHMUS, false, true); } } diff --git a/packages/contracts-bedrock/test/setup/CommonTest.sol b/packages/contracts-bedrock/test/setup/CommonTest.sol index 0be3c9cb7ae..529534b7ceb 100644 --- a/packages/contracts-bedrock/test/setup/CommonTest.sol +++ b/packages/contracts-bedrock/test/setup/CommonTest.sol @@ -14,9 +14,9 @@ import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; // Contracts import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import { DevFeatures } from "src/libraries/DevFeatures.sol"; // Libraries +import { Config } from "scripts/libraries/Config.sol"; import { console } from "forge-std/console.sol"; // Interfaces @@ -79,6 +79,12 @@ abstract contract CommonTest is Test, Setup, Events { deploy.cfg().setUseInterop(true); } if (useRevenueShareOverride) { + // Revenue share is not supported when custom gas token is enabled + if (Config.sysFeatureCustomGasToken()) { + vm.skip(true); + } + + console.log("CommonTest: enabling revenue share"); deploy.cfg().setUseRevenueShare(true); deploy.cfg().setChainFeesRecipient(chainFeesRecipient); deploy.cfg().setL1FeesDepositor(l1FeesDepositor); @@ -86,7 +92,7 @@ abstract contract CommonTest is Test, Setup, Events { if (useUpgradedFork) { deploy.cfg().setUseUpgradedFork(true); } - if (isDevFeatureEnabled(DevFeatures.CUSTOM_GAS_TOKEN)) { + if (Config.sysFeatureCustomGasToken()) { console.log("CommonTest: enabling custom gas token"); deploy.cfg().setUseCustomGasToken(true); deploy.cfg().setGasPayingTokenName("Custom Gas Token"); diff --git a/packages/contracts-bedrock/test/setup/FeatureFlags.sol b/packages/contracts-bedrock/test/setup/FeatureFlags.sol index 2f71edac4f7..10f2d20ffa6 100644 --- a/packages/contracts-bedrock/test/setup/FeatureFlags.sol +++ b/packages/contracts-bedrock/test/setup/FeatureFlags.sol @@ -44,10 +44,6 @@ abstract contract FeatureFlags { console.log("Setup: DEV_FEATURE__DEPLOY_V2_DISPUTE_GAMES is enabled"); devFeatureBitmap |= DevFeatures.DEPLOY_V2_DISPUTE_GAMES; } - if (Config.devFeatureCustomGasToken()) { - console.log("Setup: DEV_FEATURE__CUSTOM_GAS_TOKEN is enabled"); - devFeatureBitmap |= DevFeatures.CUSTOM_GAS_TOKEN; - } if (Config.devFeatureOpcmV2()) { // WARNING: OPCMv2 also automatically implies DEPLOY_V2_DISPUTE_GAMES and CANNON_KONA. console.log("Setup: DEV_FEATURE__OPCM_V2 is enabled");