diff --git a/.circleci/config.yml b/.circleci/config.yml index b6554a0ac97..d4a9534994e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1073,6 +1073,7 @@ jobs: default: "go-tests-short-ci" machine: true resource_class: <> + circleci_ip_ranges: true steps: - checkout-from-workspace - run: @@ -1182,37 +1183,41 @@ jobs: description: Timeout for when CircleCI kills the job if there's no output type: string default: 30m + use_circleci_runner: + description: Whether to use CircleCI runners (with Docker) instead of self-hosted runners + type: boolean + default: false machine: - image: ubuntu-2404:current - docker_layer_caching: true # Since we are building docker images for components, we'll cache the layers for faster builds - resource_class: xlarge + image: <<# parameters.use_circleci_runner >>ubuntu-2404:current<><<^ parameters.use_circleci_runner >>true<> + docker_layer_caching: <> + resource_class: <<# parameters.use_circleci_runner >>xlarge<><<^ parameters.use_circleci_runner >>ethereum-optimism/latitude-1<> steps: - checkout-from-workspace - - run: - name: Setup Kurtosis (if needed) - command: | - if [[ "<>" != "" ]]; then - echo "Setting up Kurtosis for external devnet testing..." + - unless: + condition: + equal: ["",<>] + steps: + - run: + name: Setup Kurtosis + command: | + echo "Setting up Kurtosis for external devnet testing..." - # Print Kurtosis version - echo "Using Kurtosis from: $(which kurtosis || echo 'not found')" - kurtosis version + # Print Kurtosis version + echo "Using Kurtosis from: $(which kurtosis || echo 'not found')" + kurtosis version - # Start Kurtosis engine - echo "Starting Kurtosis engine..." - kurtosis engine start || true + # Start Kurtosis engine + echo "Starting Kurtosis engine..." + kurtosis engine start || true - # Clean old instances - echo "Cleaning old instances..." - kurtosis clean -a || true + # Clean old instances + echo "Cleaning old instances..." + kurtosis clean -a || true - # Check engine status - kurtosis engine status || true + # Check engine status + kurtosis engine status || true - echo "Kurtosis setup complete" - else - echo "Using in-process testing (sysgo orchestrator) - no Kurtosis setup needed" - fi + echo "Kurtosis setup complete" # Notify us of a setup failure - when: condition: on_fail @@ -1241,7 +1246,7 @@ jobs: command: go test -v -c -o /dev/null $(go list -f '{{if .TestGoFiles}}{{.ImportPath}}{{end}}' ./tests/...) # Run the acceptance tests (if the devnet is running) - run: - name: Run acceptance tests (gate=<>) + name: Run acceptance tests (devnet=<>, gate=<>) working_directory: op-acceptance-tests no_output_timeout: 1h environment: @@ -1249,8 +1254,12 @@ jobs: GO111MODULE: "on" GOGC: "0" command: | - # Run the tests - LOG_LEVEL=debug just acceptance-test "<>" "<>" + if [[ "<>" == "" ]]; then + echo "Running in gateless mode - auto-discovering all tests in ./op-acceptance-tests/..." + else + echo "Running in gate mode (gate=<>)" + fi + LOG_LEVEL=info just acceptance-test "<>" "<>" - run: name: Print results (summary) working_directory: op-acceptance-tests @@ -1390,8 +1399,9 @@ jobs: - "op-program/bin/meta*" publish-cannon-prestates: - machine: true - resource_class: ethereum-optimism/latitude-1 + resource_class: medium + docker: + - image: <> steps: - utils/checkout-with-mise - attach_workspace: @@ -2474,13 +2484,11 @@ workflows: - circleci-repo-readonly-authenticated-github-token requires: - initialize - # IN-PROCESS (base) + # IN-MEMORY (all) - op-acceptance-tests: - # Acceptance Testing params - name: memory-base - gate: base - # CircleCI params - no_output_timeout: 10m + name: memory-all + gate: "" # Empty gate = gateless mode + no_output_timeout: 90m context: - circleci-repo-readonly-authenticated-github-token - discord @@ -2493,6 +2501,7 @@ workflows: name: kurtosis-simple devnet: simple gate: base + use_circleci_runner: true # CircleCI params no_output_timeout: 30m context: @@ -2506,6 +2515,7 @@ workflows: name: kurtosis-isthmus devnet: isthmus gate: isthmus + use_circleci_runner: true # CircleCI params no_output_timeout: 30m context: @@ -2519,6 +2529,7 @@ workflows: name: kurtosis-interop devnet: interop gate: interop + use_circleci_runner: true # CircleCI params no_output_timeout: 30m context: @@ -2552,13 +2563,11 @@ workflows: - circleci-repo-readonly-authenticated-github-token requires: - initialize - # IN-PROCESS (base) + # IN-MEMORY (all) - op-acceptance-tests: - # Acceptance Testing params - name: memory-base - gate: base - # CircleCI params - no_output_timeout: 10m + name: memory-all + gate: "" # Empty gate = gateless mode + no_output_timeout: 60m context: - circleci-repo-readonly-authenticated-github-token - discord @@ -2571,6 +2580,7 @@ workflows: name: kurtosis-simple devnet: simple gate: base + use_circleci_runner: true # CircleCI params no_output_timeout: 30m context: @@ -2584,6 +2594,7 @@ workflows: name: kurtosis-isthmus devnet: isthmus gate: isthmus + use_circleci_runner: true # CircleCI params no_output_timeout: 30m context: @@ -2597,6 +2608,7 @@ workflows: name: kurtosis-interop devnet: interop gate: interop + use_circleci_runner: true # CircleCI params no_output_timeout: 30m context: diff --git a/Makefile b/Makefile index d228d9f92fe..83f5c178874 100644 --- a/Makefile +++ b/Makefile @@ -206,7 +206,6 @@ TEST_PKGS := \ ./packages/contracts-bedrock/scripts/checks/... \ ./op-dripper/... \ ./devnet-sdk/... \ - ./op-acceptance-tests/... \ ./kurtosis-devnet/... \ ./op-devstack/... \ ./op-deployer/pkg/deployer/artifacts/... \ @@ -265,7 +264,7 @@ go-tests-short: $(TEST_DEPS) ## Runs comprehensive Go tests with -short flag go-tests-short-ci: ## Runs short Go tests with gotestsum for CI (assumes deps built by CI) @echo "Setting up test directories..." mkdir -p ./tmp/test-results ./tmp/testlogs - @echo "Running Go tests with gotestsum..." + @echo 'Running Go tests (short) with gotestsum...' $(DEFAULT_TEST_ENV_VARS) && \ $(CI_ENV_VARS) && \ gotestsum --format=testname \ diff --git a/mise.toml b/mise.toml index 0fcaefa823d..fb0035a9656 100644 --- a/mise.toml +++ b/mise.toml @@ -37,7 +37,7 @@ anvil = "1.1.0" codecov-uploader = "0.8.0" goreleaser-pro = "2.11.2" kurtosis = "1.8.1" -op-acceptor = "op-acceptor/v3.0.0" +op-acceptor = "op-acceptor/v3.1.0" # Fake dependencies # Put things here if you need to track versions of tools or projects that can't diff --git a/op-acceptance-tests/README.md b/op-acceptance-tests/README.md index 183bee525b2..78b077939ee 100644 --- a/op-acceptance-tests/README.md +++ b/op-acceptance-tests/README.md @@ -96,7 +96,8 @@ For rapid test development, use in-process testing: ```bash cd op-acceptance-tests -just acceptance-test "" base # Uses sysgo orchestrator - faster! +# Not providing a network uses the sysgo orchestrator (in-memory network) which is faster and easier to iterate with. +just acceptance-test "" base ``` ### Testing Against External Devnets @@ -163,8 +164,25 @@ To add new acceptance tests: package: github.com/ethereum-optimism/optimism/your/package/path ``` +### Quick Development + +For rapid development and testing: + +```bash +cd op-acceptance-tests + +# Run all tests (sysgo gateless mode) - most comprehensive coverage +just acceptance-test "" "" + +# Run specific gate-based tests (traditional mode) +just acceptance-test "" base # In-process (sysgo) with gate +just acceptance-test simple base # External devnet (sysext) with gate +``` + +Using an empty gate (`""`) triggers gateless mode with the sysgo orchestrator, auto-discovering all tests. + ## Further Information For more details about `op-acceptor` and the acceptance testing process, refer to the main documentation or ask the team for guidance. -The source code for `op-acceptor` is available at [github.com/ethereum-optimism/infra/op-acceptor](https://github.com/ethereum-optimism/infra/tree/main/op-acceptor). If you discover any bugs or have feature requests, please open an issue in that repository. \ No newline at end of file +The source code for `op-acceptor` is available at [github.com/ethereum-optimism/infra/op-acceptor](https://github.com/ethereum-optimism/infra/tree/main/op-acceptor). If you discover any bugs or have feature requests, please open an issue in that repository. diff --git a/op-acceptance-tests/cmd/main.go b/op-acceptance-tests/cmd/main.go index 1b18c7f39cf..f5e737d14de 100644 --- a/op-acceptance-tests/cmd/main.go +++ b/op-acceptance-tests/cmd/main.go @@ -257,6 +257,11 @@ func runOpAcceptor(ctx context.Context, tracer trace.Tracer, orchestrator string args = append(args, "--devnet-env-url", devnetEnvURL) } + // For sysgo, we allow skips + if orchestrator == "sysgo" { + args = append(args, "--allow-skips") + } + acceptorCmd := exec.CommandContext(ctx, acceptor, args...) acceptorCmd.Env = env acceptorCmd.Stdout = os.Stdout diff --git a/op-acceptance-tests/justfile b/op-acceptance-tests/justfile index 4c761439037..24e969741e0 100644 --- a/op-acceptance-tests/justfile +++ b/op-acceptance-tests/justfile @@ -1,47 +1,48 @@ -REPO_ROOT := `realpath ..` +REPO_ROOT := `realpath ..` # path to the root of the optimism monorepo KURTOSIS_DIR := REPO_ROOT + "/kurtosis-devnet" -ACCEPTOR_VERSION := env_var_or_default("ACCEPTOR_VERSION", "v3.0.0") +ACCEPTOR_VERSION := env_var_or_default("ACCEPTOR_VERSION", "v3.1.0") DOCKER_REGISTRY := env_var_or_default("DOCKER_REGISTRY", "us-docker.pkg.dev/oplabs-tools-artifacts/images") ACCEPTOR_IMAGE := env_var_or_default("ACCEPTOR_IMAGE", DOCKER_REGISTRY + "/op-acceptor:" + ACCEPTOR_VERSION) # Default recipe - runs acceptance tests default: - @just acceptance-test simple base + @just acceptance-test "" base holocene: - @just acceptance-test simple holocene + @just acceptance-test "" holocene isthmus: - @just acceptance-test isthmus isthmus + @just acceptance-test "" isthmus interop: - @just acceptance-test interop interop + @just acceptance-test "" interop # Run acceptance tests with mise-managed binary +# Usage: just acceptance-test [devnet] [gate] +# Examples: +# just acceptance-test "" base # In-process (sysgo) with specific gate +# just acceptance-test "" "" # In-process gateless mode (all tests) +# just acceptance-test "simple" base # External devnet with specific gate +# just acceptance-test "simple" "" # External devnet gateless mode (all tests) acceptance-test devnet="" gate="holocene": #!/usr/bin/env bash set -euo pipefail - # Check if mise is installed - if command -v mise >/dev/null; then - echo "mise is installed" - else - echo "Mise not installed, falling back to Docker..." - just acceptance-test-docker {{devnet}} {{gate}} - fi + # Determine mode and orchestrator + GATELESS_MODE=$([[ "{{gate}}" == "" ]] && echo "true" || echo "false") + ORCHESTRATOR=$([[ "{{devnet}}" == "" ]] && echo "sysgo" || echo "sysext") - if [[ "{{devnet}}" == "" ]]; then - echo -e "DEVNET: in-memory, GATE: {{gate}}\n" + # Display mode information + if [[ "$GATELESS_MODE" == "true" ]]; then + echo -e "DEVNET: $([[ "$ORCHESTRATOR" == "sysgo" ]] && echo "in-memory" || echo "{{devnet}}") ($ORCHESTRATOR), MODE: gateless (all tests)\n" else - echo -e "DEVNET: {{devnet}}, GATE: {{gate}}\n" + echo -e "DEVNET: $([[ "$ORCHESTRATOR" == "sysgo" ]] && echo "in-memory" || echo "{{devnet}}") ($ORCHESTRATOR), GATE: {{gate}}\n" fi - # For sysgo orchestrator (in-process testing) ensure: - # - contracts are built - # - cannon dependencies are built - # Note: build contracts only if not in CI (CI jobs already take care of this) - if [[ "{{devnet}}" == "" && -z "${CIRCLECI:-}" ]]; then + # Build dependencies for sysgo (in-process) mode if not in CI + # In CI jobs already take care of this, so we skip it. + if [[ "$ORCHESTRATOR" == "sysgo" && -z "${CIRCLECI:-}" ]]; then echo "Building contracts (local build)..." cd {{REPO_ROOT}} echo " - Updating submodules..." @@ -63,46 +64,68 @@ acceptance-test devnet="" gate="holocene": fi fi - # Try to install op-acceptor using mise + cd {{REPO_ROOT}}/op-acceptance-tests + + # Check mise installation and fallback to Docker if needed + if ! command -v mise >/dev/null; then + echo "Mise not installed, falling back to Docker..." + just acceptance-test-docker {{devnet}} {{gate}} + exit 0 + fi + + # Install op-acceptor using mise if ! mise install op-acceptor; then echo "WARNING: Failed to install op-acceptor with mise, falling back to Docker..." just acceptance-test-docker {{devnet}} {{gate}} exit 0 fi - # Print which binary is being used + # Set binary path and log level BINARY_PATH=$(mise which op-acceptor) echo "Using mise-managed binary: $BINARY_PATH" + LOG_LEVEL="$(echo "${LOG_LEVEL:-info}" | grep -E '^(debug|info|warn|error)$' || echo 'info')" + echo "LOG_LEVEL: $LOG_LEVEL" - # Build the command with conditional parameters - CMD_ARGS=( - "go" "run" "cmd/main.go" - "--gate" "{{gate}}" - "--testdir" "{{REPO_ROOT}}" - "--validators" "./acceptance-tests.yaml" - "--log.level" "${LOG_LEVEL:-info}" - "--acceptor" "$BINARY_PATH" - ) - - # Set orchestrator and devnet based on input - if [[ "{{devnet}}" == "" ]]; then - # In-process testing - CMD_ARGS+=("--orchestrator" "sysgo") + # Deploy devnet for sysext if it's a simple name + if [[ "$ORCHESTRATOR" == "sysext" && ! "{{devnet}}" =~ ^(kt://|ktnative://|/) ]]; then + echo "Deploying devnet {{devnet}}..." + just {{KURTOSIS_DIR}}/{{devnet}}-devnet || true + fi + + # Build command arguments based on mode + if [[ "$GATELESS_MODE" == "true" ]]; then + # Gateless mode + CMD_ARGS=( + "$BINARY_PATH" + "--testdir" "{{REPO_ROOT}}/op-acceptance-tests/..." + "--allow-skips" + "--timeout" "90m" + "--default-timeout" "10m" + ) else - # External devnet testing - CMD_ARGS+=("--orchestrator" "sysext") - CMD_ARGS+=("--devnet" "{{devnet}}") - # Include kurtosis-dir for devnet deployment - CMD_ARGS+=("--kurtosis-dir" "{{KURTOSIS_DIR}}") - # For now, run sysext in serial mode - CMD_ARGS+=("--serial") + # Gate mode + CMD_ARGS=( + "go" "run" "cmd/main.go" + "--gate" "{{gate}}" + "--testdir" "{{REPO_ROOT}}" + "--validators" "./acceptance-tests.yaml" + "--acceptor" "$BINARY_PATH" + ) + fi + + # Add common arguments + CMD_ARGS+=("--log.level" "${LOG_LEVEL}" "--orchestrator" "$ORCHESTRATOR") + + # Add sysext-specific arguments + if [[ "$ORCHESTRATOR" == "sysext" ]]; then + CMD_ARGS+=("--devnet" "{{devnet}}" "--kurtosis-dir" "{{KURTOSIS_DIR}}" "--serial") fi # Execute the command - cd {{REPO_ROOT}}/op-acceptance-tests "${CMD_ARGS[@]}" + # Run acceptance tests against a devnet using Docker (fallback if needed) acceptance-test-docker devnet="simple" gate="holocene": #!/usr/bin/env bash diff --git a/op-acceptance-tests/tests/base/withdrawal/withdrawal_test.go b/op-acceptance-tests/tests/base/withdrawal/withdrawal_test.go index de9125ac670..d2b40beb723 100644 --- a/op-acceptance-tests/tests/base/withdrawal/withdrawal_test.go +++ b/op-acceptance-tests/tests/base/withdrawal/withdrawal_test.go @@ -33,9 +33,13 @@ func TestWithdrawal(gt *testing.T) { expectedL2UserBalance := depositAmount l2User.VerifyBalanceExact(expectedL2UserBalance) - withdrawal := bridge.InitiateWithdrawal(withdrawalAmount, l2User) + // Force a fresh EOA instance to avoid stale nonce state from shared L1/L2 key usage + // This prevents "nonce too low" errors in the retry logic during withdrawal initiation + freshL2User := l1User.Key().User(sys.L2EL) + + withdrawal := bridge.InitiateWithdrawal(withdrawalAmount, freshL2User) expectedL2UserBalance = expectedL2UserBalance.Sub(withdrawalAmount).Sub(withdrawal.InitiateGasCost()) - l2User.VerifyBalanceExact(expectedL2UserBalance) + freshL2User.VerifyBalanceExact(expectedL2UserBalance) withdrawal.Prove(l1User) expectedL1UserBalance = expectedL1UserBalance.Sub(withdrawal.ProveGasCost()) diff --git a/op-acceptance-tests/tests/ecotone/fees_test.go b/op-acceptance-tests/tests/ecotone/fees_test.go index eefd8def79b..10852f673eb 100644 --- a/op-acceptance-tests/tests/ecotone/fees_test.go +++ b/op-acceptance-tests/tests/ecotone/fees_test.go @@ -28,7 +28,7 @@ func TestFees(gt *testing.T) { ecotoneFees.LogResults(result) - t.Log("Comprehensive Ecotone fees test completed successfully:", + t.Log("Ecotone fees test completed successfully", "gasUsed", result.TransactionReceipt.GasUsed, "l1Fee", result.L1Fee.String(), "l2Fee", result.L2Fee.String(), diff --git a/op-acceptance-tests/tests/fjord/check_scripts_test.go b/op-acceptance-tests/tests/fjord/check_scripts_test.go index 772e262ae22..649f4a5e9de 100644 --- a/op-acceptance-tests/tests/fjord/check_scripts_test.go +++ b/op-acceptance-tests/tests/fjord/check_scripts_test.go @@ -165,7 +165,7 @@ func checkFastLZTransactions(t devtest.T, ctx context.Context, sys *presets.Mini fastLzSize := uint64(types.FlzCompressLen(txUnsigned) + 68) gethGPOFee, err := dsl.CalculateFjordL1Cost(ctx, l2Client, types.RollupCostData{FastLzSize: fastLzSize}, receipt.BlockHash) require.NoError(err) - require.Equal(gethGPOFee.Uint64(), gpoFee.Uint64()) + require.Equalf(gethGPOFee.Uint64(), gpoFee.Uint64(), "GPO L1 fee mismatch (expected=%d actual=%d)", gethGPOFee.Uint64(), gpoFee.Uint64()) expectedFee, err := dsl.CalculateFjordL1Cost(ctx, l2Client, signedTx.RollupCostData(), receipt.BlockHash) require.NoError(err) @@ -178,7 +178,7 @@ func checkFastLZTransactions(t devtest.T, ctx context.Context, sys *presets.Mini flzUpperBound := uint64(txLenGPO + txLenGPO/255 + 16) upperBoundCost, err := dsl.CalculateFjordL1Cost(ctx, l2Client, types.RollupCostData{FastLzSize: flzUpperBound}, receipt.BlockHash) require.NoError(err) - require.Equal(upperBoundCost.Uint64(), upperBound.Uint64()) + require.Equalf(upperBoundCost.Uint64(), upperBound.Uint64(), "GPO L1 upper bound mismatch (expected=%d actual=%d)", upperBoundCost.Uint64(), upperBound.Uint64()) _, err = contractio.Read(gasPriceOracle.BaseFeeScalar(), ctx) require.NoError(err) diff --git a/op-acceptance-tests/tests/interop/interop_smoke_test.go b/op-acceptance-tests/tests/interop/interop_smoke_test.go deleted file mode 100644 index 2fa1be41b36..00000000000 --- a/op-acceptance-tests/tests/interop/interop_smoke_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package interop - -import ( - "context" - "math/big" - "testing" - - "github.com/ethereum-optimism/optimism/devnet-sdk/contracts/constants" - "github.com/ethereum-optimism/optimism/devnet-sdk/system" - "github.com/ethereum-optimism/optimism/devnet-sdk/testing/systest" - "github.com/ethereum-optimism/optimism/devnet-sdk/testing/testlib/validators" - sdktypes "github.com/ethereum-optimism/optimism/devnet-sdk/types" - "github.com/ethereum-optimism/optimism/op-service/testlog" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/log" - "github.com/stretchr/testify/require" -) - -func smokeTestScenario(chainIdx uint64, walletGetter validators.WalletGetter) systest.SystemTestFunc { - return func(t systest.T, sys system.System) { - ctx := t.Context() - - logger := testlog.Logger(t, log.LevelInfo) - logger = logger.With("test", "TestMinimal", "devnet", sys.Identifier()) - - chain := sys.L2s()[chainIdx] - logger = logger.With("chain", chain.ID()) - logger.Info("starting test") - - funds := sdktypes.NewBalance(big.NewInt(1 * constants.ETH)) - user := walletGetter(ctx) - - wethAddr := constants.WETH - weth, err := chain.Nodes()[0].ContractsRegistry().WETH(wethAddr) - require.NoError(t, err) - initialBalance, err := weth.BalanceOf(user.Address()).Call(ctx) - require.NoError(t, err) - logger = logger.With("user", user.Address()) - logger.Info("initial balance retrieved", "balance", initialBalance) - - logger.Info("sending ETH to contract", "amount", funds) - require.NoError(t, user.SendETH(wethAddr, funds).Send(ctx).Wait()) - - balance, err := weth.BalanceOf(user.Address()).Call(ctx) - require.NoError(t, err) - logger.Info("final balance retrieved", "balance", balance) - - require.Equal(t, initialBalance.Add(funds), balance) - } -} - -func TestInteropSystemNoop(t *testing.T) { - systest.InteropSystemTest(t, func(t systest.T, sys system.InteropSystem) { - testlog.Logger(t, log.LevelInfo).Info("noop") - }) -} - -func TestSmokeTestFailure(t *testing.T) { - // Create mock failing system - mockAddr := common.HexToAddress("0x1234567890123456789012345678901234567890") - mockWallet := &mockFailingWallet{ - addr: mockAddr, - bal: sdktypes.NewBalance(big.NewInt(0.1 * constants.ETH)), - } - mockL1Chain := newMockFailingL1Chain( - sdktypes.ChainID(big.NewInt(1234)), - system.WalletMap{ - "user1": mockWallet, - }, - []system.Node{&mockFailingNode{ - reg: &mockContractsRegistry{}, - }}, - ) - mockL2Chain := newMockFailingL2Chain( - sdktypes.ChainID(big.NewInt(1234)), - system.WalletMap{"user1": mockWallet}, - []system.Node{&mockFailingNode{ - reg: &mockContractsRegistry{}, - }}, - ) - mockSys := &mockFailingSystem{l1Chain: mockL1Chain, l2Chain: mockL2Chain} - - // Run the smoke test logic and capture failures - getter := func(ctx context.Context) system.Wallet { - return mockWallet - } - rt := NewRecordingT(context.TODO()) - rt.TestScenario( - smokeTestScenario(0, getter), - mockSys, - ) - - // Verify that the test failed due to SendETH error - require.True(t, rt.Failed(), "test should have failed") - require.Contains(t, rt.Logs(), "transaction failure", "unexpected failure message") -} diff --git a/op-acceptance-tests/tests/interop/message/supervisor_smoke_test.go b/op-acceptance-tests/tests/interop/message/supervisor_smoke_test.go index 59cc56d3092..36d9817eacc 100644 --- a/op-acceptance-tests/tests/interop/message/supervisor_smoke_test.go +++ b/op-acceptance-tests/tests/interop/message/supervisor_smoke_test.go @@ -12,13 +12,26 @@ func TestInteropSystemSupervisor(gt *testing.T) { t := devtest.ParallelT(gt) sys := presets.NewSimpleInterop(t) - sys.L1Network.WaitForFinalization() + // First ensure L1 network is online and has blocks + t.Log("Waiting for L1 network to be online...") + sys.L1Network.WaitForOnline() + t.Log("L1 network is online") + + t.Log("Waiting for initial L1 block...") + initialBlock := sys.L1Network.WaitForBlock() + t.Log("Got initial L1 block", "block", initialBlock) + + // Wait for finalization (this may take some time) + t.Log("Waiting for L1 block finalization...") + finalizedBlock := sys.L1Network.WaitForFinalization() + t.Log("L1 block finalized", "block", finalizedBlock) // Get the finalized L1 block from the supervisor + t.Log("Querying supervisor for finalized L1 block...") block, err := sys.Supervisor.Escape().QueryAPI().FinalizedL1(t.Ctx()) - t.Require().NoError(err) + t.Require().NoError(err, "Failed to get finalized block from supervisor") // If we get here, the supervisor has finalized L1 block information - t.Require().NotNil(block) - t.Log("finalized l1 block", "block", block) + t.Require().NotNil(block, "Supervisor returned nil finalized block") + t.Log("Successfully got finalized L1 block from supervisor", "block", block) } diff --git a/op-acceptance-tests/tests/interop/mocks_test.go b/op-acceptance-tests/tests/interop/mocks_test.go deleted file mode 100644 index fe41f0d545a..00000000000 --- a/op-acceptance-tests/tests/interop/mocks_test.go +++ /dev/null @@ -1,396 +0,0 @@ -package interop - -import ( - "bytes" - "context" - "fmt" - "math/big" - "os" - "runtime" - "time" - - "github.com/ethereum-optimism/optimism/devnet-sdk/contracts/bindings" - "github.com/ethereum-optimism/optimism/devnet-sdk/contracts/registry/empty" - "github.com/ethereum-optimism/optimism/devnet-sdk/interfaces" - "github.com/ethereum-optimism/optimism/devnet-sdk/system" - "github.com/ethereum-optimism/optimism/devnet-sdk/testing/systest" - "github.com/ethereum-optimism/optimism/devnet-sdk/types" - "github.com/ethereum-optimism/optimism/op-service/eth" - "github.com/ethereum-optimism/optimism/op-service/sources" - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/ethclient" - "github.com/ethereum/go-ethereum/params" -) - -var ( - // Ensure mockFailingTx implements WriteInvocation - _ types.WriteInvocation[any] = (*mockFailingTx)(nil) - - // Ensure mockFailingTx implements Wallet - _ system.Wallet = (*mockFailingWallet)(nil) - - // Ensure mockFailingChain implements Chain - _ system.Chain = (*mockFailingChain)(nil) - _ system.L2Chain = (*mockFailingL2Chain)(nil) -) - -// mockFailingTx implements types.WriteInvocation[any] that always fails -type mockFailingTx struct{} - -func (m *mockFailingTx) Call(ctx context.Context) (any, error) { - return nil, fmt.Errorf("simulated transaction failure") -} - -func (m *mockFailingTx) Send(ctx context.Context) types.InvocationResult { - return m -} - -func (m *mockFailingTx) Error() error { - return fmt.Errorf("transaction failure") -} - -func (m *mockFailingTx) Wait() error { - return fmt.Errorf("transaction failure") -} - -func (m *mockFailingTx) Info() any { - return nil -} - -// mockFailingWallet implements types.Wallet that fails on SendETH -type mockFailingWallet struct { - addr types.Address - key types.Key - bal types.Balance -} - -func (m *mockFailingWallet) Client() *ethclient.Client { - return nil -} - -func (m *mockFailingWallet) Address() types.Address { - return m.addr -} - -func (m *mockFailingWallet) PrivateKey() types.Key { - return m.key -} - -func (m *mockFailingWallet) Balance() types.Balance { - return m.bal -} - -func (m *mockFailingWallet) SendETH(to types.Address, amount types.Balance) types.WriteInvocation[any] { - return &mockFailingTx{} -} - -func (m *mockFailingWallet) InitiateMessage(chainID types.ChainID, target common.Address, message []byte) types.WriteInvocation[any] { - return &mockFailingTx{} -} - -func (m *mockFailingWallet) ExecuteMessage(identifier bindings.Identifier, sentMessage []byte) types.WriteInvocation[any] { - return &mockFailingTx{} -} - -func (m *mockFailingWallet) Nonce() uint64 { - return 0 -} - -func (m *mockFailingWallet) Sign(tx system.Transaction) (system.Transaction, error) { - return tx, nil -} - -func (m *mockFailingWallet) Send(ctx context.Context, tx system.Transaction) error { - return nil -} - -func (m *mockFailingWallet) Transactor() *bind.TransactOpts { - return nil -} - -// mockContractsRegistry extends empty.EmptyRegistry to provide mock contract instances -type mockContractsRegistry struct { - empty.EmptyRegistry -} - -// mockWETH implements a minimal WETH interface for testing -type mockWETH struct { - addr types.Address -} - -func (m *mockWETH) BalanceOf(account types.Address) types.ReadInvocation[types.Balance] { - return &mockReadInvocation{balance: types.NewBalance(big.NewInt(0))} -} - -// mockReadInvocation implements a read invocation that returns a fixed balance -type mockReadInvocation struct { - balance types.Balance -} - -func (m *mockReadInvocation) Call(ctx context.Context) (types.Balance, error) { - return m.balance, nil -} - -func (r *mockContractsRegistry) WETH(address types.Address) (interfaces.WETH, error) { - return &mockWETH{addr: address}, nil -} - -// mockFailingChain implements system.Chain with a failing SendETH -type mockFailingChain struct { - id types.ChainID - wallets system.WalletMap - nodes []system.Node -} - -var _ system.Chain = (*mockFailingChain)(nil) - -func newMockFailingL1Chain(id types.ChainID, wallets system.WalletMap, nodes []system.Node) *mockFailingChain { - return &mockFailingChain{ - id: id, - wallets: wallets, - nodes: nodes, - } -} - -func (m *mockFailingChain) Nodes() []system.Node { return m.nodes } -func (m *mockFailingChain) ID() types.ChainID { return m.id } -func (m *mockFailingChain) Wallets() system.WalletMap { - return m.wallets -} -func (m *mockFailingChain) Config() (*params.ChainConfig, error) { - return nil, fmt.Errorf("not implemented") -} -func (m *mockFailingChain) Addresses() system.AddressMap { - return map[string]common.Address{} -} - -var _ system.Node = (*mockFailingNode)(nil) - -type mockFailingNode struct { - reg interfaces.ContractsRegistry -} - -func (m *mockFailingNode) Client() (*sources.EthClient, error) { - return nil, fmt.Errorf("not implemented") -} -func (m *mockFailingNode) GasPrice(ctx context.Context) (*big.Int, error) { - return big.NewInt(1), nil -} -func (m *mockFailingNode) GasLimit(ctx context.Context, tx system.TransactionData) (uint64, error) { - return 1000000, nil -} -func (m *mockFailingNode) PendingNonceAt(ctx context.Context, address common.Address) (uint64, error) { - return 0, nil -} -func (m *mockFailingNode) SupportsEIP(ctx context.Context, eip uint64) bool { - return true -} -func (m *mockFailingNode) RPCURL() string { return "mock://failing" } -func (m *mockFailingNode) ContractsRegistry() interfaces.ContractsRegistry { return m.reg } -func (m *mockFailingNode) GethClient() (*ethclient.Client, error) { - return nil, fmt.Errorf("not implemented") -} -func (m *mockFailingNode) BlockByNumber(ctx context.Context, number *big.Int) (eth.BlockInfo, error) { - return nil, fmt.Errorf("not implemented") -} -func (m *mockFailingNode) Name() string { - return "mock" -} - -// mockFailingChain implements system.Chain with a failing SendETH -type mockFailingL2Chain struct { - mockFailingChain -} - -func newMockFailingL2Chain(id types.ChainID, wallets system.WalletMap, nodes []system.Node) *mockFailingL2Chain { - return &mockFailingL2Chain{ - mockFailingChain: mockFailingChain{ - id: id, - wallets: wallets, - nodes: nodes, - }, - } -} - -func (m *mockFailingL2Chain) L1Addresses() system.AddressMap { - return map[string]common.Address{} -} -func (m *mockFailingL2Chain) L1Wallets() system.WalletMap { - return map[string]system.Wallet{} -} - -// mockFailingSystem implements system.System -type mockFailingSystem struct { - l1Chain system.Chain - l2Chain system.L2Chain -} - -func (m *mockFailingSystem) Identifier() string { - return "mock-failing-system" -} - -func (m *mockFailingSystem) L1() system.Chain { - return m.l1Chain -} - -func (m *mockFailingSystem) L2s() []system.L2Chain { - return []system.L2Chain{m.l2Chain} -} - -func (m *mockFailingSystem) Close() error { - return nil -} - -// recordingT implements systest.T and records failures -type RecordingT struct { - failed bool - skipped bool - logs *bytes.Buffer - cleanup []func() - ctx context.Context -} - -func NewRecordingT(ctx context.Context) *RecordingT { - return &RecordingT{ - logs: bytes.NewBuffer(nil), - ctx: ctx, - } -} - -var _ systest.T = (*RecordingT)(nil) - -func (r *RecordingT) Context() context.Context { - return r.ctx -} - -func (r *RecordingT) WithContext(ctx context.Context) systest.T { - return &RecordingT{ - failed: r.failed, - skipped: r.skipped, - logs: r.logs, - cleanup: r.cleanup, - ctx: ctx, - } -} - -func (r *RecordingT) Deadline() (deadline time.Time, ok bool) { - // TODO - return time.Time{}, false -} - -func (r *RecordingT) Parallel() { - // TODO -} - -func (r *RecordingT) Run(name string, f func(systest.T)) { - // TODO -} - -func (r *RecordingT) Cleanup(f func()) { - r.cleanup = append(r.cleanup, f) -} - -func (r *RecordingT) Error(args ...interface{}) { - r.Log(args...) - r.Fail() -} - -func (r *RecordingT) Errorf(format string, args ...interface{}) { - r.Logf(format, args...) - r.Fail() -} - -func (r *RecordingT) Fatal(args ...interface{}) { - r.Log(args...) - r.FailNow() -} - -func (r *RecordingT) Fatalf(format string, args ...interface{}) { - r.Logf(format, args...) - r.FailNow() -} - -func (r *RecordingT) FailNow() { - r.Fail() - runtime.Goexit() -} - -func (r *RecordingT) Fail() { - r.failed = true -} - -func (r *RecordingT) Failed() bool { - return r.failed -} - -func (r *RecordingT) Helper() { - // TODO -} - -func (r *RecordingT) Log(args ...interface{}) { - fmt.Fprintln(r.logs, args...) -} - -func (r *RecordingT) Logf(format string, args ...interface{}) { - fmt.Fprintf(r.logs, format, args...) - fmt.Fprintln(r.logs) -} - -func (r *RecordingT) Name() string { - return "RecordingT" // TODO -} - -func (r *RecordingT) Setenv(key, value string) { - // Store original value - origValue, exists := os.LookupEnv(key) - - // Set new value - os.Setenv(key, value) - - // Register cleanup to restore original value - r.Cleanup(func() { - if exists { - os.Setenv(key, origValue) - } else { - os.Unsetenv(key) - } - }) - -} - -func (r *RecordingT) Skip(args ...interface{}) { - r.Log(args...) - r.SkipNow() -} - -func (r *RecordingT) SkipNow() { - r.skipped = true -} - -func (r *RecordingT) Skipf(format string, args ...interface{}) { - r.Logf(format, args...) - r.skipped = true -} - -func (r *RecordingT) Skipped() bool { - return r.skipped -} - -func (r *RecordingT) TempDir() string { - return "" // TODO -} - -func (r *RecordingT) Logs() string { - return r.logs.String() -} - -func (r *RecordingT) TestScenario(scenario systest.SystemTestFunc, sys system.System, values ...interface{}) { - // run in a separate goroutine so we can handle runtime.Goexit() - done := make(chan struct{}) - go func() { - defer close(done) - scenario(r, sys) - }() - <-done -} diff --git a/op-acceptance-tests/tests/interop/proofs/withdrawal/withdrawal_test.go b/op-acceptance-tests/tests/interop/proofs/withdrawal/withdrawal_test.go index c0acd53d599..eff0185c165 100644 --- a/op-acceptance-tests/tests/interop/proofs/withdrawal/withdrawal_test.go +++ b/op-acceptance-tests/tests/interop/proofs/withdrawal/withdrawal_test.go @@ -29,6 +29,9 @@ func TestSuperRootWithdrawal(gt *testing.T) { l1User.VerifyBalanceExact(initialL1Balance.Sub(depositAmount).Sub(deposit.GasCost())) l2User.VerifyBalanceExact(initialL2Balance.Add(depositAmount)) + // Wait for a block to ensure nonce synchronization between L1 and L2 EOA instances + sys.L2ChainA.WaitForBlock() + withdrawal := bridge.InitiateWithdrawal(withdrawalAmount, l2User) withdrawal.Prove(l1User) l2User.VerifyBalanceExact(initialL2Balance.Add(depositAmount).Sub(withdrawalAmount).Sub(withdrawal.InitiateGasCost())) diff --git a/op-acceptance-tests/tests/interop/smoke/interop_smoke_test.go b/op-acceptance-tests/tests/interop/smoke/interop_smoke_test.go new file mode 100644 index 00000000000..c056b52b780 --- /dev/null +++ b/op-acceptance-tests/tests/interop/smoke/interop_smoke_test.go @@ -0,0 +1,89 @@ +package smoke + +import ( + "testing" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/predeploys" + txib "github.com/ethereum-optimism/optimism/op-service/txintent/bindings" + "github.com/ethereum-optimism/optimism/op-service/txintent/contractio" + "github.com/ethereum/go-ethereum/core/types" +) + +func TestInteropSystemNoop(gt *testing.T) { + t := devtest.SerialT(gt) + _ = presets.NewMinimal(t) + t.Log("noop") +} + +func TestSmokeTest(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewMinimal(t) + require := t.Require() + ctx := t.Ctx() + + user := sys.FunderL2.NewFundedEOA(eth.OneTenthEther) + + l2Client := sys.L2EL.Escape().EthClient() + weth := txib.NewBindings[txib.WETH]( + txib.WithClient(l2Client), + txib.WithTo(predeploys.WETHAddr), + txib.WithTest(t), + ) + + initialBalance, err := contractio.Read(weth.BalanceOf(user.Address()), ctx) + require.NoError(err) + t.Logf("Initial WETH balance: %s", initialBalance) + + depositAmount := eth.OneHundredthEther + + tx := user.Transfer(predeploys.WETHAddr, depositAmount) + receipt, err := tx.Included.Eval(ctx) + require.NoError(err) + require.Equal(types.ReceiptStatusSuccessful, receipt.Status) + t.Logf("Deposited %s ETH to WETH contract", depositAmount) + + finalBalance, err := contractio.Read(weth.BalanceOf(user.Address()), ctx) + require.NoError(err) + t.Logf("Final WETH balance: %s", finalBalance) + + expectedBalance := initialBalance.Add(depositAmount) + require.Equal(expectedBalance, finalBalance, "WETH balance should have increased by deposited amount") +} + +func TestSmokeTestFailure(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewMinimal(t) + require := t.Require() + ctx := t.Ctx() + + user := sys.FunderL2.NewFundedEOA(eth.OneTenthEther) + + l2Client := sys.L2EL.Escape().EthClient() + weth := txib.NewBindings[txib.WETH]( + txib.WithClient(l2Client), + txib.WithTo(predeploys.WETHAddr), + txib.WithTest(t), + ) + + initialBalance, err := contractio.Read(weth.BalanceOf(user.Address()), ctx) + require.NoError(err) + t.Logf("Initial WETH balance: %s", initialBalance) + + depositAmount := eth.OneEther + + userBalance := user.GetBalance() + t.Logf("User balance: %s", userBalance) + + require.True(userBalance.Lt(depositAmount), "user should have insufficient funds for this transaction") + + t.Logf("user has insufficient funds: balance=%s, required=%s", userBalance, depositAmount) + + finalBalance, err := contractio.Read(weth.BalanceOf(user.Address()), ctx) + require.NoError(err) + t.Logf("Final WETH balance: %s", finalBalance) + + require.Equal(initialBalance, finalBalance, "WETH balance should not have changed") +} diff --git a/op-acceptance-tests/tests/interop/sync/multisupervisor_interop/interop_sync_test.go b/op-acceptance-tests/tests/interop/sync/multisupervisor_interop/interop_sync_test.go index 1190322cc1e..e7dc6dbadee 100644 --- a/op-acceptance-tests/tests/interop/sync/multisupervisor_interop/interop_sync_test.go +++ b/op-acceptance-tests/tests/interop/sync/multisupervisor_interop/interop_sync_test.go @@ -1,3 +1,5 @@ +//go:build !ci + package sync import ( @@ -202,6 +204,8 @@ func TestUnsafeChainKnownToL2CL(gt *testing.T) { // TestUnsafeChainUnknownToL2CL tests the below scenario: // supervisor unsafe ahead of L2CL unsafe, aka L2CL processes new blocks first. func TestUnsafeChainUnknownToL2CL(gt *testing.T) { + gt.Skip("TODO(#16972): skipping due to flakiness and impending op-node/supervisor refactor") + t := devtest.SerialT(gt) sys := presets.NewMultiSupervisorInterop(t) diff --git a/op-acceptance-tests/tests/interop/sync/simple_interop/interop_sync_test.go b/op-acceptance-tests/tests/interop/sync/simple_interop/interop_sync_test.go index 95fe56b4dc6..f7dcd44e6a0 100644 --- a/op-acceptance-tests/tests/interop/sync/simple_interop/interop_sync_test.go +++ b/op-acceptance-tests/tests/interop/sync/simple_interop/interop_sync_test.go @@ -1,3 +1,5 @@ +//go:build !ci + package sync import ( diff --git a/op-acceptance-tests/tests/interop/upgrade/post_test.go b/op-acceptance-tests/tests/interop/upgrade/post_test.go index 6ec1266d1fe..4531c9a2334 100644 --- a/op-acceptance-tests/tests/interop/upgrade/post_test.go +++ b/op-acceptance-tests/tests/interop/upgrade/post_test.go @@ -1,3 +1,5 @@ +//go:build !ci + package upgrade import ( @@ -40,7 +42,7 @@ func TestPostInbox(gt *testing.T) { } func TestPostInteropUpgradeComprehensive(gt *testing.T) { - t := devtest.ParallelT(gt) + t := devtest.SerialT(gt) sys := presets.NewSimpleInterop(t) require := t.Require() logger := t.Logger() diff --git a/op-acceptance-tests/tests/interop/upgrade/pre_test.go b/op-acceptance-tests/tests/interop/upgrade/pre_test.go index 4eea3a7d813..8f5f23567fa 100644 --- a/op-acceptance-tests/tests/interop/upgrade/pre_test.go +++ b/op-acceptance-tests/tests/interop/upgrade/pre_test.go @@ -1,3 +1,5 @@ +//go:build !ci + package upgrade import ( @@ -21,6 +23,8 @@ import ( stypes "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" ) +// This test is known to be flaky +// See: https://github.com/ethereum-optimism/optimism/issues/17298 func TestPreNoInbox(gt *testing.T) { t := devtest.ParallelT(gt) sys := presets.NewSimpleInterop(t) diff --git a/op-acceptance-tests/tests/sync_tester_ext_el/init_test.go b/op-acceptance-tests/tests/sync_tester_ext_el/init_test.go new file mode 100644 index 00000000000..c28d966bab8 --- /dev/null +++ b/op-acceptance-tests/tests/sync_tester_ext_el/init_test.go @@ -0,0 +1,39 @@ +package sync_tester_ext_el + +import ( + "os" + "testing" + + "github.com/ethereum-optimism/optimism/op-devstack/compat" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-service/eth" +) + +var ( + InitialL2Block = uint64(32012748) +) + +func TestMain(m *testing.M) { + L2NetworkName := "op-sepolia" + L1ChainID := eth.ChainIDFromUInt64(11155111) + + L2ELEndpoint := "https://ci-sepolia-l2.optimism.io" + L1CLBeaconEndpoint := "https://ci-sepolia-beacon.optimism.io" + L1ELEndpoint := "https://ci-sepolia-l1.optimism.io" + + // Endpoints when running with Tailscale networking + if os.Getenv("TAILSCALE_NETWORKING") == "true" { + L2ELEndpoint = "https://proxyd-l2-sepolia.primary.client.dev.oplabs.cloud" + L1CLBeaconEndpoint = "https://beacon-api-proxy-sepolia.primary.client.dev.oplabs.cloud" + L1ELEndpoint = "https://proxyd-l1-sepolia.primary.client.dev.oplabs.cloud" + } + + presets.DoMain(m, presets.WithMinimalExternalELWithSuperchainRegistry(L1CLBeaconEndpoint, L1ELEndpoint, L2ELEndpoint, L1ChainID, L2NetworkName, eth.FCUState{ + Latest: InitialL2Block, + Safe: InitialL2Block, + Finalized: InitialL2Block, + }), + presets.WithCompatibleTypes(compat.SysGo), + ) + +} diff --git a/op-acceptance-tests/tests/sync_tester_ext_el/sync_tester_ext_el_test.go b/op-acceptance-tests/tests/sync_tester_ext_el/sync_tester_ext_el_test.go new file mode 100644 index 00000000000..0d797e529bb --- /dev/null +++ b/op-acceptance-tests/tests/sync_tester_ext_el/sync_tester_ext_el_test.go @@ -0,0 +1,52 @@ +package sync_tester_ext_el + +import ( + "testing" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" +) + +func TestSyncTesterExtEL(gt *testing.T) { + t := devtest.SerialT(gt) + + sys := presets.NewMinimalExternalELWithExternalL1(t) + require := t.Require() + + // Test that we can get chain IDs from L2CL node + l2CLChainID := sys.L2CL.ID().ChainID() + require.Equal(eth.ChainIDFromUInt64(11155420), l2CLChainID, "L2CL should be on chain 11155420") + + // Test that the network started successfully + require.NotNil(sys.L1EL, "L1 EL node should be available") + require.NotNil(sys.L2CL, "L2 CL node should be available") + require.NotNil(sys.SyncTester, "SyncTester should be available") + + // Test that we can get sync status from L2CL node + l2CLSyncStatus := sys.L2CL.SyncStatus() + require.NotNil(l2CLSyncStatus, "L2CL should have sync status") + + blocksToSync := uint64(20) + targetBlock := InitialL2Block + blocksToSync + sys.L2CL.Reached(types.LocalUnsafe, targetBlock, 500) + + l2CLSyncStatus = sys.L2CL.SyncStatus() + require.NotNil(l2CLSyncStatus, "L2CL should have sync status") + + unsafeL2Ref := l2CLSyncStatus.UnsafeL2 + blk := sys.L2EL.BlockRefByNumber(unsafeL2Ref.Number) + require.Equal(unsafeL2Ref.Hash, blk.Hash, "L2EL should be on the same block as L2CL") + + stSessions := sys.SyncTester.ListSessions() + require.Equal(len(stSessions), 1, "expect exactly one session") + + stSession := sys.SyncTester.GetSession(stSessions[0]) + require.GreaterOrEqual(stSession.CurrentState.Latest, stSession.InitialState.Latest+blocksToSync, "SyncTester session Latest should be on the same block as L2CL") + require.GreaterOrEqual(stSession.CurrentState.Safe, stSession.InitialState.Safe+blocksToSync, "SyncTester session Safe should be on the same block as L2CL") + + t.Logger().Info("SyncTester ExtEL test completed successfully", + "l2cl_chain_id", l2CLChainID, + "l2cl_sync_status", l2CLSyncStatus) +} diff --git a/op-deployer/pkg/deployer/integration_test/apply_test.go b/op-deployer/pkg/deployer/integration_test/apply_test.go index aa62be2fd45..96e70fc3313 100644 --- a/op-deployer/pkg/deployer/integration_test/apply_test.go +++ b/op-deployer/pkg/deployer/integration_test/apply_test.go @@ -12,6 +12,7 @@ import ( "time" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/bootstrap" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/inspect" "github.com/ethereum/go-ethereum/params" "github.com/ethereum-optimism/optimism/op-service/testutils" @@ -45,6 +46,8 @@ import ( "github.com/stretchr/testify/require" ) +const testCustomGasLimit = uint64(90_123_456) + type deployerKey struct{} func (d *deployerKey) HDPath() string { @@ -243,7 +246,6 @@ func TestGlobalOverrides(t *testing.T) { defer cancel() opts, intent, st := setupGenesisChain(t, devnet.DefaultChainID) - expectedGasLimit := strings.ToLower("0x1C9C380") expectedBaseFeeVaultRecipient := common.HexToAddress("0x0000000000000000000000000000000000000001") expectedL1FeeVaultRecipient := common.HexToAddress("0x0000000000000000000000000000000000000002") expectedSequencerFeeVaultRecipient := common.HexToAddress("0x0000000000000000000000000000000000000003") @@ -255,7 +257,6 @@ func TestGlobalOverrides(t *testing.T) { expectedUseFaultProofs := false intent.GlobalDeployOverrides = map[string]interface{}{ "l2BlockTime": float64(3), - "l2GenesisBlockGasLimit": expectedGasLimit, "baseFeeVaultRecipient": expectedBaseFeeVaultRecipient, "l1FeeVaultRecipient": expectedL1FeeVaultRecipient, "sequencerFeeVaultRecipient": expectedSequencerFeeVaultRecipient, @@ -272,7 +273,6 @@ func TestGlobalOverrides(t *testing.T) { cfg, err := state.CombineDeployConfig(intent, intent.Chains[0], st, st.Chains[0]) require.NoError(t, err) require.Equal(t, uint64(3), cfg.L2InitializationConfig.L2CoreDeployConfig.L2BlockTime, "L2 block time should be 3 seconds") - require.Equal(t, expectedGasLimit, strings.ToLower(cfg.L2InitializationConfig.L2GenesisBlockDeployConfig.L2GenesisBlockGasLimit.String()), "L2 Genesis Block Gas Limit should be 30_000_000") require.Equal(t, expectedBaseFeeVaultRecipient, cfg.L2InitializationConfig.L2VaultsDeployConfig.BaseFeeVaultRecipient, "Base Fee Vault Recipient should be the expected address") require.Equal(t, expectedL1FeeVaultRecipient, cfg.L2InitializationConfig.L2VaultsDeployConfig.L1FeeVaultRecipient, "L1 Fee Vault Recipient should be the expected address") require.Equal(t, expectedSequencerFeeVaultRecipient, cfg.L2InitializationConfig.L2VaultsDeployConfig.SequencerFeeVaultRecipient, "Sequencer Fee Vault Recipient should be the expected address") @@ -700,6 +700,7 @@ func newChainIntent(t *testing.T, dk *devkeys.MnemonicDevKeys, l1ChainID *big.In Eip1559DenominatorCanyon: standard.Eip1559DenominatorCanyon, Eip1559Denominator: standard.Eip1559Denominator, Eip1559Elasticity: standard.Eip1559Elasticity, + GasLimit: testCustomGasLimit, Roles: state.ChainRoles{ L1ProxyAdminOwner: addrFor(t, dk, devkeys.L2ProxyAdminOwnerRole.Key(l1ChainID)), L2ProxyAdminOwner: addrFor(t, dk, devkeys.L2ProxyAdminOwnerRole.Key(l1ChainID)), @@ -841,6 +842,12 @@ func validateOPChainDeployment(t *testing.T, cg codeGetter, st *state.State, int require.False(t, ok, "governance token should not be deployed by default") } + genesis, rollup, err := inspect.GenesisAndRollup(st, chainState.ID) + require.NoError(t, err) + require.Equal(t, rollup.Genesis.SystemConfig.GasLimit, testCustomGasLimit, "rollup gasLimit") + require.Equal(t, genesis.GasLimit, testCustomGasLimit, "genesis gasLimit") + + require.Equal(t, chainIntent.GasLimit, testCustomGasLimit, "chainIntent gasLimit") require.Equal(t, int(chainIntent.Eip1559Denominator), 50, "EIP1559Denominator should be set") require.Equal(t, int(chainIntent.Eip1559Elasticity), 6, "EIP1559Elasticity should be set") } diff --git a/op-deployer/pkg/deployer/pipeline/opchain.go b/op-deployer/pkg/deployer/pipeline/opchain.go index 4b58057d896..b7d63d7b033 100644 --- a/op-deployer/pkg/deployer/pipeline/opchain.go +++ b/op-deployer/pkg/deployer/pipeline/opchain.go @@ -101,7 +101,7 @@ func makeDCI(intent *state.Intent, thisIntent *state.ChainIntent, chainID common L2ChainId: chainID.Big(), Opcm: st.ImplementationsDeployment.OpcmImpl, SaltMixer: st.Create2Salt.String(), // passing through salt generated at state initialization - GasLimit: standard.GasLimit, + GasLimit: thisIntent.GasLimit, DisputeGameType: proofParams.DisputeGameType, DisputeAbsolutePrestate: proofParams.DisputeAbsolutePrestate, DisputeMaxGameDepth: proofParams.DisputeMaxGameDepth, diff --git a/op-deployer/pkg/deployer/state/chain_intent.go b/op-deployer/pkg/deployer/state/chain_intent.go index c69a11a5327..8127367f337 100644 --- a/op-deployer/pkg/deployer/state/chain_intent.go +++ b/op-deployer/pkg/deployer/state/chain_intent.go @@ -70,6 +70,7 @@ type ChainIntent struct { Eip1559DenominatorCanyon uint64 `json:"eip1559DenominatorCanyon" toml:"eip1559DenominatorCanyon"` Eip1559Denominator uint64 `json:"eip1559Denominator" toml:"eip1559Denominator"` Eip1559Elasticity uint64 `json:"eip1559Elasticity" toml:"eip1559Elasticity"` + GasLimit uint64 `json:"gasLimit" toml:"gasLimit"` Roles ChainRoles `json:"roles" toml:"roles"` DeployOverrides map[string]any `json:"deployOverrides" toml:"deployOverrides"` DangerousAltDAConfig genesis.AltDADeployConfig `json:"dangerousAltDAConfig,omitempty" toml:"dangerousAltDAConfig,omitempty"` @@ -93,6 +94,7 @@ type ChainRoles struct { } var ErrFeeVaultZeroAddress = fmt.Errorf("chain has a fee vault set to zero address") +var ErrGasLimitZeroValue = fmt.Errorf("chain has a gas limit set to zero value") var ErrNonStandardValue = fmt.Errorf("chain contains non-standard config value") var ErrEip1559ZeroValue = fmt.Errorf("eip1559 param is set to zero value") var ErrIncompatibleValue = fmt.Errorf("chain contains incompatible config value") @@ -111,6 +113,11 @@ func (c *ChainIntent) Check() error { c.Eip1559Elasticity == 0 { return fmt.Errorf("%w: chainId=%s", ErrEip1559ZeroValue, c.ID) } + + if c.GasLimit == 0 { + return fmt.Errorf("%w: chainId=%s", ErrGasLimitZeroValue, c.ID) + } + if c.BaseFeeVaultRecipient == emptyAddress || c.L1FeeVaultRecipient == emptyAddress || c.SequencerFeeVaultRecipient == emptyAddress { diff --git a/op-deployer/pkg/deployer/state/deploy_config.go b/op-deployer/pkg/deployer/state/deploy_config.go index 6dec2071486..e5c46e4c2f7 100644 --- a/op-deployer/pkg/deployer/state/deploy_config.go +++ b/op-deployer/pkg/deployer/state/deploy_config.go @@ -39,7 +39,7 @@ func CombineDeployConfig(intent *Intent, chainIntent *ChainIntent, state *State, FundDevAccounts: intent.FundDevAccounts, }, L2GenesisBlockDeployConfig: genesis.L2GenesisBlockDeployConfig{ - L2GenesisBlockGasLimit: 60_000_000, + L2GenesisBlockGasLimit: hexutil.Uint64(chainIntent.GasLimit), L2GenesisBlockBaseFeePerGas: &l2GenesisBlockBaseFeePerGas, }, L2VaultsDeployConfig: genesis.L2VaultsDeployConfig{ diff --git a/op-deployer/pkg/deployer/state/deploy_config_test.go b/op-deployer/pkg/deployer/state/deploy_config_test.go index d8b7247df25..6e9bfd0d06d 100644 --- a/op-deployer/pkg/deployer/state/deploy_config_test.go +++ b/op-deployer/pkg/deployer/state/deploy_config_test.go @@ -5,6 +5,7 @@ import ( "github.com/ethereum-optimism/optimism/op-chain-ops/addresses" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/artifacts" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/stretchr/testify/require" @@ -21,6 +22,7 @@ func TestCombineDeployConfig(t *testing.T) { chainIntent := ChainIntent{ Eip1559Denominator: 1, Eip1559Elasticity: 2, + GasLimit: standard.GasLimit, BaseFeeVaultRecipient: common.HexToAddress("0x123"), L1FeeVaultRecipient: common.HexToAddress("0x456"), SequencerFeeVaultRecipient: common.HexToAddress("0x789"), diff --git a/op-deployer/pkg/deployer/state/intent.go b/op-deployer/pkg/deployer/state/intent.go index 70fe84abd78..e365c28118c 100644 --- a/op-deployer/pkg/deployer/state/intent.go +++ b/op-deployer/pkg/deployer/state/intent.go @@ -151,6 +151,9 @@ func (c *Intent) validateStandardValues() error { chain.Eip1559Elasticity != standard.Eip1559Elasticity { return fmt.Errorf("%w: chainId=%s", ErrNonStandardValue, chain.ID) } + if chain.GasLimit != standard.GasLimit { + return fmt.Errorf("%w: chainId=%s", ErrNonStandardValue, chain.ID) + } if len(chain.AdditionalDisputeGames) > 0 { return fmt.Errorf("%w: chainId=%s additionalDisputeGames must be nil", ErrNonStandardValue, chain.ID) } @@ -296,7 +299,8 @@ func NewIntentCustom(l1ChainId uint64, l2ChainIds []common.Hash) (Intent, error) for _, l2ChainID := range l2ChainIds { intent.Chains = append(intent.Chains, &ChainIntent{ - ID: l2ChainID, + ID: l2ChainID, + GasLimit: standard.GasLimit, CustomGasToken: &CustomGasToken{ Enabled: standard.CustomGasTokenEnabled, Name: standard.CustomGasTokenName, @@ -340,6 +344,7 @@ func NewIntentStandard(l1ChainId uint64, l2ChainIds []common.Hash) (Intent, erro Eip1559DenominatorCanyon: standard.Eip1559DenominatorCanyon, Eip1559Denominator: standard.Eip1559Denominator, Eip1559Elasticity: standard.Eip1559Elasticity, + GasLimit: standard.GasLimit, Roles: ChainRoles{ Challenger: challenger, L1ProxyAdminOwner: l1ProxyAdminOwner, diff --git a/op-devstack/dsl/ecotone_fees.go b/op-devstack/dsl/ecotone_fees.go index 3412f893c49..d1970a94b92 100644 --- a/op-devstack/dsl/ecotone_fees.go +++ b/op-devstack/dsl/ecotone_fees.go @@ -54,29 +54,40 @@ func (ef *EcotoneFees) ValidateTransaction(from *EOA, to *EOA, amount *big.Int) ef.require.NoError(err) ef.require.Equal(types.ReceiptStatusSuccessful, receipt.Status) + // Get block info for base fee information + blockInfo, err := client.InfoByHash(ef.ctx, receipt.BlockHash) + ef.require.NoError(err) + endBalance := from.GetBalance() vaultsAfter := ef.getVaultBalances(client) vaultIncreases := ef.calculateVaultIncreases(vaultsBefore, vaultsAfter) - l1Fee := big.NewInt(0) - if receipt.L1Fee != nil { - l1Fee = receipt.L1Fee - } + // In Ecotone, L1 fee includes both base fee and blob base fee components + l1Fee := vaultIncreases.L1FeeVault // Use actual vault increase as the source of truth - block, err := client.InfoByHash(ef.ctx, receipt.BlockHash) - ef.require.NoError(err) + // Calculate receipt-based fees for validation + receiptBaseFee := new(big.Int).Mul(blockInfo.BaseFee(), big.NewInt(int64(receipt.GasUsed))) + receiptL2Fee := new(big.Int).Mul(receipt.EffectiveGasPrice, big.NewInt(int64(receipt.GasUsed))) + + // Calculate L2 fees from vault increases + baseFee := vaultIncreases.BaseFeeVault // Use actual vault increase as the source of truth + priorityFee := vaultIncreases.SequencerVault // Use actual vault increase as the source of truth + l2Fee := new(big.Int).Add(baseFee, priorityFee) - baseFee := new(big.Int).Mul(block.BaseFee(), big.NewInt(int64(receipt.GasUsed))) - l2Fee := new(big.Int).Mul(receipt.EffectiveGasPrice, big.NewInt(int64(receipt.GasUsed))) - priorityFee := new(big.Int).Sub(l2Fee, baseFee) - totalFee := new(big.Int).Add(l1Fee, l2Fee) + // Total fee is the sum of all vault increases (excluding OperatorVault which should be zero in Ecotone) + totalFee := new(big.Int).Add(vaultIncreases.BaseFeeVault, vaultIncreases.L1FeeVault) + totalFee.Add(totalFee, vaultIncreases.SequencerVault) walletBalanceDiff := new(big.Int).Sub(startBalance.ToBig(), endBalance.ToBig()) walletBalanceDiff.Sub(walletBalanceDiff, amount) - ef.validateFeeDistribution(l1Fee, baseFee, priorityFee, vaultIncreases) + // Validate total balance first to ensure all fees are accounted for ef.validateTotalBalance(walletBalanceDiff, totalFee, vaultIncreases) + + // Then validate individual fee components + ef.validateFeeDistribution(l1Fee, baseFee, priorityFee, vaultIncreases) ef.validateEcotoneFeatures(receipt, l1Fee) + ef.validateReceiptFees(receipt, l1Fee, baseFee, l2Fee, receiptBaseFee, receiptL2Fee) return EcotoneFeesValidationResult{ TransactionReceipt: receipt, @@ -129,13 +140,15 @@ func (ef *EcotoneFees) validateFeeDistribution(l1Fee, baseFee, priorityFee *big. ef.require.Equal(baseFee, vaults.BaseFeeVault, "Base fee must match BaseFeeVault increase") ef.require.Equal(priorityFee, vaults.SequencerVault, "Priority fee must match SequencerFeeVault increase") - ef.require.True(vaults.OperatorVault.Sign() >= 0, "Operator vault increase must be non-negative") + // In Ecotone, operator fees should not exist (introduced in Isthmus) + ef.require.Equal(vaults.OperatorVault.Cmp(big.NewInt(0)), 0, + "Operator vault increase must be zero in Ecotone (operator fees introduced in Isthmus)") } func (ef *EcotoneFees) validateTotalBalance(walletDiff *big.Int, totalFee *big.Int, vaults VaultBalances) { + // In Ecotone, only BaseFeeVault, L1FeeVault, and SequencerVault should have increases totalVaultIncrease := new(big.Int).Add(vaults.BaseFeeVault, vaults.L1FeeVault) totalVaultIncrease.Add(totalVaultIncrease, vaults.SequencerVault) - totalVaultIncrease.Add(totalVaultIncrease, vaults.OperatorVault) ef.require.Equal(walletDiff, totalFee, "Wallet balance difference must equal total fees") ef.require.Equal(totalVaultIncrease, totalFee, "Total vault increases must equal total fees") @@ -149,6 +162,27 @@ func (ef *EcotoneFees) validateEcotoneFeatures(receipt *types.Receipt, l1Fee *bi ef.require.Greater(receipt.EffectiveGasPrice.Uint64(), uint64(0), "Effective gas price should be > 0") } +func (ef *EcotoneFees) validateReceiptFees(receipt *types.Receipt, l1Fee, vaultBaseFee, vaultL2Fee, receiptBaseFee, receiptL2Fee *big.Int) { + // Check that receipt's L1Fee matches the vault increase + if receipt.L1Fee != nil { + ef.require.Equal(receipt.L1Fee, l1Fee, "Receipt L1Fee must match L1FeeVault increase") + } + + // Sanity check: Receipt-calculated fees should match vault-based fees + ef.require.Equal(receiptBaseFee, vaultBaseFee, + "Receipt-calculated base fee (block.BaseFee * gasUsed) must match BaseFeeVault increase") + ef.require.Equal(receiptL2Fee, vaultL2Fee, + "Receipt-calculated L2 fee (effectiveGasPrice * gasUsed) must match L2 vault increases (BaseFee + SequencerFee)") + + // Validate receipt-based calculations are positive + ef.require.True(receiptBaseFee.Sign() > 0, "Receipt-based base fee must be positive") + ef.require.True(receiptL2Fee.Sign() > 0, "Receipt-based L2 fee must be positive") + + // The effective gas price should be consistent with the calculated L2 fee + ef.require.Equal(receiptL2Fee.Cmp(receiptBaseFee) >= 0, true, + "Receipt L2 fee (effectiveGasPrice * gasUsed) should be >= base fee") +} + func (ef *EcotoneFees) LogResults(result EcotoneFeesValidationResult) { ef.log.Info("Comprehensive Ecotone fees validation completed", "gasUsed", result.TransactionReceipt.GasUsed, diff --git a/op-devstack/dsl/supervisor.go b/op-devstack/dsl/supervisor.go index c3dc800c510..786136ae6f8 100644 --- a/op-devstack/dsl/supervisor.go +++ b/op-devstack/dsl/supervisor.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strings" "time" "github.com/ethereum-optimism/optimism/op-devstack/stack" @@ -94,12 +95,18 @@ func (s *Supervisor) FetchSyncStatus() eth.SupervisorSyncStatus { s.log.Debug("Fetching supervisor sync status") ctx, cancel := context.WithTimeout(s.ctx, DefaultTimeout) defer cancel() - syncStatus, err := retry.Do(ctx, 2, retry.Fixed(500*time.Millisecond), func() (eth.SupervisorSyncStatus, error) { + syncStatus, err := retry.Do(ctx, 10, retry.Fixed(500*time.Millisecond), func() (eth.SupervisorSyncStatus, error) { ctx, cancel := context.WithTimeout(s.ctx, 300*time.Millisecond) defer cancel() syncStatus, err := s.inner.QueryAPI().SyncStatus(ctx) if errors.Is(err, status.ErrStatusTrackerNotReady) { s.log.Debug("Sync status not ready from supervisor") + return syncStatus, err + } + // Check for L1 sync mismatch error and retry + if err != nil && strings.Contains(err.Error(), "min synced L1 mismatch") { + s.log.Debug("L1 sync mismatch, retrying", "error", err) + return syncStatus, err } return syncStatus, err }) diff --git a/op-devstack/presets/minimal_external_el.go b/op-devstack/presets/minimal_external_el.go new file mode 100644 index 00000000000..a14bc897abc --- /dev/null +++ b/op-devstack/presets/minimal_external_el.go @@ -0,0 +1,60 @@ +package presets + +import ( + "github.com/ethereum/go-ethereum/log" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-devstack/shim" + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-devstack/stack/match" + "github.com/ethereum-optimism/optimism/op-devstack/sysgo" + "github.com/ethereum-optimism/optimism/op-service/eth" +) + +type MinimalExternalEL struct { + Log log.Logger + T devtest.T + ControlPlane stack.ControlPlane + + L1Network *dsl.L1Network + L1EL *dsl.L1ELNode + + L2Chain *dsl.L2Network + L2CL *dsl.L2CLNode + L2EL *dsl.L2ELNode + + SyncTester *dsl.SyncTester +} + +func (m *MinimalExternalEL) L2Networks() []*dsl.L2Network { + return []*dsl.L2Network{ + m.L2Chain, + } +} + +func WithMinimalExternalELWithSuperchainRegistry(l1CLBeaconRPC, l1ELRPC, l2ELRPC string, l1ChainID eth.ChainID, networkName string, fcu eth.FCUState) stack.CommonOption { + return stack.MakeCommon(sysgo.DefaultMinimalExternalELSystemWithEndpointAndSuperchainRegistry(&sysgo.DefaultMinimalExternalELSystemIDs{}, l1CLBeaconRPC, l1ELRPC, l2ELRPC, l1ChainID, networkName, fcu)) +} + +func NewMinimalExternalELWithExternalL1(t devtest.T) *MinimalExternalEL { + system := shim.NewSystem(t) + orch := Orchestrator() + orch.Hydrate(system) + + l2 := system.L2Network(match.Assume(t, match.L2ChainA)) + verifierCL := l2.L2CLNode(match.FirstL2CL) + syncTester := l2.SyncTester(match.Assume(t, match.FirstSyncTester)) + + return &MinimalExternalEL{ + Log: t.Logger(), + T: t, + ControlPlane: orch.ControlPlane(), + L1Network: dsl.NewL1Network(system.L1Network(match.FirstL1Network)), + L1EL: dsl.NewL1ELNode(system.L1Network(match.FirstL1Network).L1ELNode(match.FirstL1EL)), + L2Chain: dsl.NewL2Network(l2, orch.ControlPlane()), + L2CL: dsl.NewL2CLNode(verifierCL, orch.ControlPlane()), + L2EL: dsl.NewL2ELNode(l2.L2ELNode(match.FirstL2EL), orch.ControlPlane()), + SyncTester: dsl.NewSyncTester(syncTester), + } +} diff --git a/op-devstack/sysgo/faucet.go b/op-devstack/sysgo/faucet.go index 0d8f80717aa..f746ade136b 100644 --- a/op-devstack/sysgo/faucet.go +++ b/op-devstack/sysgo/faucet.go @@ -23,6 +23,10 @@ type FaucetService struct { } func (n *FaucetService) hydrate(system stack.ExtensibleSystem) { + if n == nil || n.service == nil { + return + } + require := system.T().Require() for faucetID, chainID := range n.service.Faucets() { diff --git a/op-devstack/sysgo/l1_nodes.go b/op-devstack/sysgo/l1_nodes.go index 272b15ccef0..750182edbc1 100644 --- a/op-devstack/sysgo/l1_nodes.go +++ b/op-devstack/sysgo/l1_nodes.go @@ -113,3 +113,24 @@ func WithL1Nodes(l1ELID stack.L1ELNodeID, l1CLID stack.L1CLNodeID) stack.Option[ require.True(orch.l1CLs.SetIfMissing(l1CLID, l1CLNode), "must not already exist") }) } + +// WithExtL1Nodes initializes L1 EL and CL nodes that connect to external RPC endpoints +func WithExtL1Nodes(l1ELID stack.L1ELNodeID, l1CLID stack.L1CLNodeID, elRPCEndpoint string, clRPCEndpoint string) stack.Option[*Orchestrator] { + return stack.AfterDeploy(func(orch *Orchestrator) { + require := orch.P().Require() + + // Create L1 EL node with external RPC + l1ELNode := &L1ELNode{ + id: l1ELID, + userRPC: elRPCEndpoint, + } + require.True(orch.l1ELs.SetIfMissing(l1ELID, l1ELNode), "must not already exist") + + // Create L1 CL node with external RPC + l1CLNode := &L1CLNode{ + id: l1CLID, + beaconHTTPAddr: clRPCEndpoint, + } + require.True(orch.l1CLs.SetIfMissing(l1CLID, l1CLNode), "must not already exist") + }) +} diff --git a/op-devstack/sysgo/l2_cl_opnode.go b/op-devstack/sysgo/l2_cl_opnode.go index d0377b63aa8..d290496a57c 100644 --- a/op-devstack/sysgo/l2_cl_opnode.go +++ b/op-devstack/sysgo/l2_cl_opnode.go @@ -256,7 +256,7 @@ func WithOpNode(l2CLID stack.L2CLNodeID, l1CLID stack.L1CLNodeID, l1ELID stack.L L2EngineJWTSecret: jwtSecret, }, Beacon: &config.L1BeaconEndpointConfig{ - BeaconAddr: l1CL.beacon.BeaconAddr(), + BeaconAddr: l1CL.beaconHTTPAddr, }, Driver: driver.Config{ SequencerEnabled: cfg.IsSequencer, diff --git a/op-devstack/sysgo/l2_network_superchain_registry.go b/op-devstack/sysgo/l2_network_superchain_registry.go new file mode 100644 index 00000000000..f395d2e813d --- /dev/null +++ b/op-devstack/sysgo/l2_network_superchain_registry.go @@ -0,0 +1,86 @@ +package sysgo + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/core" + + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-node/chaincfg" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/superutil" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/backend/depset" +) + +// WithL2NetworkFromSuperchainRegistry creates an L2 network using the rollup config from the superchain registry +func WithL2NetworkFromSuperchainRegistry(l2NetworkID stack.L2NetworkID, networkName string) stack.Option[*Orchestrator] { + return stack.BeforeDeploy(func(orch *Orchestrator) { + p := orch.P().WithCtx(stack.ContextWithID(orch.P().Ctx(), l2NetworkID)) + require := p.Require() + + // Load the rollup config from the superchain registry + rollupCfg, err := chaincfg.GetRollupConfig(networkName) + require.NoError(err, "failed to load rollup config for network %s", networkName) + + // Get the chain config from the superchain registry + chainCfg := chaincfg.ChainByName(networkName) + require.NotNil(chainCfg, "chain config not found for network %s", networkName) + + // Load the chain config using superutil + paramsChainConfig, err := superutil.LoadOPStackChainConfigFromChainID(chainCfg.ChainID) + require.NoError(err, "failed to load chain config for network %s", networkName) + + // Create a genesis config from the chain config + genesis := &core.Genesis{ + Config: paramsChainConfig, + } + + // Create the L2 network + l2Net := &L2Network{ + id: l2NetworkID, + l1ChainID: eth.ChainIDFromBig(rollupCfg.L1ChainID), + genesis: genesis, + rollupCfg: rollupCfg, + keys: orch.keys, + } + + require.True(orch.l2Nets.SetIfMissing(l2NetworkID.ChainID(), l2Net), + fmt.Sprintf("must not already exist: %s", l2NetworkID)) + }) +} + +// WithL2NetworkFromSuperchainRegistryWithDependencySet creates an L2 network using the rollup config from the superchain registry +// and also sets up the dependency set for interop support +func WithL2NetworkFromSuperchainRegistryWithDependencySet(l2NetworkID stack.L2NetworkID, networkName string) stack.Option[*Orchestrator] { + return stack.Combine( + WithL2NetworkFromSuperchainRegistry(l2NetworkID, networkName), + stack.BeforeDeploy(func(orch *Orchestrator) { + p := orch.P().WithCtx(stack.ContextWithID(orch.P().Ctx(), l2NetworkID)) + require := p.Require() + + // Load the dependency set from the superchain registry + chainCfg := chaincfg.ChainByName(networkName) + require.NotNil(chainCfg, "chain config not found for network %s", networkName) + + _, err := depset.FromRegistry(eth.ChainIDFromUInt64(chainCfg.ChainID)) + if err != nil { + // If dependency set is not available, that's okay - it's optional + p.Logger().Info("No dependency set available for network", "network", networkName, "err", err) + return + } + + // Create a cluster to hold the dependency set + clusterID := stack.ClusterID(networkName) + + // Create a minimal full config set with just the dependency set + // This is a simplified approach - in a real implementation you might want + // to create a proper FullConfigSetMerged + cluster := &Cluster{ + id: clusterID, + cfgset: depset.FullConfigSetMerged{}, // Empty for now + } + + orch.clusters.Set(clusterID, cluster) + }), + ) +} diff --git a/op-devstack/sysgo/sync_tester.go b/op-devstack/sysgo/sync_tester.go index 2e15316dc6c..ace022fd639 100644 --- a/op-devstack/sysgo/sync_tester.go +++ b/op-devstack/sysgo/sync_tester.go @@ -8,6 +8,7 @@ import ( "github.com/ethereum-optimism/optimism/op-devstack/stack" "github.com/ethereum-optimism/optimism/op-service/client" "github.com/ethereum-optimism/optimism/op-service/endpoint" + "github.com/ethereum-optimism/optimism/op-service/eth" oprpc "github.com/ethereum-optimism/optimism/op-service/rpc" "github.com/ethereum-optimism/optimism/op-sync-tester/config" @@ -88,3 +89,43 @@ func WithSyncTester(syncTesterID stack.SyncTesterID, l2ELs []stack.L2ELNodeID) s orch.syncTester = &SyncTesterService{id: syncTesterID, service: srv} }) } + +func WithSyncTesterWithExternalEndpoint(syncTesterID stack.SyncTesterID, endpointRPC string, chainID eth.ChainID) stack.Option[*Orchestrator] { + return stack.AfterDeploy(func(orch *Orchestrator) { + p := orch.P().WithCtx(stack.ContextWithID(orch.P().Ctx(), syncTesterID)) + + require := p.Require() + + require.Nil(orch.syncTester, "can only support a single sync-tester-service in sysgo") + + syncTesters := make(map[sttypes.SyncTesterID]*stconf.SyncTesterEntry) + + // Create a sync tester entry with the external endpoint + id := sttypes.SyncTesterID(fmt.Sprintf("dev-sync-tester-%s", chainID)) + syncTesters[id] = &stconf.SyncTesterEntry{ + ELRPC: endpoint.MustRPC{Value: endpoint.URL(endpointRPC)}, + ChainID: chainID, + } + + cfg := &config.Config{ + RPC: oprpc.CLIConfig{ + ListenAddr: "127.0.0.1", + }, + SyncTesters: &stconf.Config{ + SyncTesters: syncTesters, + }, + } + logger := p.Logger() + srv, err := synctester.FromConfig(p.Ctx(), cfg, logger) + require.NoError(err, "must setup sync tester service") + require.NoError(srv.Start(p.Ctx())) + p.Cleanup(func() { + ctx, cancel := context.WithCancel(context.Background()) + cancel() // force-quit + logger.Info("Closing sync tester") + _ = srv.Stop(ctx) + logger.Info("Closed sync tester") + }) + orch.syncTester = &SyncTesterService{id: syncTesterID, service: srv} + }) +} diff --git a/op-devstack/sysgo/system_synctester_ext.go b/op-devstack/sysgo/system_synctester_ext.go new file mode 100644 index 00000000000..4d6ef8e211f --- /dev/null +++ b/op-devstack/sysgo/system_synctester_ext.go @@ -0,0 +1,94 @@ +package sysgo + +import ( + "fmt" + "math/big" + + "github.com/ethereum-optimism/optimism/op-chain-ops/devkeys" + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-node/chaincfg" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/params" +) + +type DefaultMinimalExternalELSystemIDs struct { + L1 stack.L1NetworkID + L1EL stack.L1ELNodeID + L1CL stack.L1CLNodeID + + L2 stack.L2NetworkID + L2CL stack.L2CLNodeID + L2EL stack.L2ELNodeID + + SyncTester stack.SyncTesterID +} + +func NewDefaultMinimalExternalELSystemIDs(l1ID, l2ID eth.ChainID) DefaultMinimalExternalELSystemIDs { + ids := DefaultMinimalExternalELSystemIDs{ + L1: stack.L1NetworkID(l1ID), + L1EL: stack.NewL1ELNodeID("l1", l1ID), + L1CL: stack.NewL1CLNodeID("l1", l1ID), + L2: stack.L2NetworkID(l2ID), + L2CL: stack.NewL2CLNodeID("verifier", l2ID), + L2EL: stack.NewL2ELNodeID("sync-tester-el", l2ID), + SyncTester: stack.NewSyncTesterID("sync-tester", l2ID), + } + return ids +} + +// DefaultMinimalExternalELSystemWithEndpointAndSuperchainRegistry creates a minimal external EL system +// using a network from the superchain registry instead of the deployer +func DefaultMinimalExternalELSystemWithEndpointAndSuperchainRegistry(dest *DefaultMinimalExternalELSystemIDs, l1CLBeaconRPC, l1ELRPC, l2ELRPC string, l1ChainID eth.ChainID, networkName string, fcu eth.FCUState) stack.Option[*Orchestrator] { + chainCfg := chaincfg.ChainByName(networkName) + if chainCfg == nil { + panic(fmt.Sprintf("network %s not found in superchain registry", networkName)) + } + l2ChainID := eth.ChainIDFromUInt64(chainCfg.ChainID) + + ids := NewDefaultMinimalExternalELSystemIDs(l1ChainID, l2ChainID) + + opt := stack.Combine[*Orchestrator]() + opt.Add(stack.BeforeDeploy(func(o *Orchestrator) { + o.P().Logger().Info("Setting up with superchain registry network", "network", networkName) + })) + + opt.Add(WithMnemonicKeys(devkeys.TestMnemonic)) + + // Skip deployer since we're using external L1 and superchain registry for L2 config + // Create L1 network record for external L1 + opt.Add(stack.BeforeDeploy(func(o *Orchestrator) { + chainID, _ := ids.L1.ChainID().Uint64() + l1Net := &L1Network{ + id: ids.L1, + genesis: &core.Genesis{ + Config: ¶ms.ChainConfig{ + ChainID: big.NewInt(int64(chainID)), + }, + }, + blockTime: 12, + } + o.l1Nets.Set(ids.L1.ChainID(), l1Net) + })) + + opt.Add(WithExtL1Nodes(ids.L1EL, ids.L1CL, l1ELRPC, l1CLBeaconRPC)) + + // Use superchain registry instead of deployer + opt.Add(WithL2NetworkFromSuperchainRegistryWithDependencySet( + stack.L2NetworkID(l2ChainID), + networkName, + )) + + // Add SyncTester service with external endpoint + opt.Add(WithSyncTesterWithExternalEndpoint(ids.SyncTester, l2ELRPC, l2ChainID)) + + // Add SyncTesterL2ELNode as the L2EL replacement for real-world EL endpoint + opt.Add(WithSyncTesterL2ELNode(ids.L2EL, ids.L2EL, fcu)) + opt.Add(WithL2CLNode(ids.L2CL, ids.L1CL, ids.L1EL, ids.L2EL)) + + opt.Add(stack.Finally(func(orch *Orchestrator) { + *dest = ids + })) + + return opt +} diff --git a/op-e2e/config/init.go b/op-e2e/config/init.go index 944f02755a3..7202d0677ca 100644 --- a/op-e2e/config/init.go +++ b/op-e2e/config/init.go @@ -16,6 +16,7 @@ import ( "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/artifacts" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/inspect" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/pipeline" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/state" "github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum/go-ethereum/common" @@ -386,6 +387,7 @@ func defaultIntent(root string, loc *artifacts.Locator, deployer common.Address, Eip1559Denominator: 250, Eip1559DenominatorCanyon: 250, Eip1559Elasticity: 6, + GasLimit: standard.GasLimit, Roles: state.ChainRoles{ // Use deployer as L1PAO to deploy additional dispute impls L1ProxyAdminOwner: deployer, diff --git a/op-e2e/e2eutils/intentbuilder/builder.go b/op-e2e/e2eutils/intentbuilder/builder.go index 8b8598b9ed7..b75d25fc9a3 100644 --- a/op-e2e/e2eutils/intentbuilder/builder.go +++ b/op-e2e/e2eutils/intentbuilder/builder.go @@ -195,6 +195,7 @@ func (b *intentBuilder) WithL2(l2ChainID eth.ChainID) (Builder, L2Configurator) Eip1559DenominatorCanyon: standard.Eip1559DenominatorCanyon, Eip1559Denominator: standard.Eip1559Denominator, Eip1559Elasticity: standard.Eip1559Elasticity, + GasLimit: standard.GasLimit, DeployOverrides: make(map[string]any), } b.intent.Chains = append(b.intent.Chains, chainIntent) diff --git a/op-e2e/e2eutils/intentbuilder/builder_test.go b/op-e2e/e2eutils/intentbuilder/builder_test.go index b2ab4be7e9c..c2b40da9b83 100644 --- a/op-e2e/e2eutils/intentbuilder/builder_test.go +++ b/op-e2e/e2eutils/intentbuilder/builder_test.go @@ -13,6 +13,7 @@ import ( "github.com/ethereum-optimism/optimism/op-chain-ops/addresses" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/artifacts" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/state" "github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-service/eth" @@ -158,6 +159,7 @@ func TestBuilder(t *testing.T) { Eip1559DenominatorCanyon: 250, Eip1559Denominator: 50, Eip1559Elasticity: 10, + GasLimit: standard.GasLimit, OperatorFeeScalar: 100, OperatorFeeConstant: 200, CustomGasToken: &state.CustomGasToken{ diff --git a/op-node/flags/flags.go b/op-node/flags/flags.go index d7b8ece67a1..e032e14c439 100644 --- a/op-node/flags/flags.go +++ b/op-node/flags/flags.go @@ -171,7 +171,7 @@ var ( L1CacheSize = &cli.UintFlag{ Name: "l1.cache-size", Usage: "Cache size for blocks, receipts and transactions. " + - "If this flag is set to 0, 2/3 of the sequencing window size is used (usually 2400). " + + "If this flag is set to 0, 3/2 of the sequencing window size is used (usually 2400). " + "The default value of 900 (~3h of L1 blocks) is good for (high-throughput) networks that see frequent safe head increments. " + "On (low-throughput) networks with infrequent safe head increments, it is recommended to set this value to 0, " + "or a value that well covers the typical span between safe head increments. " + diff --git a/op-sync-tester/synctester/backend/sync_tester.go b/op-sync-tester/synctester/backend/sync_tester.go index c1373332122..201dcbf8b05 100644 --- a/op-sync-tester/synctester/backend/sync_tester.go +++ b/op-sync-tester/synctester/backend/sync_tester.go @@ -93,7 +93,7 @@ func (s *SyncTester) fetchSession(ctx context.Context) (*eth.SyncTesterSession, return nil, fmt.Errorf("session already deleted: %s", session.SessionID) } if existing, ok := s.sessions[session.SessionID]; ok { - s.log.Info("Using existing session", "session", existing) + s.log.Debug("Using existing session", "session", existing) return existing, nil } else { s.storeSession(session) diff --git a/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol b/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol index ca2c3ebe144..81c5f554f2d 100644 --- a/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol +++ b/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol @@ -24,8 +24,10 @@ interface ISystemConfig is IProxyAdminOwnedBase { } error ReinitializableBase_ZeroInitVersion(); + error SystemConfig_InvalidFeatureState(); event ConfigUpdate(uint256 indexed version, UpdateType indexed updateType, bytes data); + event FeatureSet(bytes32 indexed feature, bool indexed enabled); event Initialized(uint8 version); event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); @@ -92,6 +94,8 @@ interface ISystemConfig is IProxyAdminOwnedBase { function paused() external view returns (bool); function superchainConfig() external view returns (ISuperchainConfig); function guardian() external view returns (address); + function setFeature(bytes32 _feature, bool _enabled) external; + function isFeatureEnabled(bytes32) external view returns (bool); function __constructor__() external; } diff --git a/packages/contracts-bedrock/snapshots/abi/SystemConfig.json b/packages/contracts-bedrock/snapshots/abi/SystemConfig.json index a295b986db2..03f82fcb103 100644 --- a/packages/contracts-bedrock/snapshots/abi/SystemConfig.json +++ b/packages/contracts-bedrock/snapshots/abi/SystemConfig.json @@ -413,6 +413,25 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "isFeatureEnabled", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "l1CrossDomainMessenger", @@ -704,6 +723,24 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_feature", + "type": "bytes32" + }, + { + "internalType": "bool", + "name": "_enabled", + "type": "bool" + } + ], + "name": "setFeature", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -892,6 +929,25 @@ "name": "ConfigUpdate", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "feature", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bool", + "name": "enabled", + "type": "bool" + } + ], + "name": "FeatureSet", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -958,5 +1014,10 @@ "inputs": [], "name": "ReinitializableBase_ZeroInitVersion", "type": "error" + }, + { + "inputs": [], + "name": "SystemConfig_InvalidFeatureState", + "type": "error" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 97e23831d85..2a01a6a46ef 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -24,8 +24,8 @@ "sourceCodeHash": "0x7dedc013126acc2a7e6cc8fb32e0212b79446271003f2483e2bd2c93ba774328" }, "src/L1/OPContractsManagerStandardValidator.sol:OPContractsManagerStandardValidator": { - "initCodeHash": "0xe83e83cddbcae1d6af62f9754517b25f95ae371f3843dd8ee8030ddf8ba4622d", - "sourceCodeHash": "0x5faad71f147b776ed4d3290d319d1f4697e4e37b50eee5a4ef52f901c04efaa0" + "initCodeHash": "0x3e8a3bed20aa10b6d285cd926903c4d8c34aad3ba5c0e9e32da3cde13b179238", + "sourceCodeHash": "0x6a31abe2f73c7279a00a8fcecb6741af8e6fe54a1112420e6a15859753487dbb" }, "src/L1/OptimismPortal2.sol:OptimismPortal2": { "initCodeHash": "0xb77226987310e895fed89a0083502da15bc347e5c41fe2a1287d594aa290d5a2", @@ -40,8 +40,8 @@ "sourceCodeHash": "0xad12c20a00dc20683bd3f68e6ee254f968da6cc2d98930be6534107ee5cb11d9" }, "src/L1/SystemConfig.sol:SystemConfig": { - "initCodeHash": "0x07b7039de5b8a4dc57642ee9696e949d70516b7f6dce41dde4920efb17105ef2", - "sourceCodeHash": "0x997212ceadabb306c2abd31918b09bccbba0b21662c1d8930a3599831c374b13" + "initCodeHash": "0x8c6a1fb65d650525a3cd3fd617ae923d44da157b2ef928ab8ec39dbf39e25a98", + "sourceCodeHash": "0xdf526027678f23da79a56c1d6127a3ed3fd92927ec8d5541982b6f1c2119f70e" }, "src/L2/BaseFeeVault.sol:BaseFeeVault": { "initCodeHash": "0x9b664e3d84ad510091337b4aacaa494b142512e2f6f7fbcdb6210ed62ca9b885", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json b/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json index be5a739fa69..53d4ca20c46 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json @@ -124,5 +124,12 @@ "offset": 0, "slot": "108", "type": "contract ISuperchainConfig" + }, + { + "bytes": "32", + "label": "isFeatureEnabled", + "offset": 0, + "slot": "109", + "type": "mapping(bytes32 => bool)" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L1/OPContractsManagerStandardValidator.sol b/packages/contracts-bedrock/src/L1/OPContractsManagerStandardValidator.sol index cd63fa40893..64f48d1624b 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManagerStandardValidator.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManagerStandardValidator.sol @@ -36,8 +36,8 @@ import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; /// before and after an upgrade. contract OPContractsManagerStandardValidator is ISemver { /// @notice The semantic version of the OPContractsManagerStandardValidator contract. - /// @custom:semver 1.8.0 - string public constant version = "1.8.0"; + /// @custom:semver 1.9.0 + string public constant version = "1.9.0"; /// @notice The SuperchainConfig contract. ISuperchainConfig public superchainConfig; @@ -176,7 +176,7 @@ contract OPContractsManagerStandardValidator is ISemver { /// @notice Returns the expected SystemConfig version. function systemConfigVersion() public pure returns (string memory) { - return "3.4.0"; + return "3.5.0"; } /// @notice Returns the expected OptimismPortal version. diff --git a/packages/contracts-bedrock/src/L1/SystemConfig.sol b/packages/contracts-bedrock/src/L1/SystemConfig.sol index 604a9936d3a..48a9c63f2a7 100644 --- a/packages/contracts-bedrock/src/L1/SystemConfig.sol +++ b/packages/contracts-bedrock/src/L1/SystemConfig.sol @@ -135,16 +135,28 @@ contract SystemConfig is ProxyAdminOwnedBase, OwnableUpgradeable, Reinitializabl /// @notice The SuperchainConfig contract that manages the pause state. ISuperchainConfig public superchainConfig; + /// @notice Bytes32 feature flag name to boolean enabled value. + mapping(bytes32 => bool) public isFeatureEnabled; + /// @notice Emitted when configuration is updated. /// @param version SystemConfig version. /// @param updateType Type of update. /// @param data Encoded update data. event ConfigUpdate(uint256 indexed version, UpdateType indexed updateType, bytes data); + /// @notice Emitted when a feature is set. + /// @param feature Feature that was set. + /// @param enabled Whether the feature is enabled. + event FeatureSet(bytes32 indexed feature, bool indexed enabled); + + /// @notice Thrown when attempting to enable/disable a feature when already enabled/disabled, + /// respectively. + error SystemConfig_InvalidFeatureState(); + /// @notice Semantic version. - /// @custom:semver 3.4.0 + /// @custom:semver 3.5.0 function version() public pure virtual returns (string memory) { - return "3.4.0"; + return "3.5.0"; } /// @notice Constructs the SystemConfig contract. @@ -484,6 +496,27 @@ contract SystemConfig is ProxyAdminOwnedBase, OwnableUpgradeable, Reinitializabl _resourceConfig = _config; } + /// @notice Sets a feature flag enabled or disabled. Can only be called by the ProxyAdmin or + /// its owner. + /// @param _feature Feature to set. + /// @param _enabled Whether the feature should be enabled or disabled. + function setFeature(bytes32 _feature, bool _enabled) external { + // Features can only be set by the ProxyAdmin or its owner. + _assertOnlyProxyAdminOrProxyAdminOwner(); + + // As a sanity check, prevent users from enabling the feature if already enabled or + // disabling the feature if already disabled. This helps to prevent accidental misuse. + if ((_enabled && isFeatureEnabled[_feature]) || (!_enabled && !isFeatureEnabled[_feature])) { + revert SystemConfig_InvalidFeatureState(); + } + + // Set the feature. + isFeatureEnabled[_feature] = _enabled; + + // Emit an event. + emit FeatureSet(_feature, _enabled); + } + /// @notice Returns the current pause state of the system by checking if the SuperchainConfig is paused for this /// chain's ETHLockbox. /// @return bool True if the system is paused, false otherwise. diff --git a/packages/contracts-bedrock/src/libraries/Features.sol b/packages/contracts-bedrock/src/libraries/Features.sol new file mode 100644 index 00000000000..1521b1d1a33 --- /dev/null +++ b/packages/contracts-bedrock/src/libraries/Features.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @notice Features is a library that stores feature name constants. Can be used alongside the +/// feature flagging functionality in the SystemConfig contract to selectively enable or +/// disable customizable features of the OP Stack. +library Features { + /// @notice The ETH_LOCKBOX feature determines if the system is configured to use the + /// ETHLockbox contract in the OptimismPortal. When the ETH_LOCKBOX feature is active + /// and the ETHLockbox contract has been configured, the OptimismPortal will use the + /// ETHLockbox to store ETH instead of storing ETH directly in the portal itself. + bytes32 internal constant ETH_LOCKBOX = "ETH_LOCKBOX"; +} diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index 775834fcf87..7a322fd0bcb 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -475,7 +475,7 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { // Make sure that the SystemConfig is upgraded to the right version. It must also have the // right l2ChainId and must be properly initialized. - assertEq(ISemver(address(systemConfig)).version(), "3.4.0"); + assertEq(ISemver(address(systemConfig)).version(), "3.5.0"); assertEq(impls.systemConfigImpl, EIP1967Helper.getImplementation(address(systemConfig))); assertEq(systemConfig.l2ChainId(), l2ChainId); DeployUtils.assertInitialized({ _contractAddress: address(systemConfig), _isProxy: true, _slot: 0, _offset: 0 }); diff --git a/packages/contracts-bedrock/test/L1/SystemConfig.t.sol b/packages/contracts-bedrock/test/L1/SystemConfig.t.sol index 9b1e1c3d813..92d758db035 100644 --- a/packages/contracts-bedrock/test/L1/SystemConfig.t.sol +++ b/packages/contracts-bedrock/test/L1/SystemConfig.t.sol @@ -9,6 +9,7 @@ import { ForgeArtifacts, StorageSlot } from "scripts/libraries/ForgeArtifacts.so // Libraries import { Constants } from "src/libraries/Constants.sol"; +import { Features } from "src/libraries/Features.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; // Interfaces @@ -725,6 +726,144 @@ contract SystemConfig_Paused_Test is SystemConfig_TestInit { } } +/// @title SystemConfig_SetFeature_Test +/// @notice Test contract for SystemConfig `setFeature` function. +contract SystemConfig_SetFeature_Test is SystemConfig_TestInit { + event FeatureSet(bytes32 indexed feature, bool indexed enabled); + + /// @notice Tests that `setFeature` reverts if the caller is not ProxyAdmin or ProxyAdmin owner. + /// @param _sender The address to test. + function testFuzz_setFeature_notProxyAdminOrProxyAdminOwner_reverts(address _sender) external { + // Ensure sender is not ProxyAdmin or ProxyAdmin owner + vm.assume(_sender != address(systemConfig.proxyAdmin()) && _sender != systemConfig.proxyAdminOwner()); + + vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner.selector); + vm.prank(_sender); + systemConfig.setFeature(Features.ETH_LOCKBOX, true); + } + + /// @notice Tests that `setFeature` enables a feature successfully when called by ProxyAdmin. + function test_setFeature_enableFeatureByProxyAdmin_succeeds() external { + // Verify feature is initially disabled + assertFalse(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); + + vm.expectEmit(address(systemConfig)); + emit FeatureSet(Features.ETH_LOCKBOX, true); + + vm.prank(address(systemConfig.proxyAdmin())); + systemConfig.setFeature(Features.ETH_LOCKBOX, true); + + // Verify feature is now enabled + assertTrue(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); + } + + /// @notice Tests that `setFeature` disables a feature successfully when called by ProxyAdmin. + function test_setFeature_disableFeatureByProxyAdmin_succeeds() external { + // First enable the feature + vm.prank(address(systemConfig.proxyAdmin())); + systemConfig.setFeature(Features.ETH_LOCKBOX, true); + assertTrue(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); + + vm.expectEmit(address(systemConfig)); + emit FeatureSet(Features.ETH_LOCKBOX, false); + + vm.prank(address(systemConfig.proxyAdmin())); + systemConfig.setFeature(Features.ETH_LOCKBOX, false); + + // Verify feature is now disabled + assertFalse(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); + } + + /// @notice Tests that `setFeature` enables a feature successfully when called by ProxyAdmin owner. + function test_setFeature_enableFeatureByProxyAdminOwner_succeeds() external { + // Verify feature is initially disabled + assertFalse(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); + + vm.expectEmit(address(systemConfig)); + emit FeatureSet(Features.ETH_LOCKBOX, true); + + vm.prank(systemConfig.proxyAdminOwner()); + systemConfig.setFeature(Features.ETH_LOCKBOX, true); + + // Verify feature is now enabled + assertTrue(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); + } + + /// @notice Tests that `setFeature` can toggle the same feature multiple times. + function test_setFeature_multipleToggles_succeeds() external { + address proxyAdmin = address(systemConfig.proxyAdmin()); + + // Initially disabled + assertFalse(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); + + // Enable feature + vm.prank(proxyAdmin); + systemConfig.setFeature(Features.ETH_LOCKBOX, true); + assertTrue(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); + + // Disable feature + vm.prank(proxyAdmin); + systemConfig.setFeature(Features.ETH_LOCKBOX, false); + assertFalse(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); + + // Enable again + vm.prank(proxyAdmin); + systemConfig.setFeature(Features.ETH_LOCKBOX, true); + assertTrue(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); + } + + /// @notice Tests that `setFeature` reverts when trying to enable a feature that is already + /// enabled. + function test_setFeature_alreadyEnabled_reverts() external { + vm.prank(address(systemConfig.proxyAdmin())); + systemConfig.setFeature(Features.ETH_LOCKBOX, true); + assertTrue(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); + + vm.prank(address(systemConfig.proxyAdmin())); + vm.expectRevert(ISystemConfig.SystemConfig_InvalidFeatureState.selector); + systemConfig.setFeature(Features.ETH_LOCKBOX, true); + } + + /// @notice Tests that `setFeature` reverts when trying to disable a feature that is already + /// disabled. + function test_setFeature_alreadyDisabled_reverts() external { + vm.prank(address(systemConfig.proxyAdmin())); + vm.expectRevert(ISystemConfig.SystemConfig_InvalidFeatureState.selector); + systemConfig.setFeature(Features.ETH_LOCKBOX, false); + } +} + +/// @title SystemConfig_IsFeatureEnabled_Test +/// @notice Test contract for SystemConfig `isFeatureEnabled` function. +contract SystemConfig_IsFeatureEnabled_Test is SystemConfig_TestInit { + /// @notice Tests that `isFeatureEnabled` returns false for unset features. + /// @param _feature The feature to check. + function testFuzz_isFeatureEnabled_unsetFeature_succeeds(bytes32 _feature) external view { + assertFalse(systemConfig.isFeatureEnabled(_feature)); + } + + /// @notice Tests that `isFeatureEnabled` returns correct value after feature is enabled. + function test_isFeatureEnabled_afterEnable_succeeds() external { + vm.prank(address(systemConfig.proxyAdmin())); + systemConfig.setFeature(Features.ETH_LOCKBOX, true); + + assertTrue(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); + } + + /// @notice Tests that `isFeatureEnabled` returns correct value after feature is disabled. + function test_isFeatureEnabled_afterDisable_succeeds() external { + // First enable the feature + vm.prank(address(systemConfig.proxyAdmin())); + systemConfig.setFeature(Features.ETH_LOCKBOX, true); + assertTrue(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); + + // Then disable it + vm.prank(address(systemConfig.proxyAdmin())); + systemConfig.setFeature(Features.ETH_LOCKBOX, false); + assertFalse(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); + } +} + /// @title SystemConfig_Guardian_Test /// @notice Test contract for SystemConfig `guardian` function. contract SystemConfig_Guardian_Test is SystemConfig_TestInit {