diff --git a/.circleci/config.yml b/.circleci/config.yml index a209d32849aee..5776446dd0905 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1182,37 +1182,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 +1245,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 working_directory: op-acceptance-tests no_output_timeout: 1h environment: @@ -1249,8 +1253,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 @@ -2475,13 +2483,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 @@ -2494,6 +2500,7 @@ workflows: name: kurtosis-simple devnet: simple gate: base + use_circleci_runner: true # CircleCI params no_output_timeout: 30m context: @@ -2507,6 +2514,7 @@ workflows: name: kurtosis-isthmus devnet: isthmus gate: isthmus + use_circleci_runner: true # CircleCI params no_output_timeout: 30m context: @@ -2520,6 +2528,7 @@ workflows: name: kurtosis-interop devnet: interop gate: interop + use_circleci_runner: true # CircleCI params no_output_timeout: 30m context: @@ -2553,13 +2562,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 @@ -2572,6 +2579,7 @@ workflows: name: kurtosis-simple devnet: simple gate: base + use_circleci_runner: true # CircleCI params no_output_timeout: 30m context: @@ -2585,6 +2593,7 @@ workflows: name: kurtosis-isthmus devnet: isthmus gate: isthmus + use_circleci_runner: true # CircleCI params no_output_timeout: 30m context: @@ -2598,6 +2607,7 @@ workflows: name: kurtosis-interop devnet: interop gate: interop + use_circleci_runner: true # CircleCI params no_output_timeout: 30m context: diff --git a/mise.toml b/mise.toml index 0fcaefa823df7..4ab2451c6897f 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.0.1" # 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 183bee525b222..78b077939ee48 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 1b18c7f39cf3e..f5e737d14de3a 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 4c761439037df..ae000e84111d8 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.0.1") 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 de9125ac670a0..d2b40beb723a9 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 eefd8def79b53..10852f673eb2c 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/interop/message/supervisor_smoke_test.go b/op-acceptance-tests/tests/interop/message/supervisor_smoke_test.go index 59cc56d309245..36d9817eacc39 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/proofs/withdrawal/withdrawal_test.go b/op-acceptance-tests/tests/interop/proofs/withdrawal/withdrawal_test.go index c0acd53d59950..eff0185c1659e 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/sync/multisupervisor_interop/interop_sync_test.go b/op-acceptance-tests/tests/interop/sync/multisupervisor_interop/interop_sync_test.go index 1190322cc1e30..e7dc6dbadeeaf 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 95fe56b4dc65a..f7dcd44e6a068 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 6ec1266d1fe95..4531c9a2334d1 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 4eea3a7d81370..8f5f23567fadd 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-devstack/dsl/ecotone_fees.go b/op-devstack/dsl/ecotone_fees.go index 3412f893c4912..d1970a94b926b 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 c3dc800c5105d..786136ae6f83a 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 })