diff --git a/.circleci/config.yml b/.circleci/config.yml index 3f21cf2fe2..7b2b291341 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1219,7 +1219,12 @@ jobs: steps: - utils/checkout-with-mise - setup_remote_docker - - run: make -C op-program verify-reproducibility + - run: + name: Verify reproducibility + command: make -C op-program verify-reproducibility + - store_artifacts: + path: ./op-program/temp/logs + when: always - notify-failures-on-develop: mentions: "@proofs-team" @@ -1289,20 +1294,6 @@ jobs: steps: - run: echo Done - fpp-verify: - circleci_ip_ranges: true - docker: - - image: cimg/go:1.21 - steps: - - utils/checkout-with-mise - - run: - name: verify-sepolia - command: | - make verify-sepolia - working_directory: op-program - - notify-failures-on-develop: - mentions: "@proofs-team" - op-program-compat: machine: true resource_class: ethereum-optimism/latitude-1 @@ -1454,23 +1445,28 @@ workflows: name: contracts-bedrock-build # Build with just core + script contracts. build_args: --deny-warnings --skip test + context: circleci-repo-readonly-authenticated-github-token - check-kontrol-build: requires: - contracts-bedrock-build + context: circleci-repo-readonly-authenticated-github-token - contracts-bedrock-tests: # Test everything except PreimageOracle.t.sol since it's slow. name: contracts-bedrock-tests test_list: find test -name "*.t.sol" -not -name "PreimageOracle.t.sol" + context: circleci-repo-readonly-authenticated-github-token - contracts-bedrock-tests: # PreimageOracle test is slow, run it separately to unblock CI. name: contracts-bedrock-tests-preimage-oracle test_list: find test -name "PreimageOracle.t.sol" + context: circleci-repo-readonly-authenticated-github-token - contracts-bedrock-tests: # Heavily fuzz any fuzz tests within added or modified test files. name: contracts-bedrock-tests-heavy-fuzz-modified test_list: git diff origin/develop...HEAD --name-only --diff-filter=AM -- './test/**/*.t.sol' | sed 's|packages/contracts-bedrock/||' test_timeout: 1h test_profile: ciheavy + context: circleci-repo-readonly-authenticated-github-token - contracts-bedrock-coverage: # Generate coverage reports. name: contracts-bedrock-coverage @@ -1479,25 +1475,49 @@ workflows: # need this requires to ensure that all FFI JSONs exist requires: - contracts-bedrock-build + context: circleci-repo-readonly-authenticated-github-token - contracts-bedrock-tests-upgrade: name: contracts-bedrock-tests-upgrade fork_op_chain: op fork_base_chain: mainnet fork_base_rpc: https://ci-mainnet-l1-archive.optimism.io + context: circleci-repo-readonly-authenticated-github-token + - contracts-bedrock-tests-upgrade: + name: contracts-bedrock-tests-upgrade base-mainnet + fork_op_chain: base + fork_base_chain: mainnet + fork_base_rpc: https://ci-mainnet-l1-archive.optimism.io + context: circleci-repo-readonly-authenticated-github-token + - contracts-bedrock-tests-upgrade: + name: contracts-bedrock-tests-upgrade ink-mainnet + fork_op_chain: ink + fork_base_chain: mainnet + fork_base_rpc: https://ci-mainnet-l1-archive.optimism.io + context: circleci-repo-readonly-authenticated-github-token + - contracts-bedrock-tests-upgrade: + name: contracts-bedrock-tests-upgrade unichain-mainnet + fork_op_chain: unichain + fork_base_chain: mainnet + fork_base_rpc: https://ci-mainnet-l1-archive.optimism.io + context: circleci-repo-readonly-authenticated-github-token - contracts-bedrock-checks: requires: - contracts-bedrock-build + context: circleci-repo-readonly-authenticated-github-token - contracts-bedrock-frozen-code: requires: - contracts-bedrock-build - - diff-asterisc-bytecode + context: circleci-repo-readonly-authenticated-github-token + - diff-asterisc-bytecode: + context: circleci-repo-readonly-authenticated-github-token - semgrep-scan: name: semgrep-scan-local scan_command: semgrep scan --timeout=100 --config .semgrep/rules/ --error . - semgrep-scan: name: semgrep-test scan_command: semgrep scan --test --config .semgrep/rules/ .semgrep/tests/ - - go-lint + - go-lint: + context: circleci-repo-readonly-authenticated-github-token - fuzz-golang: name: fuzz-golang-<> on_changes: <> @@ -1508,18 +1528,21 @@ workflows: - op-node - op-service - op-chain-ops + context: circleci-repo-readonly-authenticated-github-token - fuzz-golang: name: cannon-fuzz package_name: cannon on_changes: cannon,packages/contracts-bedrock/src/cannon uses_artifacts: true requires: ["contracts-bedrock-build"] + context: circleci-repo-readonly-authenticated-github-token - fuzz-golang: name: op-e2e-fuzz package_name: op-e2e on_changes: op-e2e,packages/contracts-bedrock/src uses_artifacts: true requires: ["contracts-bedrock-build"] + context: circleci-repo-readonly-authenticated-github-token - go-tests: environment_overrides: | export PARALLEL=24 @@ -1546,10 +1569,15 @@ workflows: packages/contracts-bedrock/scripts/checks packages/contracts-bedrock/scripts/verify op-dripper + devnet-sdk + kurtosis-devnet + op-acceptance-tests requires: - contracts-bedrock-build - cannon-prestate-quick - - op-program-compat + context: circleci-repo-readonly-authenticated-github-token + - op-program-compat: + context: circleci-repo-readonly-authenticated-github-token - bedrock-go-tests: requires: - go-lint @@ -1567,6 +1595,7 @@ workflows: - proofs-tools-docker-build - go-tests - sanitize-op-program + context: circleci-repo-readonly-authenticated-github-tokens - docker-build: name: <>-docker-build docker_tags: <>,<> @@ -1586,12 +1615,17 @@ workflows: - op-supervisor - cannon - op-dripper - - cannon-prestate-quick + context: circleci-repo-readonly-authenticated-github-token + - cannon-prestate-quick: + context: circleci-repo-readonly-authenticated-github-token - sanitize-op-program: requires: - cannon-prestate-quick - - check-generated-mocks-op-node - - check-generated-mocks-op-service + context: circleci-repo-readonly-authenticated-github-token + - check-generated-mocks-op-node: + context: circleci-repo-readonly-authenticated-github-token + - check-generated-mocks-op-service: + context: circleci-repo-readonly-authenticated-github-token - cannon-go-lint-and-test: name: cannon-go-lint-and-test-<>-bit requires: @@ -1601,10 +1635,13 @@ workflows: matrix: parameters: mips_word_size: [32, 64] - - cannon-build-test-vectors + context: circleci-repo-readonly-authenticated-github-token + - cannon-build-test-vectors: + context: circleci-repo-readonly-authenticated-github-token - todo-issues: name: todo-issues-check check_closed: false + context: circleci-repo-readonly-authenticated-github-token - shellcheck/check: name: shell-check # We don't need the `exclude` key as the orb detects the `.shellcheckrc` @@ -1744,15 +1781,7 @@ workflows: name: todo-issue-checks context: - slack - - scheduled-fpp: - when: - equal: [build_hourly, <>] - jobs: - - fpp-verify: - context: - - slack - - oplabs-fpp-nodes + - circleci-repo-readonly-authenticated-github-token develop-publish-contract-artifacts: when: @@ -1761,7 +1790,9 @@ workflows: - equal: [true, <>] jobs: - - publish-contract-artifacts + - publish-contract-artifacts: + context: + - circleci-repo-readonly-authenticated-github-token develop-fault-proofs: when: @@ -1772,14 +1803,18 @@ workflows: - not: equal: [scheduled_pipeline, << pipeline.trigger_source >>] jobs: - - cannon-prestate + - cannon-prestate: + context: + - circleci-repo-readonly-authenticated-github-token - cannon-stf-verify: context: - slack + - circleci-repo-readonly-authenticated-github-token - contracts-bedrock-build: build_args: --deny-warnings --skip test context: - slack + - circleci-repo-readonly-authenticated-github-token - go-tests: name: op-e2e-cannon-tests notify: true @@ -1794,6 +1829,7 @@ workflows: op-e2e/faultproofs context: - slack + - circleci-repo-readonly-authenticated-github-token requires: - contracts-bedrock-build - cannon-prestate @@ -1822,6 +1858,7 @@ workflows: context: - slack - runtimeverification + - circleci-repo-readonly-authenticated-github-token scheduled-cannon-full-tests: when: @@ -1839,6 +1876,7 @@ workflows: notify: true context: - slack + - circleci-repo-readonly-authenticated-github-token matrix: parameters: mips_word_size: [32, 64] @@ -1897,7 +1935,9 @@ workflows: - equal: [true, << pipeline.parameters.reproducibility_dispatch >>] jobs: - preimage-reproducibility: - context: slack + context: + - slack + - circleci-repo-readonly-authenticated-github-token scheduled-stale-check: when: diff --git a/.envrc.example b/.envrc.example deleted file mode 100644 index 43ccf74842..0000000000 --- a/.envrc.example +++ /dev/null @@ -1,68 +0,0 @@ -################################################## -# Getting Started # -################################################## - -# Admin account -export GS_ADMIN_ADDRESS= -export GS_ADMIN_PRIVATE_KEY= - -# Batcher account -export GS_BATCHER_ADDRESS= -export GS_BATCHER_PRIVATE_KEY= - -# Proposer account -export GS_PROPOSER_ADDRESS= -export GS_PROPOSER_PRIVATE_KEY= - -# Sequencer account -export GS_SEQUENCER_ADDRESS= -export GS_SEQUENCER_PRIVATE_KEY= - - -################################################## -# Chain Information # -################################################## - -# L1 chain information -export L1_CHAIN_ID=11155111 -export L1_BLOCK_TIME=12 - -# L2 chain information -export L2_CHAIN_ID=42069 -export L2_BLOCK_TIME=2 - -################################################## -# op-node Configuration # -################################################## - -# The kind of RPC provider, used to inform optimal transactions receipts -# fetching. Valid options: alchemy, quicknode, infura, parity, nethermind, -# debug_geth, erigon, basic, any. -export L1_RPC_KIND= - -################################################## -# Contract Deployment # -################################################## - -# RPC URL for the L1 network to interact with -export L1_RPC_URL= - -# Salt used via CREATE2 to determine implementation addresses -# NOTE: If you want to deploy contracts from scratch you MUST reload this -# variable to ensure the salt is regenerated and the contracts are -# deployed to new addresses (otherwise deployment will fail) -export IMPL_SALT=$(openssl rand -hex 32) - -# Name for the deployed network -export DEPLOYMENT_CONTEXT=getting-started - -# Optional Tenderly details for simulation link during deployment -export TENDERLY_PROJECT= -export TENDERLY_USERNAME= - -# Optional Etherscan API key for contract verification -export ETHERSCAN_API_KEY= - -# Private key to use for contract deployments, you don't need to worry about -# this for the Getting Started guide. -export PRIVATE_KEY= diff --git a/.semgrep/rules/sol-rules.yaml b/.semgrep/rules/sol-rules.yaml index 2e97715f69..7b3a26317b 100644 --- a/.semgrep/rules/sol-rules.yaml +++ b/.semgrep/rules/sol-rules.yaml @@ -200,7 +200,6 @@ rules: paths: exclude: - packages/contracts-bedrock/src/L1/OPContractsManager.sol - - packages/contracts-bedrock/src/L1/OPContractsManagerInterop.sol - packages/contracts-bedrock/src/legacy/L1ChugSplashProxy.sol - id: sol-style-enforce-require-msg @@ -241,10 +240,6 @@ rules: _disableInitializers(); ... } - paths: - exclude: - - packages/contracts-bedrock/src/L1/SystemConfigInterop.sol - - packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol - id: sol-safety-proper-initializer languages: [solidity] @@ -273,9 +268,6 @@ rules: function initialize(...) public reinitializer(...) { ... } - paths: - exclude: - - packages/contracts-bedrock/src/L1/SystemConfigInterop.sol - id: sol-safety-proper-upgrade-function languages: [solidity] diff --git a/cannon/cmd/load_elf.go b/cannon/cmd/load_elf.go index d28455479c..a5ee43d139 100644 --- a/cannon/cmd/load_elf.go +++ b/cannon/cmd/load_elf.go @@ -20,7 +20,7 @@ import ( var ( LoadELFVMTypeFlag = &cli.StringFlag{ Name: "type", - Usage: "VM type to create state for. Valid options: " + openum.EnumString(stateVersions()), + Usage: "VM type to create state for. Valid options: " + openum.EnumString(versions.GetStateVersionStrings()), Required: true, } LoadELFPathFlag = &cli.PathFlag{ @@ -43,14 +43,6 @@ var ( } ) -func stateVersions() []string { - vers := make([]string, len(versions.StateVersionTypes)) - for i, v := range versions.StateVersionTypes { - vers[i] = v.String() - } - return vers -} - func LoadELF(ctx *cli.Context) error { elfPath := ctx.Path(LoadELFPathFlag.Name) elfProgram, err := elf.Open(elfPath) @@ -69,7 +61,7 @@ func LoadELF(ctx *cli.Context) error { return err } switch ver { - case versions.VersionSingleThreaded2: + case versions.GetCurrentSingleThreaded(): createInitialState = func(f *elf.File) (mipsevm.FPVMState, error) { return program.LoadELF(f, singlethreaded.CreateInitialState) } @@ -80,7 +72,7 @@ func LoadELF(ctx *cli.Context) error { } return program.PatchStack(state) } - case versions.VersionMultiThreaded_v2, versions.VersionMultiThreaded64_v3: + case versions.GetCurrentMultiThreaded(), versions.GetCurrentMultiThreaded64(): createInitialState = func(f *elf.File) (mipsevm.FPVMState, error) { return program.LoadELF(f, multithreaded.CreateInitialState) } diff --git a/cannon/mipsevm/tests/evm_common64_test.go b/cannon/mipsevm/tests/evm_common64_test.go index 5c8d9b449e..de994c6ff0 100644 --- a/cannon/mipsevm/tests/evm_common64_test.go +++ b/cannon/mipsevm/tests/evm_common64_test.go @@ -86,18 +86,13 @@ func TestEVM_SingleStep_Operators64(t *testing.T) { testOperators(t, cases, false) } +// Additional 64-bit tests func TestEVM_SingleStep_Bitwise64(t *testing.T) { cases := []operatorTestCase{ - {name: "and", funct: 0x24, isImm: false, rs: Word(1200), rt: Word(490), expectRes: Word(160)}, // and t0, s1, s2 - {name: "andi", opcode: 0xc, isImm: true, rs: Word(4), rt: Word(1), imm: uint16(40), expectRes: Word(0)}, // andi t0, s1, 40 - {name: "or", funct: 0x25, isImm: false, rs: Word(1200), rt: Word(490), expectRes: Word(1530)}, // or t0, s1, s2 - {name: "ori", opcode: 0xd, isImm: true, rs: Word(4), rt: Word(1), imm: uint16(40), expectRes: Word(44)}, // ori t0, s1, 40 - {name: "xor", funct: 0x26, isImm: false, rs: Word(1200), rt: Word(490), expectRes: Word(1370)}, // xor t0, s1, s2 - {name: "xori", opcode: 0xe, isImm: true, rs: Word(4), rt: Word(1), imm: uint16(40), expectRes: Word(44)}, // xori t0, s1, 40 - {name: "nor", funct: 0x27, isImm: false, rs: Word(0x4b0), rt: Word(0x1ea), expectRes: Word(0xFF_FF_FF_FF_FF_FF_FA_05)}, // nor t0, s1, s2 - {name: "slt", funct: 0x2a, isImm: false, rs: 0xFF_FF_FF_FE, rt: Word(5), expectRes: Word(0)}, // slt t0, s1, s2 - {name: "slt", funct: 0x2a, isImm: false, rs: 0xFF_FF_FF_FF_FF_FF_FF_FE, rt: Word(5), expectRes: Word(1)}, // slt t0, s1, s2 - {name: "sltu", funct: 0x2b, isImm: false, rs: Word(1200), rt: Word(490), expectRes: Word(0)}, // sltu t0, s1, s2 + {name: "slt", funct: 0x2a, isImm: false, rs: 0xFF_FF_FF_FE, rt: Word(5), expectRes: Word(0)}, // slt t0, s1, s2 + {name: "slt", funct: 0x2a, isImm: false, rs: 0xFF_FF_FF_FF_FF_FF_FF_FE, rt: Word(5), expectRes: Word(1)}, // slt t0, s1, s2 + {name: "slti", opcode: 0xa, isImm: true, rs: 0xFF_FF_FF_FE, imm: 5, expectRes: Word(0)}, // slt t0, s1, s2 + {name: "slti", opcode: 0xa, isImm: true, rs: 0xFF_FF_FF_FF_FF_FF_FF_FE, imm: 5, expectRes: Word(1)}, // slt t0, s1, s2 } testOperators(t, cases, false) } diff --git a/cannon/mipsevm/tests/evm_common_test.go b/cannon/mipsevm/tests/evm_common_test.go index c43fb19e23..a8a06f9acc 100644 --- a/cannon/mipsevm/tests/evm_common_test.go +++ b/cannon/mipsevm/tests/evm_common_test.go @@ -186,19 +186,36 @@ func TestEVM_SingleStep_Operators(t *testing.T) { testOperators(t, cases, true) } -func TestEVM_SingleStep_Bitwise32(t *testing.T) { - testutil.Cannon32OnlyTest(t, "These tests are fully covered for 64-bits in TestEVM_SingleStep_Bitwise64") +func TestEVM_SingleStep_Bitwise(t *testing.T) { // bitwise operations that use the full word size cases := []operatorTestCase{ - {name: "and", funct: 0x24, isImm: false, rs: Word(1200), rt: Word(490), expectRes: Word(160)}, // and t0, s1, s2 - {name: "andi", opcode: 0xc, isImm: true, rs: Word(4), rt: Word(1), imm: uint16(40), expectRes: Word(0)}, // andi t0, s1, 40 - {name: "or", funct: 0x25, isImm: false, rs: Word(1200), rt: Word(490), expectRes: Word(1530)}, // or t0, s1, s2 - {name: "ori", opcode: 0xd, isImm: true, rs: Word(4), rt: Word(1), imm: uint16(40), expectRes: Word(44)}, // ori t0, s1, 40 - {name: "xor", funct: 0x26, isImm: false, rs: Word(1200), rt: Word(490), expectRes: Word(1370)}, // xor t0, s1, s2 - {name: "xori", opcode: 0xe, isImm: true, rs: Word(4), rt: Word(1), imm: uint16(40), expectRes: Word(44)}, // xori t0, s1, 40 - {name: "nor", funct: 0x27, isImm: false, rs: Word(1200), rt: Word(490), expectRes: Word(4294965765)}, // nor t0, s1, s2 - {name: "slt", funct: 0x2a, isImm: false, rs: 0xFF_FF_FF_FE, rt: Word(5), expectRes: Word(1)}, // slt t0, s1, s2 - {name: "sltu", funct: 0x2b, isImm: false, rs: Word(1200), rt: Word(490), expectRes: Word(0)}, // sltu t0, s1, s2 + {name: "and", funct: 0x24, isImm: false, rs: Word(0b1010_1100), rt: Word(0b1100_0101), expectRes: Word(0b1000_0100)}, // and t0, s1, s2 + {name: "andi", opcode: 0xc, isImm: true, rs: Word(0b1010_1100), rt: Word(1), imm: uint16(0b1100_0101), expectRes: Word(0b1000_0100)}, // andi t0, s1, imm + {name: "or", funct: 0x25, isImm: false, rs: Word(0b1010_1100), rt: Word(0b1100_0101), expectRes: Word(0b1110_1101)}, // or t0, s1, s2 + {name: "ori", opcode: 0xd, isImm: true, rs: Word(0b1010_1100), rt: Word(0xFFFF_FFFF), imm: uint16(0b1100_0101), expectRes: Word(0b1110_1101)}, // ori t0, s1, imm + {name: "xor", funct: 0x26, isImm: false, rs: Word(0b1010_1100), rt: Word(0b1100_0101), expectRes: Word(0b0110_1001)}, // xor t0, s1, s2 + {name: "xori", opcode: 0xe, isImm: true, rs: Word(0b1010_1100), rt: Word(1), imm: uint16(0b1100_0101), expectRes: Word(0b0110_1001)}, // xori t0, s1, imm + {name: "nor", funct: 0x27, isImm: false, rs: Word(0b1010_1100), rt: Word(0b1100_0101), expectRes: signExtend64(0b0001_0010 | 0xFFFF_FF00)}, // nor t0, s1, s2 + {name: "slt, success, positive vals", funct: 0x2a, isImm: false, rs: 1, rt: Word(5), expectRes: Word(1)}, // slt t0, s1, s2 + {name: "slt, success, mixed vals", funct: 0x2a, isImm: false, rs: signExtend64(0xFF_FF_FF_FE), rt: Word(5), expectRes: Word(1)}, // slt t0, s1, s2 + {name: "slt, success, negative vals", funct: 0x2a, isImm: false, rs: signExtend64(0xFF_FF_FF_FD), rt: signExtend64(0xFF_FF_FF_FE), expectRes: Word(1)}, // slt t0, s1, s2 + {name: "slt, fail, negative values", funct: 0x2a, isImm: false, rs: signExtend64(0xFF_FF_FF_FE), rt: signExtend64(0xFF_FF_FF_FD), expectRes: Word(0)}, // slt t0, s1, s2 + {name: "slt, fail, positive values", funct: 0x2a, isImm: false, rs: 555, rt: 123, expectRes: Word(0)}, // slt t0, s1, s2 + {name: "slt, fail, mixed values", funct: 0x2a, isImm: false, rs: 555, rt: signExtend64(0xFF_FF_FF_FD), expectRes: Word(0)}, // slt t0, s1, s2 + {name: "slti, success, positive vals", opcode: 0xa, isImm: true, rs: 1, imm: 5, expectRes: Word(1)}, + {name: "slti, success, mixed vals", opcode: 0xa, isImm: true, rs: signExtend64(0xFF_FF_FF_FE), imm: 5, expectRes: Word(1)}, + {name: "slti, success, negative vals", opcode: 0xa, isImm: true, rs: signExtend64(0xFF_FF_FF_FD), imm: 0xFFFE, expectRes: Word(1)}, + {name: "slti, fail, negative values", opcode: 0xa, isImm: true, rs: signExtend64(0xFF_FF_FF_FE), imm: 0xFFFD, expectRes: Word(0)}, + {name: "slti, fail, positive values", opcode: 0xa, isImm: true, rs: 555, imm: 123, expectRes: Word(0)}, + {name: "slti, fail, mixed values", opcode: 0xa, isImm: true, rs: 555, imm: 0xFFFD, expectRes: Word(0)}, + {name: "sltu, success", funct: 0x2b, isImm: false, rs: Word(490), rt: Word(1200), expectRes: Word(1)}, // sltu t0, s1, s2 + {name: "sltu, success, large values", funct: 0x2b, isImm: false, rs: signExtend64(0xFF_FF_FF_FD), rt: signExtend64(0xFF_FF_FF_FE), expectRes: Word(1)}, // sltu t0, s1, s2 + {name: "sltu, fail", funct: 0x2b, isImm: false, rs: Word(1200), rt: Word(490), expectRes: Word(0)}, // sltu t0, s1, s2 + {name: "sltu, fail, large values", funct: 0x2b, isImm: false, rs: signExtend64(0xFF_FF_FF_FE), rt: signExtend64(0xFF_FF_FF_FD), expectRes: Word(0)}, // sltu t0, s1, s2 + {name: "sltiu, success", opcode: 0xb, isImm: true, rs: Word(490), imm: 1200, expectRes: Word(1)}, + {name: "sltiu, success, large values", opcode: 0xb, isImm: true, rs: signExtend64(0xFF_FF_FF_FD), imm: 0xFFFE, expectRes: Word(1)}, + {name: "sltiu, fail", opcode: 0xb, isImm: true, rs: Word(1200), imm: 490, expectRes: Word(0)}, + {name: "sltiu, fail, large values", opcode: 0xb, isImm: true, rs: signExtend64(0xFF_FF_FF_FE), imm: 0xFFFD, expectRes: Word(0)}, } testOperators(t, cases, false) } @@ -338,11 +355,17 @@ func TestEVM_SingleStep_CloClz(t *testing.T) { func TestEVM_SingleStep_MovzMovn(t *testing.T) { versions := GetMipsVersionTestCases(t) cases := []struct { - name string - funct uint32 + name string + funct uint32 + testValue Word + shouldSucceed bool }{ - {name: "movz", funct: uint32(0xa)}, - {name: "movn", funct: uint32(0xb)}, + {name: "movz, success", funct: uint32(0xa), testValue: 0, shouldSucceed: true}, + {name: "movz, failure, testVal=1", funct: uint32(0xa), testValue: 1, shouldSucceed: false}, + {name: "movz, failure, testVal=2", funct: uint32(0xa), testValue: 2, shouldSucceed: false}, + {name: "movn, success, testVal=1", funct: uint32(0xb), testValue: 1, shouldSucceed: true}, + {name: "movn, success, testVal=2", funct: uint32(0xb), testValue: 2, shouldSucceed: true}, + {name: "movn, failure", funct: uint32(0xb), testValue: 0, shouldSucceed: false}, } for _, v := range versions { for i, tt := range cases { @@ -354,39 +377,21 @@ func TestEVM_SingleStep_MovzMovn(t *testing.T) { rtReg := uint32(10) rdReg := uint32(8) insn := rsReg<<21 | rtReg<<16 | rdReg<<11 | tt.funct - var t2 Word - if tt.funct == 0xa { - t2 = 0x0 - } else { - t2 = 0x1 - } - state.GetRegistersRef()[rtReg] = t2 + + state.GetRegistersRef()[rtReg] = tt.testValue state.GetRegistersRef()[rsReg] = Word(0xb) state.GetRegistersRef()[rdReg] = Word(0xa) testutil.StoreInstruction(state.GetMemory(), 0, insn) step := state.GetStep() + // Setup expectations expected := testutil.NewExpectedState(state) expected.ExpectStep() - expected.Registers[rdReg] = state.GetRegistersRef()[rsReg] - - stepWitness, err := goVm.Step(true) - require.NoError(t, err) - // Check expectations - expected.Validate(t, state) - testutil.ValidateEVM(t, stepWitness, step, goVm, v.StateHashFn, v.Contracts) - - if tt.funct == 0xa { - t2 = 0x1 - } else { - t2 = 0x0 + if tt.shouldSucceed { + expected.Registers[rdReg] = state.GetRegistersRef()[rsReg] } - state.GetRegistersRef()[rtReg] = t2 - expected.ExpectStep() - expected.Registers[rtReg] = t2 - expected.Registers[rdReg] = state.GetRegistersRef()[rdReg] - stepWitness, err = goVm.Step(true) + stepWitness, err := goVm.Step(true) require.NoError(t, err) // Check expectations expected.Validate(t, state) @@ -510,35 +515,47 @@ func TestEVM_SingleStep_MthiMtlo(t *testing.T) { } func TestEVM_SingleStep_BeqBne(t *testing.T) { + initialPC := Word(800) + negative := func(value Word) uint16 { + flipped := testutil.FlipSign(value) + return uint16(flipped) + } versions := GetMipsVersionTestCases(t) cases := []struct { - name string - imm uint32 - opcode uint32 - rs Word - rt Word + name string + imm uint16 + opcode uint32 + rs Word + rt Word + expectedNextPC Word }{ - {name: "bne", opcode: uint32(0x5), imm: uint32(0x10), rs: Word(0xaa), rt: Word(0xdeadbeef)}, // bne $t0, $t1, 16 - {name: "beq", opcode: uint32(0x4), imm: uint32(0x10), rs: Word(0xdeadbeef), rt: Word(0xdeadbeef)}, // beq $t0, $t1, 16 + // on success, expectedNextPC should be: (imm * 4) + pc + 4 + {name: "bne, success", opcode: uint32(0x5), imm: 10, rs: Word(0x123), rt: Word(0x456), expectedNextPC: 844}, // bne $t0, $t1, 16 + {name: "bne, success, signed-extended offset", opcode: uint32(0x5), imm: negative(3), rs: Word(0x123), rt: Word(0x456), expectedNextPC: 792}, // bne $t0, $t1, 16 + {name: "bne, fail", opcode: uint32(0x5), imm: 10, rs: Word(0x123), rt: Word(0x123), expectedNextPC: 808}, // bne $t0, $t1, 16 + {name: "beq, success", opcode: uint32(0x4), imm: 10, rs: Word(0x123), rt: Word(0x123), expectedNextPC: 844}, // beq $t0, $t1, 16 + {name: "beq, success, sign-extended offset", opcode: uint32(0x4), imm: negative(25), rs: Word(0x123), rt: Word(0x123), expectedNextPC: 704}, // beq $t0, $t1, 16 + {name: "beq, fail", opcode: uint32(0x4), imm: 10, rs: Word(0x123), rt: Word(0x456), expectedNextPC: 808}, // beq $t0, $t1, 16 } for _, v := range versions { for i, tt := range cases { testName := fmt.Sprintf("%v (%v)", tt.name, v.Name) t.Run(testName, func(t *testing.T) { - goVm := v.VMFactory(nil, os.Stdout, os.Stderr, testutil.CreateLogger(), testutil.WithRandomization(int64(i)), testutil.WithPC(0), testutil.WithNextPC(4)) + goVm := v.VMFactory(nil, os.Stdout, os.Stderr, testutil.CreateLogger(), testutil.WithRandomization(int64(i)), testutil.WithPCAndNextPC(initialPC)) state := goVm.GetState() rsReg := uint32(9) rtReg := uint32(8) - insn := tt.opcode<<26 | rsReg<<21 | rtReg<<16 | tt.imm + insn := tt.opcode<<26 | rsReg<<21 | rtReg<<16 | uint32(tt.imm) state.GetRegistersRef()[rtReg] = tt.rt state.GetRegistersRef()[rsReg] = tt.rs - testutil.StoreInstruction(state.GetMemory(), 0, insn) + testutil.StoreInstruction(state.GetMemory(), initialPC, insn) step := state.GetStep() + // Setup expectations expected := testutil.NewExpectedState(state) expected.Step = state.GetStep() + 1 expected.PC = state.GetCpu().NextPC - expected.NextPC = state.GetCpu().NextPC + Word(tt.imm<<2) + expected.NextPC = tt.expectedNextPC stepWitness, err := goVm.Step(true) require.NoError(t, err) @@ -561,22 +578,23 @@ func TestEVM_SingleStep_SlSr(t *testing.T) { funct uint16 expectVal Word }{ - {name: "sll", funct: uint16(4) << 6, rt: Word(0x20), rsReg: uint32(0x0), expectVal: Word(0x20) << uint8(4)}, // sll t0, t1, 3 + {name: "sll", funct: uint16(4) << 6, rt: Word(0x20), rsReg: uint32(0x0), expectVal: Word(0x200)}, // sll t0, t1, 3 {name: "sll with overflow", funct: uint16(1) << 6, rt: Word(0x8000_0000), rsReg: uint32(0x0), expectVal: 0x0}, {name: "sll with sign extension", funct: uint16(4) << 6, rt: Word(0x0800_0000), rsReg: uint32(0x0), expectVal: signExtend64(0x8000_0000)}, {name: "sll with max shift, sign extension", funct: uint16(31) << 6, rt: Word(0x01), rsReg: uint32(0x0), expectVal: signExtend64(0x8000_0000)}, {name: "sll with max shift, overflow", funct: uint16(31) << 6, rt: Word(0x02), rsReg: uint32(0x0), expectVal: 0x0}, - {name: "srl", funct: uint16(4)<<6 | 2, rt: Word(0x20), rsReg: uint32(0x0), expectVal: Word(0x20) >> uint8(4)}, // srl t0, t1, 3 + {name: "srl", funct: uint16(4)<<6 | 2, rt: Word(0x20), rsReg: uint32(0x0), expectVal: Word(0x2)}, // srl t0, t1, 3 {name: "srl with sign extension", funct: uint16(0)<<6 | 2, rt: Word(0x8000_0000), rsReg: uint32(0x0), expectVal: signExtend64(0x8000_0000)}, // srl t0, t1, 3 {name: "sra", funct: uint16(4)<<6 | 3, rt: Word(0x70_00_00_20), rsReg: uint32(0x0), expectVal: signExtend64(0x07_00_00_02)}, // sra t0, t1, 3 {name: "sra with sign extension", funct: uint16(4)<<6 | 3, rt: Word(0x80_00_00_20), rsReg: uint32(0x0), expectVal: signExtend64(0xF8_00_00_02)}, // sra t0, t1, 3 - {name: "sllv", funct: uint16(4), rt: Word(0x20), rs: Word(4), rsReg: uint32(0xa), expectVal: Word(0x20) << Word(4)}, // sllv t0, t1, t2 + {name: "sllv", funct: uint16(4), rt: Word(0x20), rs: Word(4), rsReg: uint32(0xa), expectVal: Word(0x200)}, // sllv t0, t1, t2 {name: "sllv with overflow", funct: uint16(4), rt: Word(0x8000_0000), rs: Word(1), rsReg: uint32(0xa), expectVal: 0x0}, {name: "sllv with sign extension", funct: uint16(4), rt: Word(0x0800_0000), rs: Word(4), rsReg: uint32(0xa), expectVal: signExtend64(0x8000_0000)}, {name: "sllv with max shift, sign extension", funct: uint16(4), rt: Word(0x01), rs: Word(31), rsReg: uint32(0xa), expectVal: signExtend64(0x8000_0000)}, {name: "sllv with max shift, overflow", funct: uint16(4), rt: Word(0x02), rs: Word(31), rsReg: uint32(0xa), expectVal: 0x0}, - {name: "srlv", funct: uint16(6), rt: Word(0x20_00), rs: Word(4), rsReg: uint32(0xa), expectVal: Word(0x20_00) >> Word(4)}, // srlv t0, t1, t2 + {name: "srlv", funct: uint16(6), rt: Word(0x20_00), rs: Word(4), rsReg: uint32(0xa), expectVal: Word(0x02_00)}, // srlv t0, t1, t2 {name: "srlv with sign extension", funct: uint16(6), rt: Word(0x8000_0000), rs: Word(0), rsReg: uint32(0xa), expectVal: signExtend64(0x8000_0000)}, // srlv t0, t1, t2 + {name: "srlv with zero extension", funct: uint16(6), rt: Word(0x8000_0000), rs: Word(1), rsReg: uint32(0xa), expectVal: 0x4000_0000}, // srlv t0, t1, t2 {name: "srav", funct: uint16(7), rt: Word(0x1deafbee), rs: Word(12), rsReg: uint32(0xa), expectVal: signExtend64(Word(0x0001deaf))}, // srav t0, t1, t2 {name: "srav with sign extension", funct: uint16(7), rt: Word(0xdeafbeef), rs: Word(12), rsReg: uint32(0xa), expectVal: signExtend64(Word(0xfffdeafb))}, // srav t0, t1, t2 } @@ -618,42 +636,80 @@ func TestEVM_SingleStep_JrJalr(t *testing.T) { cases := []struct { name string funct uint16 + rsReg uint32 + jumpTo Word rdReg uint32 + pc Word + nextPC Word expectLink bool + errorMsg string }{ - {name: "jr", funct: uint16(0x8), rdReg: uint32(0)}, // jr t0 - {name: "jalr", funct: uint16(0x9), rdReg: uint32(0x9), expectLink: true}, // jalr t1, t0 + {name: "jr", funct: uint16(0x8), rsReg: 8, jumpTo: 0x34, pc: 0, nextPC: 4}, // jr t0 + {name: "jr, delay slot", funct: uint16(0x8), rsReg: 8, jumpTo: 0x34, pc: 0, nextPC: 8, errorMsg: "jump in delay slot"}, // jr t0 + {name: "jalr", funct: uint16(0x9), rsReg: 8, jumpTo: 0x34, rdReg: uint32(0x9), expectLink: true, pc: 0, nextPC: 4}, // jalr t1, t0 + {name: "jalr, delay slot", funct: uint16(0x9), rsReg: 8, jumpTo: 0x34, rdReg: uint32(0x9), expectLink: true, pc: 0, nextPC: 100, errorMsg: "jump in delay slot"}, // jalr t1, t0 } for _, v := range versions { for i, tt := range cases { testName := fmt.Sprintf("%v (%v)", tt.name, v.Name) t.Run(testName, func(t *testing.T) { - goVm := v.VMFactory(nil, os.Stdout, os.Stderr, testutil.CreateLogger(), testutil.WithRandomization(int64(i)), testutil.WithPC(0), testutil.WithNextPC(4)) + goVm := v.VMFactory(nil, os.Stdout, os.Stderr, testutil.CreateLogger(), testutil.WithRandomization(int64(i)), testutil.WithPC(tt.pc), testutil.WithNextPC(tt.nextPC)) state := goVm.GetState() - rsReg := uint32(8) - insn := rsReg<<21 | tt.rdReg<<11 | uint32(tt.funct) - state.GetRegistersRef()[rsReg] = Word(0x34) + insn := tt.rsReg<<21 | tt.rdReg<<11 | uint32(tt.funct) + state.GetRegistersRef()[tt.rsReg] = tt.jumpTo testutil.StoreInstruction(state.GetMemory(), 0, insn) step := state.GetStep() - // Setup expectations - expected := testutil.NewExpectedState(state) - expected.Step = state.GetStep() + 1 - expected.PC = state.GetCpu().NextPC - expected.NextPC = state.GetRegistersRef()[rsReg] - if tt.expectLink { - expected.Registers[tt.rdReg] = state.GetPC() + 8 - } - stepWitness, err := goVm.Step(true) - require.NoError(t, err) - // Check expectations - expected.Validate(t, state) - testutil.ValidateEVM(t, stepWitness, step, goVm, v.StateHashFn, v.Contracts) + if tt.errorMsg != "" { + proofData := v.ProofGenerator(t, goVm.GetState()) + errorMatcher := testutil.CreateErrorStringMatcher(tt.errorMsg) + require.Panics(t, func() { _, _ = goVm.Step(false) }) + testutil.AssertEVMReverts(t, state, v.Contracts, nil, proofData, errorMatcher) + } else { + // Setup expectations + expected := testutil.NewExpectedState(state) + expected.Step = state.GetStep() + 1 + expected.PC = state.GetCpu().NextPC + expected.NextPC = tt.jumpTo + if tt.expectLink { + expected.Registers[tt.rdReg] = state.GetPC() + 8 + } + + stepWitness, err := goVm.Step(true) + require.NoError(t, err) + // Check expectations + expected.Validate(t, state) + testutil.ValidateEVM(t, stepWitness, step, goVm, v.StateHashFn, v.Contracts) + } }) } } } +func TestEVM_SingleStep_Sync(t *testing.T) { + versions := GetMipsVersionTestCases(t) + syncInsn := uint32(0x0000_000F) + for _, v := range versions { + testName := fmt.Sprintf("Sync (%v)", v.Name) + t.Run(testName, func(t *testing.T) { + goVm := v.VMFactory(nil, os.Stdout, os.Stderr, testutil.CreateLogger(), testutil.WithRandomization(int64(248))) + state := goVm.GetState() + testutil.StoreInstruction(state.GetMemory(), state.GetPC(), syncInsn) + step := state.GetStep() + + // Setup expectations + expected := testutil.NewExpectedState(state) + expected.ExpectStep() + + stepWitness, err := goVm.Step(true) + require.NoError(t, err) + // Check expectations + expected.Validate(t, state) + testutil.ValidateEVM(t, stepWitness, step, goVm, v.StateHashFn, v.Contracts) + }) + } +} + func TestEVM_MMap(t *testing.T) { versions := GetMipsVersionTestCases(t) cases := []struct { diff --git a/cannon/mipsevm/versions/detect.go b/cannon/mipsevm/versions/detect.go index c667ade282..3e8d4e317d 100644 --- a/cannon/mipsevm/versions/detect.go +++ b/cannon/mipsevm/versions/detect.go @@ -26,10 +26,8 @@ func DetectVersion(path string) (StateVersion, error) { return 0, err } - switch ver { - case VersionSingleThreaded, VersionMultiThreaded, VersionSingleThreaded2, VersionMultiThreaded64, VersionMultiThreaded64_v2, VersionMultiThreaded_v2, VersionMultiThreaded64_v3: - return ver, nil - default: + if !IsValidStateVersion(ver) { return 0, fmt.Errorf("%w: %d", ErrUnknownVersion, ver) } + return ver, nil } diff --git a/cannon/mipsevm/versions/detect_test.go b/cannon/mipsevm/versions/detect_test.go index f0d9e19e97..efdc650e9f 100644 --- a/cannon/mipsevm/versions/detect_test.go +++ b/cannon/mipsevm/versions/detect_test.go @@ -50,7 +50,7 @@ func TestDetectVersion_fromFile(t *testing.T) { // Check that the latest supported versions write new states in a way that is detected correctly func TestDetectVersion_singleThreadedBinary(t *testing.T) { - targetVersion := VersionSingleThreaded2 + targetVersion := GetCurrentSingleThreaded() if !arch.IsMips32 { t.Skip("Single-threaded states are not supported for 64-bit VMs") } @@ -64,9 +64,9 @@ func TestDetectVersion_singleThreadedBinary(t *testing.T) { } func TestDetectVersion_multiThreadedBinary(t *testing.T) { - targetVersion := VersionMultiThreaded_v2 + targetVersion := GetCurrentMultiThreaded() if !arch.IsMips32 { - targetVersion = VersionMultiThreaded64_v3 + targetVersion = GetCurrentMultiThreaded64() } state, err := NewFromState(multithreaded.CreateEmptyState()) diff --git a/cannon/mipsevm/versions/state.go b/cannon/mipsevm/versions/state.go index 1e6c17ed14..bba831bfc3 100644 --- a/cannon/mipsevm/versions/state.go +++ b/cannon/mipsevm/versions/state.go @@ -14,34 +14,12 @@ import ( "github.com/ethereum-optimism/optimism/op-service/serialize" ) -type StateVersion uint8 - -const ( - // VersionSingleThreaded is the version of the Cannon STF found in op-contracts/v1.6.0 - https://github.com/ethereum-optimism/optimism/blob/op-contracts/v1.6.0/packages/contracts-bedrock/src/cannon/MIPS.sol - VersionSingleThreaded StateVersion = iota - // VersionMultiThreaded is the original implementation of 32-bit multithreaded cannon, tagged at cannon/v1.3.0 - VersionMultiThreaded - // VersionSingleThreaded2 is based on VersionSingleThreaded with the addition of support for fcntl(F_GETFD) syscall - // This is the latest 32-bit single-threaded vm - VersionSingleThreaded2 - // VersionMultiThreaded64 is the original 64-bit MTCannon implementation (pre-audit), tagged at cannon/v1.2.0 - VersionMultiThreaded64 - // VersionMultiThreaded64_v2 includes an audit fix to ensure futex values are always 32-bit, tagged at cannon/v1.3.0 - VersionMultiThreaded64_v2 - // VersionMultiThreaded_v2 is the latest 32-bit multithreaded vm - VersionMultiThreaded_v2 - // VersionMultiThreaded64_v3 is the latest 64-bit multithreaded vm - VersionMultiThreaded64_v3 -) - var ( ErrUnknownVersion = errors.New("unknown version") ErrJsonNotSupported = errors.New("json not supported") ErrUnsupportedMipsArch = errors.New("mips architecture is not supported") ) -var StateVersionTypes = []StateVersion{VersionSingleThreaded, VersionMultiThreaded, VersionSingleThreaded2, VersionMultiThreaded64, VersionMultiThreaded64_v2, VersionMultiThreaded_v2, VersionMultiThreaded64_v3} - func LoadStateFromFile(path string) (*VersionedState, error) { if !serialize.IsBinaryFile(path) { // Always use singlethreaded for JSON states @@ -61,18 +39,18 @@ func NewFromState(state mipsevm.FPVMState) (*VersionedState, error) { return nil, ErrUnsupportedMipsArch } return &VersionedState{ - Version: VersionSingleThreaded2, + Version: GetCurrentSingleThreaded(), FPVMState: state, }, nil case *multithreaded.State: if arch.IsMips32 { return &VersionedState{ - Version: VersionMultiThreaded_v2, + Version: GetCurrentMultiThreaded(), FPVMState: state, }, nil } else { return &VersionedState{ - Version: VersionMultiThreaded64_v3, + Version: GetCurrentMultiThreaded64(), FPVMState: state, }, nil } @@ -103,7 +81,7 @@ func (s *VersionedState) Deserialize(in io.Reader) error { } switch s.Version { - case VersionSingleThreaded2: + case GetCurrentSingleThreaded(): if !arch.IsMips32 { return ErrUnsupportedMipsArch } @@ -113,7 +91,7 @@ func (s *VersionedState) Deserialize(in io.Reader) error { } s.FPVMState = state return nil - case VersionMultiThreaded_v2: + case GetCurrentMultiThreaded(): if !arch.IsMips32 { return ErrUnsupportedMipsArch } @@ -123,7 +101,7 @@ func (s *VersionedState) Deserialize(in io.Reader) error { } s.FPVMState = state return nil - case VersionMultiThreaded64_v3: + case GetCurrentMultiThreaded64(): if arch.IsMips32 { return ErrUnsupportedMipsArch } @@ -149,45 +127,3 @@ func (s *VersionedState) MarshalJSON() ([]byte, error) { } return json.Marshal(s.FPVMState) } - -func (s StateVersion) String() string { - switch s { - case VersionSingleThreaded: - return "singlethreaded" - case VersionMultiThreaded: - return "multithreaded" - case VersionSingleThreaded2: - return "singlethreaded-2" - case VersionMultiThreaded64: - return "multithreaded64" - case VersionMultiThreaded64_v2: - return "multithreaded64-2" - case VersionMultiThreaded_v2: - return "multithreaded-2" - case VersionMultiThreaded64_v3: - return "multithreaded64-3" - default: - return "unknown" - } -} - -func ParseStateVersion(ver string) (StateVersion, error) { - switch ver { - case "singlethreaded": - return VersionSingleThreaded, nil - case "multithreaded": - return VersionMultiThreaded, nil - case "singlethreaded-2": - return VersionSingleThreaded2, nil - case "multithreaded64": - return VersionMultiThreaded64, nil - case "multithreaded64-2": - return VersionMultiThreaded64_v2, nil - case "multithreaded-2": - return VersionMultiThreaded_v2, nil - case "multithreaded64-3": - return VersionMultiThreaded64_v3, nil - default: - return StateVersion(0), errors.New("unknown state version") - } -} diff --git a/cannon/mipsevm/versions/state64_test.go b/cannon/mipsevm/versions/state64_test.go index d86bee3e81..b8f74d1db0 100644 --- a/cannon/mipsevm/versions/state64_test.go +++ b/cannon/mipsevm/versions/state64_test.go @@ -19,7 +19,7 @@ func TestNewFromState(t *testing.T) { actual, err := NewFromState(multithreaded.CreateEmptyState()) require.NoError(t, err) require.IsType(t, &multithreaded.State{}, actual.FPVMState) - require.Equal(t, VersionMultiThreaded64_v3, actual.Version) + require.Equal(t, GetCurrentMultiThreaded64(), actual.Version) }) } @@ -40,7 +40,7 @@ func TestVersionsOtherThanZeroDoNotSupportJSON(t *testing.T) { version StateVersion createState func() mipsevm.FPVMState }{ - {VersionMultiThreaded64_v3, func() mipsevm.FPVMState { return multithreaded.CreateEmptyState() }}, + {GetCurrentMultiThreaded64(), func() mipsevm.FPVMState { return multithreaded.CreateEmptyState() }}, } for _, test := range tests { test := test diff --git a/cannon/mipsevm/versions/state_test.go b/cannon/mipsevm/versions/state_test.go index 1768ecaea0..930906b6fb 100644 --- a/cannon/mipsevm/versions/state_test.go +++ b/cannon/mipsevm/versions/state_test.go @@ -20,14 +20,14 @@ func TestNewFromState(t *testing.T) { actual, err := NewFromState(singlethreaded.CreateEmptyState()) require.NoError(t, err) require.IsType(t, &singlethreaded.State{}, actual.FPVMState) - require.Equal(t, VersionSingleThreaded2, actual.Version) + require.Equal(t, GetCurrentSingleThreaded(), actual.Version) }) t.Run("multithreaded-latestVersion", func(t *testing.T) { actual, err := NewFromState(multithreaded.CreateEmptyState()) require.NoError(t, err) require.IsType(t, &multithreaded.State{}, actual.FPVMState) - require.Equal(t, VersionMultiThreaded_v2, actual.Version) + require.Equal(t, GetCurrentMultiThreaded(), actual.Version) }) } @@ -58,8 +58,8 @@ func TestVersionsOtherThanZeroDoNotSupportJSON(t *testing.T) { version StateVersion createState func() mipsevm.FPVMState }{ - {VersionSingleThreaded2, func() mipsevm.FPVMState { return singlethreaded.CreateEmptyState() }}, - {VersionMultiThreaded_v2, func() mipsevm.FPVMState { return multithreaded.CreateEmptyState() }}, + {GetCurrentSingleThreaded(), func() mipsevm.FPVMState { return singlethreaded.CreateEmptyState() }}, + {GetCurrentMultiThreaded(), func() mipsevm.FPVMState { return multithreaded.CreateEmptyState() }}, } for _, test := range tests { test := test @@ -75,16 +75,6 @@ func TestVersionsOtherThanZeroDoNotSupportJSON(t *testing.T) { } } -func TestParseStateVersion(t *testing.T) { - for _, version := range StateVersionTypes { - t.Run(version.String(), func(t *testing.T) { - result, err := ParseStateVersion(version.String()) - require.NoError(t, err) - require.Equal(t, version, result) - }) - } -} - func writeToFile(t *testing.T, filename string, data serialize.Serializable) string { dir := t.TempDir() path := filepath.Join(dir, filename) diff --git a/cannon/mipsevm/versions/version.go b/cannon/mipsevm/versions/version.go new file mode 100644 index 0000000000..0c33144e13 --- /dev/null +++ b/cannon/mipsevm/versions/version.go @@ -0,0 +1,105 @@ +package versions + +import ( + "errors" + "slices" +) + +type StateVersion uint8 + +const ( + // VersionSingleThreaded is the version of the Cannon STF found in op-contracts/v1.6.0 - https://github.com/ethereum-optimism/optimism/blob/op-contracts/v1.6.0/packages/contracts-bedrock/src/cannon/MIPS.sol + VersionSingleThreaded StateVersion = iota + // VersionMultiThreaded is the original implementation of 32-bit multithreaded cannon, tagged at cannon/v1.3.0 + VersionMultiThreaded + // VersionSingleThreaded2 is based on VersionSingleThreaded with the addition of support for fcntl(F_GETFD) syscall + // This is the latest 32-bit single-threaded vm + VersionSingleThreaded2 + // VersionMultiThreaded64 is the original 64-bit MTCannon implementation (pre-audit), tagged at cannon/v1.2.0 + VersionMultiThreaded64 + // VersionMultiThreaded64_v2 includes an audit fix to ensure futex values are always 32-bit, tagged at cannon/v1.3.0 + VersionMultiThreaded64_v2 + // VersionMultiThreaded_v2 is the latest 32-bit multithreaded vm + VersionMultiThreaded_v2 + // VersionMultiThreaded64_v3 is the latest 64-bit multithreaded vm + VersionMultiThreaded64_v3 +) + +var StateVersionTypes = []StateVersion{ + VersionSingleThreaded, + VersionMultiThreaded, + VersionSingleThreaded2, + VersionMultiThreaded64, + VersionMultiThreaded64_v2, + VersionMultiThreaded_v2, + VersionMultiThreaded64_v3, +} + +func (s StateVersion) String() string { + switch s { + case VersionSingleThreaded: + return "singlethreaded" + case VersionMultiThreaded: + return "multithreaded" + case VersionSingleThreaded2: + return "singlethreaded-2" + case VersionMultiThreaded64: + return "multithreaded64" + case VersionMultiThreaded64_v2: + return "multithreaded64-2" + case VersionMultiThreaded_v2: + return "multithreaded-2" + case VersionMultiThreaded64_v3: + return "multithreaded64-3" + default: + return "unknown" + } +} + +func ParseStateVersion(ver string) (StateVersion, error) { + switch ver { + case "singlethreaded": + return VersionSingleThreaded, nil + case "multithreaded": + return VersionMultiThreaded, nil + case "singlethreaded-2": + return VersionSingleThreaded2, nil + case "multithreaded64": + return VersionMultiThreaded64, nil + case "multithreaded64-2": + return VersionMultiThreaded64_v2, nil + case "multithreaded-2": + return VersionMultiThreaded_v2, nil + case "multithreaded64-3": + return VersionMultiThreaded64_v3, nil + default: + return StateVersion(0), errors.New("unknown state version") + } +} + +func IsValidStateVersion(ver StateVersion) bool { + return slices.Contains(StateVersionTypes, ver) +} + +func GetStateVersionStrings() []string { + vers := make([]string, len(StateVersionTypes)) + for i, v := range StateVersionTypes { + vers[i] = v.String() + } + return vers +} + +// GetCurrentMultiThreaded64 returns the 64-bit multithreaded VM version that is currently supported +func GetCurrentMultiThreaded64() StateVersion { + return VersionMultiThreaded64_v3 +} + +// GetCurrentMultiThreaded returns the 32-bit multithreaded VM version that is currently supported +func GetCurrentMultiThreaded() StateVersion { + return VersionMultiThreaded_v2 +} + +// GetCurrentSingleThreaded returns the 32-bit single-threaded VM version that is currently supported +func GetCurrentSingleThreaded() StateVersion { + return VersionSingleThreaded2 +} diff --git a/cannon/mipsevm/versions/version_test.go b/cannon/mipsevm/versions/version_test.go new file mode 100644 index 0000000000..2bfcf335ed --- /dev/null +++ b/cannon/mipsevm/versions/version_test.go @@ -0,0 +1,17 @@ +package versions + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParseStateVersion(t *testing.T) { + for _, version := range StateVersionTypes { + t.Run(version.String(), func(t *testing.T) { + result, err := ParseStateVersion(version.String()) + require.NoError(t, err) + require.Equal(t, version, result) + }) + } +} diff --git a/cannon/multicannon/exec.go b/cannon/multicannon/exec.go index 982b83c556..f8709a2fe1 100644 --- a/cannon/multicannon/exec.go +++ b/cannon/multicannon/exec.go @@ -8,7 +8,6 @@ import ( "os" "os/exec" "path/filepath" - "slices" "github.com/ethereum-optimism/optimism/cannon/mipsevm/versions" ) @@ -21,7 +20,7 @@ var vmFS embed.FS const baseDir = "embeds" func ExecuteCannon(ctx context.Context, args []string, ver versions.StateVersion) error { - if !slices.Contains(versions.StateVersionTypes, ver) { + if !versions.IsValidStateVersion(ver) { return errors.New("unsupported version") } diff --git a/devnet-sdk/book/src/shell.md b/devnet-sdk/book/src/shell.md index e5a4e91e31..f0ad9cad1d 100644 --- a/devnet-sdk/book/src/shell.md +++ b/devnet-sdk/book/src/shell.md @@ -53,7 +53,7 @@ export ETH_JWT_SECRET=... ```bash # Enter devnet shell -go run devnet-sdk/shll/cmd/enter/main.go --descriptor devnet.json --chain ... +go run devnet-sdk/shell/cmd/enter/main.go --descriptor devnet.json --chain ... # Now you can use tools directly cast block latest @@ -72,6 +72,7 @@ exit ## Implementation Details The shell integration: + 1. Reads the descriptor file 2. Sets up environment variables based on the descriptor content 3. Creates a new shell session with the configured environment diff --git a/devnet-sdk/constraints/constraints_test.go b/devnet-sdk/constraints/constraints_test.go index 02bf69ac01..b55bd2892d 100644 --- a/devnet-sdk/constraints/constraints_test.go +++ b/devnet-sdk/constraints/constraints_test.go @@ -5,6 +5,7 @@ import ( "math/big" "testing" + "github.com/ethereum-optimism/optimism/devnet-sdk/contracts/bindings" "github.com/ethereum-optimism/optimism/devnet-sdk/system" "github.com/ethereum-optimism/optimism/devnet-sdk/types" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -35,6 +36,14 @@ func (m mockWallet) SendETH(to types.Address, amount types.Balance) types.WriteI panic("not implemented") } +func (m mockWallet) InitiateMessage(chainID types.ChainID, target common.Address, message []byte) types.WriteInvocation[any] { + panic("not implemented") +} + +func (m mockWallet) ExecuteMessage(identifier bindings.Identifier, sentMessage []byte) types.WriteInvocation[any] { + panic("not implemented") +} + func (m mockWallet) Nonce() uint64 { return 0 } diff --git a/devnet-sdk/contracts/bindings/l2tol2crossdomainmessenger.go b/devnet-sdk/contracts/bindings/l2tol2crossdomainmessenger.go new file mode 100644 index 0000000000..01d733398b --- /dev/null +++ b/devnet-sdk/contracts/bindings/l2tol2crossdomainmessenger.go @@ -0,0 +1,788 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package bindings + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription +) + +// Identifier is an auto generated low-level Go binding around an user-defined struct. +type Identifier struct { + Origin common.Address + BlockNumber *big.Int + LogIndex *big.Int + Timestamp *big.Int + ChainId *big.Int +} + +// L2ToL2CrossDomainMessengerMetaData contains all meta data concerning the L2ToL2CrossDomainMessenger contract. +var L2ToL2CrossDomainMessengerMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[],\"name\":\"crossDomainMessageContext\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"sender_\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"source_\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"crossDomainMessageSender\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"sender_\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"crossDomainMessageSource\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"source_\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"messageNonce\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"messageVersion\",\"outputs\":[{\"internalType\":\"uint16\",\"name\":\"\",\"type\":\"uint16\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"origin\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"logIndex\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"timestamp\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"chainId\",\"type\":\"uint256\"}],\"internalType\":\"structIdentifier\",\"name\":\"_id\",\"type\":\"tuple\"},{\"internalType\":\"bytes\",\"name\":\"_sentMessage\",\"type\":\"bytes\"}],\"name\":\"relayMessage\",\"outputs\":[{\"internalType\":\"bytes\",\"name\":\"returnData_\",\"type\":\"bytes\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_destination\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"_target\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"_message\",\"type\":\"bytes\"}],\"name\":\"sendMessage\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"name\":\"successfulMessages\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"version\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"source\",\"type\":\"uint256\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"messageNonce\",\"type\":\"uint256\"},{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"messageHash\",\"type\":\"bytes32\"}],\"name\":\"RelayedMessage\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"destination\",\"type\":\"uint256\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"messageNonce\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"message\",\"type\":\"bytes\"}],\"name\":\"SentMessage\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"EventPayloadNotSentMessage\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"IdOriginNotL2ToL2CrossDomainMessenger\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidChainId\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"MessageAlreadyRelayed\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"MessageDestinationNotRelayChain\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"MessageDestinationSameChain\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"MessageTargetCrossL2Inbox\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"MessageTargetL2ToL2CrossDomainMessenger\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"NotEntered\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ReentrantCall\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"TargetCallFailed\",\"type\":\"error\"}]", +} + +// L2ToL2CrossDomainMessengerABI is the input ABI used to generate the binding from. +// Deprecated: Use L2ToL2CrossDomainMessengerMetaData.ABI instead. +var L2ToL2CrossDomainMessengerABI = L2ToL2CrossDomainMessengerMetaData.ABI + +// L2ToL2CrossDomainMessenger is an auto generated Go binding around an Ethereum contract. +type L2ToL2CrossDomainMessenger struct { + L2ToL2CrossDomainMessengerCaller // Read-only binding to the contract + L2ToL2CrossDomainMessengerTransactor // Write-only binding to the contract + L2ToL2CrossDomainMessengerFilterer // Log filterer for contract events +} + +// L2ToL2CrossDomainMessengerCaller is an auto generated read-only Go binding around an Ethereum contract. +type L2ToL2CrossDomainMessengerCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// L2ToL2CrossDomainMessengerTransactor is an auto generated write-only Go binding around an Ethereum contract. +type L2ToL2CrossDomainMessengerTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// L2ToL2CrossDomainMessengerFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type L2ToL2CrossDomainMessengerFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// L2ToL2CrossDomainMessengerSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type L2ToL2CrossDomainMessengerSession struct { + Contract *L2ToL2CrossDomainMessenger // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// L2ToL2CrossDomainMessengerCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type L2ToL2CrossDomainMessengerCallerSession struct { + Contract *L2ToL2CrossDomainMessengerCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// L2ToL2CrossDomainMessengerTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type L2ToL2CrossDomainMessengerTransactorSession struct { + Contract *L2ToL2CrossDomainMessengerTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// L2ToL2CrossDomainMessengerRaw is an auto generated low-level Go binding around an Ethereum contract. +type L2ToL2CrossDomainMessengerRaw struct { + Contract *L2ToL2CrossDomainMessenger // Generic contract binding to access the raw methods on +} + +// L2ToL2CrossDomainMessengerCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type L2ToL2CrossDomainMessengerCallerRaw struct { + Contract *L2ToL2CrossDomainMessengerCaller // Generic read-only contract binding to access the raw methods on +} + +// L2ToL2CrossDomainMessengerTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type L2ToL2CrossDomainMessengerTransactorRaw struct { + Contract *L2ToL2CrossDomainMessengerTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewL2ToL2CrossDomainMessenger creates a new instance of L2ToL2CrossDomainMessenger, bound to a specific deployed contract. +func NewL2ToL2CrossDomainMessenger(address common.Address, backend bind.ContractBackend) (*L2ToL2CrossDomainMessenger, error) { + contract, err := bindL2ToL2CrossDomainMessenger(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &L2ToL2CrossDomainMessenger{L2ToL2CrossDomainMessengerCaller: L2ToL2CrossDomainMessengerCaller{contract: contract}, L2ToL2CrossDomainMessengerTransactor: L2ToL2CrossDomainMessengerTransactor{contract: contract}, L2ToL2CrossDomainMessengerFilterer: L2ToL2CrossDomainMessengerFilterer{contract: contract}}, nil +} + +// NewL2ToL2CrossDomainMessengerCaller creates a new read-only instance of L2ToL2CrossDomainMessenger, bound to a specific deployed contract. +func NewL2ToL2CrossDomainMessengerCaller(address common.Address, caller bind.ContractCaller) (*L2ToL2CrossDomainMessengerCaller, error) { + contract, err := bindL2ToL2CrossDomainMessenger(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &L2ToL2CrossDomainMessengerCaller{contract: contract}, nil +} + +// NewL2ToL2CrossDomainMessengerTransactor creates a new write-only instance of L2ToL2CrossDomainMessenger, bound to a specific deployed contract. +func NewL2ToL2CrossDomainMessengerTransactor(address common.Address, transactor bind.ContractTransactor) (*L2ToL2CrossDomainMessengerTransactor, error) { + contract, err := bindL2ToL2CrossDomainMessenger(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &L2ToL2CrossDomainMessengerTransactor{contract: contract}, nil +} + +// NewL2ToL2CrossDomainMessengerFilterer creates a new log filterer instance of L2ToL2CrossDomainMessenger, bound to a specific deployed contract. +func NewL2ToL2CrossDomainMessengerFilterer(address common.Address, filterer bind.ContractFilterer) (*L2ToL2CrossDomainMessengerFilterer, error) { + contract, err := bindL2ToL2CrossDomainMessenger(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &L2ToL2CrossDomainMessengerFilterer{contract: contract}, nil +} + +// bindL2ToL2CrossDomainMessenger binds a generic wrapper to an already deployed contract. +func bindL2ToL2CrossDomainMessenger(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := abi.JSON(strings.NewReader(L2ToL2CrossDomainMessengerABI)) + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _L2ToL2CrossDomainMessenger.Contract.L2ToL2CrossDomainMessengerCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _L2ToL2CrossDomainMessenger.Contract.L2ToL2CrossDomainMessengerTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _L2ToL2CrossDomainMessenger.Contract.L2ToL2CrossDomainMessengerTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _L2ToL2CrossDomainMessenger.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _L2ToL2CrossDomainMessenger.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _L2ToL2CrossDomainMessenger.Contract.contract.Transact(opts, method, params...) +} + +// CrossDomainMessageContext is a free data retrieval call binding the contract method 0x7936cbee. +// +// Solidity: function crossDomainMessageContext() view returns(address sender_, uint256 source_) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerCaller) CrossDomainMessageContext(opts *bind.CallOpts) (struct { + Sender common.Address + Source *big.Int +}, error) { + var out []interface{} + err := _L2ToL2CrossDomainMessenger.contract.Call(opts, &out, "crossDomainMessageContext") + + outstruct := new(struct { + Sender common.Address + Source *big.Int + }) + if err != nil { + return *outstruct, err + } + + outstruct.Sender = *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + outstruct.Source = *abi.ConvertType(out[1], new(*big.Int)).(**big.Int) + + return *outstruct, err + +} + +// CrossDomainMessageContext is a free data retrieval call binding the contract method 0x7936cbee. +// +// Solidity: function crossDomainMessageContext() view returns(address sender_, uint256 source_) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerSession) CrossDomainMessageContext() (struct { + Sender common.Address + Source *big.Int +}, error) { + return _L2ToL2CrossDomainMessenger.Contract.CrossDomainMessageContext(&_L2ToL2CrossDomainMessenger.CallOpts) +} + +// CrossDomainMessageContext is a free data retrieval call binding the contract method 0x7936cbee. +// +// Solidity: function crossDomainMessageContext() view returns(address sender_, uint256 source_) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerCallerSession) CrossDomainMessageContext() (struct { + Sender common.Address + Source *big.Int +}, error) { + return _L2ToL2CrossDomainMessenger.Contract.CrossDomainMessageContext(&_L2ToL2CrossDomainMessenger.CallOpts) +} + +// CrossDomainMessageSender is a free data retrieval call binding the contract method 0x38ffde18. +// +// Solidity: function crossDomainMessageSender() view returns(address sender_) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerCaller) CrossDomainMessageSender(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _L2ToL2CrossDomainMessenger.contract.Call(opts, &out, "crossDomainMessageSender") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// CrossDomainMessageSender is a free data retrieval call binding the contract method 0x38ffde18. +// +// Solidity: function crossDomainMessageSender() view returns(address sender_) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerSession) CrossDomainMessageSender() (common.Address, error) { + return _L2ToL2CrossDomainMessenger.Contract.CrossDomainMessageSender(&_L2ToL2CrossDomainMessenger.CallOpts) +} + +// CrossDomainMessageSender is a free data retrieval call binding the contract method 0x38ffde18. +// +// Solidity: function crossDomainMessageSender() view returns(address sender_) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerCallerSession) CrossDomainMessageSender() (common.Address, error) { + return _L2ToL2CrossDomainMessenger.Contract.CrossDomainMessageSender(&_L2ToL2CrossDomainMessenger.CallOpts) +} + +// CrossDomainMessageSource is a free data retrieval call binding the contract method 0x24794462. +// +// Solidity: function crossDomainMessageSource() view returns(uint256 source_) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerCaller) CrossDomainMessageSource(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _L2ToL2CrossDomainMessenger.contract.Call(opts, &out, "crossDomainMessageSource") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// CrossDomainMessageSource is a free data retrieval call binding the contract method 0x24794462. +// +// Solidity: function crossDomainMessageSource() view returns(uint256 source_) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerSession) CrossDomainMessageSource() (*big.Int, error) { + return _L2ToL2CrossDomainMessenger.Contract.CrossDomainMessageSource(&_L2ToL2CrossDomainMessenger.CallOpts) +} + +// CrossDomainMessageSource is a free data retrieval call binding the contract method 0x24794462. +// +// Solidity: function crossDomainMessageSource() view returns(uint256 source_) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerCallerSession) CrossDomainMessageSource() (*big.Int, error) { + return _L2ToL2CrossDomainMessenger.Contract.CrossDomainMessageSource(&_L2ToL2CrossDomainMessenger.CallOpts) +} + +// MessageNonce is a free data retrieval call binding the contract method 0xecc70428. +// +// Solidity: function messageNonce() view returns(uint256) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerCaller) MessageNonce(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _L2ToL2CrossDomainMessenger.contract.Call(opts, &out, "messageNonce") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// MessageNonce is a free data retrieval call binding the contract method 0xecc70428. +// +// Solidity: function messageNonce() view returns(uint256) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerSession) MessageNonce() (*big.Int, error) { + return _L2ToL2CrossDomainMessenger.Contract.MessageNonce(&_L2ToL2CrossDomainMessenger.CallOpts) +} + +// MessageNonce is a free data retrieval call binding the contract method 0xecc70428. +// +// Solidity: function messageNonce() view returns(uint256) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerCallerSession) MessageNonce() (*big.Int, error) { + return _L2ToL2CrossDomainMessenger.Contract.MessageNonce(&_L2ToL2CrossDomainMessenger.CallOpts) +} + +// MessageVersion is a free data retrieval call binding the contract method 0x52617f3c. +// +// Solidity: function messageVersion() view returns(uint16) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerCaller) MessageVersion(opts *bind.CallOpts) (uint16, error) { + var out []interface{} + err := _L2ToL2CrossDomainMessenger.contract.Call(opts, &out, "messageVersion") + + if err != nil { + return *new(uint16), err + } + + out0 := *abi.ConvertType(out[0], new(uint16)).(*uint16) + + return out0, err + +} + +// MessageVersion is a free data retrieval call binding the contract method 0x52617f3c. +// +// Solidity: function messageVersion() view returns(uint16) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerSession) MessageVersion() (uint16, error) { + return _L2ToL2CrossDomainMessenger.Contract.MessageVersion(&_L2ToL2CrossDomainMessenger.CallOpts) +} + +// MessageVersion is a free data retrieval call binding the contract method 0x52617f3c. +// +// Solidity: function messageVersion() view returns(uint16) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerCallerSession) MessageVersion() (uint16, error) { + return _L2ToL2CrossDomainMessenger.Contract.MessageVersion(&_L2ToL2CrossDomainMessenger.CallOpts) +} + +// SuccessfulMessages is a free data retrieval call binding the contract method 0xb1b1b209. +// +// Solidity: function successfulMessages(bytes32 ) view returns(bool) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerCaller) SuccessfulMessages(opts *bind.CallOpts, arg0 [32]byte) (bool, error) { + var out []interface{} + err := _L2ToL2CrossDomainMessenger.contract.Call(opts, &out, "successfulMessages", arg0) + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// SuccessfulMessages is a free data retrieval call binding the contract method 0xb1b1b209. +// +// Solidity: function successfulMessages(bytes32 ) view returns(bool) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerSession) SuccessfulMessages(arg0 [32]byte) (bool, error) { + return _L2ToL2CrossDomainMessenger.Contract.SuccessfulMessages(&_L2ToL2CrossDomainMessenger.CallOpts, arg0) +} + +// SuccessfulMessages is a free data retrieval call binding the contract method 0xb1b1b209. +// +// Solidity: function successfulMessages(bytes32 ) view returns(bool) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerCallerSession) SuccessfulMessages(arg0 [32]byte) (bool, error) { + return _L2ToL2CrossDomainMessenger.Contract.SuccessfulMessages(&_L2ToL2CrossDomainMessenger.CallOpts, arg0) +} + +// Version is a free data retrieval call binding the contract method 0x54fd4d50. +// +// Solidity: function version() view returns(string) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerCaller) Version(opts *bind.CallOpts) (string, error) { + var out []interface{} + err := _L2ToL2CrossDomainMessenger.contract.Call(opts, &out, "version") + + if err != nil { + return *new(string), err + } + + out0 := *abi.ConvertType(out[0], new(string)).(*string) + + return out0, err + +} + +// Version is a free data retrieval call binding the contract method 0x54fd4d50. +// +// Solidity: function version() view returns(string) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerSession) Version() (string, error) { + return _L2ToL2CrossDomainMessenger.Contract.Version(&_L2ToL2CrossDomainMessenger.CallOpts) +} + +// Version is a free data retrieval call binding the contract method 0x54fd4d50. +// +// Solidity: function version() view returns(string) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerCallerSession) Version() (string, error) { + return _L2ToL2CrossDomainMessenger.Contract.Version(&_L2ToL2CrossDomainMessenger.CallOpts) +} + +// RelayMessage is a paid mutator transaction binding the contract method 0x8d1d298f. +// +// Solidity: function relayMessage((address,uint256,uint256,uint256,uint256) _id, bytes _sentMessage) payable returns(bytes returnData_) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerTransactor) RelayMessage(opts *bind.TransactOpts, _id Identifier, _sentMessage []byte) (*types.Transaction, error) { + return _L2ToL2CrossDomainMessenger.contract.Transact(opts, "relayMessage", _id, _sentMessage) +} + +// RelayMessage is a paid mutator transaction binding the contract method 0x8d1d298f. +// +// Solidity: function relayMessage((address,uint256,uint256,uint256,uint256) _id, bytes _sentMessage) payable returns(bytes returnData_) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerSession) RelayMessage(_id Identifier, _sentMessage []byte) (*types.Transaction, error) { + return _L2ToL2CrossDomainMessenger.Contract.RelayMessage(&_L2ToL2CrossDomainMessenger.TransactOpts, _id, _sentMessage) +} + +// RelayMessage is a paid mutator transaction binding the contract method 0x8d1d298f. +// +// Solidity: function relayMessage((address,uint256,uint256,uint256,uint256) _id, bytes _sentMessage) payable returns(bytes returnData_) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerTransactorSession) RelayMessage(_id Identifier, _sentMessage []byte) (*types.Transaction, error) { + return _L2ToL2CrossDomainMessenger.Contract.RelayMessage(&_L2ToL2CrossDomainMessenger.TransactOpts, _id, _sentMessage) +} + +// SendMessage is a paid mutator transaction binding the contract method 0x7056f41f. +// +// Solidity: function sendMessage(uint256 _destination, address _target, bytes _message) returns(bytes32) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerTransactor) SendMessage(opts *bind.TransactOpts, _destination *big.Int, _target common.Address, _message []byte) (*types.Transaction, error) { + return _L2ToL2CrossDomainMessenger.contract.Transact(opts, "sendMessage", _destination, _target, _message) +} + +// SendMessage is a paid mutator transaction binding the contract method 0x7056f41f. +// +// Solidity: function sendMessage(uint256 _destination, address _target, bytes _message) returns(bytes32) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerSession) SendMessage(_destination *big.Int, _target common.Address, _message []byte) (*types.Transaction, error) { + return _L2ToL2CrossDomainMessenger.Contract.SendMessage(&_L2ToL2CrossDomainMessenger.TransactOpts, _destination, _target, _message) +} + +// SendMessage is a paid mutator transaction binding the contract method 0x7056f41f. +// +// Solidity: function sendMessage(uint256 _destination, address _target, bytes _message) returns(bytes32) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerTransactorSession) SendMessage(_destination *big.Int, _target common.Address, _message []byte) (*types.Transaction, error) { + return _L2ToL2CrossDomainMessenger.Contract.SendMessage(&_L2ToL2CrossDomainMessenger.TransactOpts, _destination, _target, _message) +} + +// L2ToL2CrossDomainMessengerRelayedMessageIterator is returned from FilterRelayedMessage and is used to iterate over the raw logs and unpacked data for RelayedMessage events raised by the L2ToL2CrossDomainMessenger contract. +type L2ToL2CrossDomainMessengerRelayedMessageIterator struct { + Event *L2ToL2CrossDomainMessengerRelayedMessage // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *L2ToL2CrossDomainMessengerRelayedMessageIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(L2ToL2CrossDomainMessengerRelayedMessage) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(L2ToL2CrossDomainMessengerRelayedMessage) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *L2ToL2CrossDomainMessengerRelayedMessageIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *L2ToL2CrossDomainMessengerRelayedMessageIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// L2ToL2CrossDomainMessengerRelayedMessage represents a RelayedMessage event raised by the L2ToL2CrossDomainMessenger contract. +type L2ToL2CrossDomainMessengerRelayedMessage struct { + Source *big.Int + MessageNonce *big.Int + MessageHash [32]byte + Raw types.Log // Blockchain specific contextual infos +} + +// FilterRelayedMessage is a free log retrieval operation binding the contract event 0x5948076590932b9d173029c7df03fe386e755a61c86c7fe2671011a2faa2a379. +// +// Solidity: event RelayedMessage(uint256 indexed source, uint256 indexed messageNonce, bytes32 indexed messageHash) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerFilterer) FilterRelayedMessage(opts *bind.FilterOpts, source []*big.Int, messageNonce []*big.Int, messageHash [][32]byte) (*L2ToL2CrossDomainMessengerRelayedMessageIterator, error) { + + var sourceRule []interface{} + for _, sourceItem := range source { + sourceRule = append(sourceRule, sourceItem) + } + var messageNonceRule []interface{} + for _, messageNonceItem := range messageNonce { + messageNonceRule = append(messageNonceRule, messageNonceItem) + } + var messageHashRule []interface{} + for _, messageHashItem := range messageHash { + messageHashRule = append(messageHashRule, messageHashItem) + } + + logs, sub, err := _L2ToL2CrossDomainMessenger.contract.FilterLogs(opts, "RelayedMessage", sourceRule, messageNonceRule, messageHashRule) + if err != nil { + return nil, err + } + return &L2ToL2CrossDomainMessengerRelayedMessageIterator{contract: _L2ToL2CrossDomainMessenger.contract, event: "RelayedMessage", logs: logs, sub: sub}, nil +} + +// WatchRelayedMessage is a free log subscription operation binding the contract event 0x5948076590932b9d173029c7df03fe386e755a61c86c7fe2671011a2faa2a379. +// +// Solidity: event RelayedMessage(uint256 indexed source, uint256 indexed messageNonce, bytes32 indexed messageHash) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerFilterer) WatchRelayedMessage(opts *bind.WatchOpts, sink chan<- *L2ToL2CrossDomainMessengerRelayedMessage, source []*big.Int, messageNonce []*big.Int, messageHash [][32]byte) (event.Subscription, error) { + + var sourceRule []interface{} + for _, sourceItem := range source { + sourceRule = append(sourceRule, sourceItem) + } + var messageNonceRule []interface{} + for _, messageNonceItem := range messageNonce { + messageNonceRule = append(messageNonceRule, messageNonceItem) + } + var messageHashRule []interface{} + for _, messageHashItem := range messageHash { + messageHashRule = append(messageHashRule, messageHashItem) + } + + logs, sub, err := _L2ToL2CrossDomainMessenger.contract.WatchLogs(opts, "RelayedMessage", sourceRule, messageNonceRule, messageHashRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(L2ToL2CrossDomainMessengerRelayedMessage) + if err := _L2ToL2CrossDomainMessenger.contract.UnpackLog(event, "RelayedMessage", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseRelayedMessage is a log parse operation binding the contract event 0x5948076590932b9d173029c7df03fe386e755a61c86c7fe2671011a2faa2a379. +// +// Solidity: event RelayedMessage(uint256 indexed source, uint256 indexed messageNonce, bytes32 indexed messageHash) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerFilterer) ParseRelayedMessage(log types.Log) (*L2ToL2CrossDomainMessengerRelayedMessage, error) { + event := new(L2ToL2CrossDomainMessengerRelayedMessage) + if err := _L2ToL2CrossDomainMessenger.contract.UnpackLog(event, "RelayedMessage", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// L2ToL2CrossDomainMessengerSentMessageIterator is returned from FilterSentMessage and is used to iterate over the raw logs and unpacked data for SentMessage events raised by the L2ToL2CrossDomainMessenger contract. +type L2ToL2CrossDomainMessengerSentMessageIterator struct { + Event *L2ToL2CrossDomainMessengerSentMessage // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *L2ToL2CrossDomainMessengerSentMessageIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(L2ToL2CrossDomainMessengerSentMessage) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(L2ToL2CrossDomainMessengerSentMessage) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *L2ToL2CrossDomainMessengerSentMessageIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *L2ToL2CrossDomainMessengerSentMessageIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// L2ToL2CrossDomainMessengerSentMessage represents a SentMessage event raised by the L2ToL2CrossDomainMessenger contract. +type L2ToL2CrossDomainMessengerSentMessage struct { + Destination *big.Int + Target common.Address + MessageNonce *big.Int + Sender common.Address + Message []byte + Raw types.Log // Blockchain specific contextual infos +} + +// FilterSentMessage is a free log retrieval operation binding the contract event 0x382409ac69001e11931a28435afef442cbfd20d9891907e8fa373ba7d351f320. +// +// Solidity: event SentMessage(uint256 indexed destination, address indexed target, uint256 indexed messageNonce, address sender, bytes message) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerFilterer) FilterSentMessage(opts *bind.FilterOpts, destination []*big.Int, target []common.Address, messageNonce []*big.Int) (*L2ToL2CrossDomainMessengerSentMessageIterator, error) { + + var destinationRule []interface{} + for _, destinationItem := range destination { + destinationRule = append(destinationRule, destinationItem) + } + var targetRule []interface{} + for _, targetItem := range target { + targetRule = append(targetRule, targetItem) + } + var messageNonceRule []interface{} + for _, messageNonceItem := range messageNonce { + messageNonceRule = append(messageNonceRule, messageNonceItem) + } + + logs, sub, err := _L2ToL2CrossDomainMessenger.contract.FilterLogs(opts, "SentMessage", destinationRule, targetRule, messageNonceRule) + if err != nil { + return nil, err + } + return &L2ToL2CrossDomainMessengerSentMessageIterator{contract: _L2ToL2CrossDomainMessenger.contract, event: "SentMessage", logs: logs, sub: sub}, nil +} + +// WatchSentMessage is a free log subscription operation binding the contract event 0x382409ac69001e11931a28435afef442cbfd20d9891907e8fa373ba7d351f320. +// +// Solidity: event SentMessage(uint256 indexed destination, address indexed target, uint256 indexed messageNonce, address sender, bytes message) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerFilterer) WatchSentMessage(opts *bind.WatchOpts, sink chan<- *L2ToL2CrossDomainMessengerSentMessage, destination []*big.Int, target []common.Address, messageNonce []*big.Int) (event.Subscription, error) { + + var destinationRule []interface{} + for _, destinationItem := range destination { + destinationRule = append(destinationRule, destinationItem) + } + var targetRule []interface{} + for _, targetItem := range target { + targetRule = append(targetRule, targetItem) + } + var messageNonceRule []interface{} + for _, messageNonceItem := range messageNonce { + messageNonceRule = append(messageNonceRule, messageNonceItem) + } + + logs, sub, err := _L2ToL2CrossDomainMessenger.contract.WatchLogs(opts, "SentMessage", destinationRule, targetRule, messageNonceRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(L2ToL2CrossDomainMessengerSentMessage) + if err := _L2ToL2CrossDomainMessenger.contract.UnpackLog(event, "SentMessage", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseSentMessage is a log parse operation binding the contract event 0x382409ac69001e11931a28435afef442cbfd20d9891907e8fa373ba7d351f320. +// +// Solidity: event SentMessage(uint256 indexed destination, address indexed target, uint256 indexed messageNonce, address sender, bytes message) +func (_L2ToL2CrossDomainMessenger *L2ToL2CrossDomainMessengerFilterer) ParseSentMessage(log types.Log) (*L2ToL2CrossDomainMessengerSentMessage, error) { + event := new(L2ToL2CrossDomainMessengerSentMessage) + if err := _L2ToL2CrossDomainMessenger.contract.UnpackLog(event, "SentMessage", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/devnet-sdk/contracts/registry/client/client.go b/devnet-sdk/contracts/registry/client/client.go index d2c498bd11..b3f675de4e 100644 --- a/devnet-sdk/contracts/registry/client/client.go +++ b/devnet-sdk/contracts/registry/client/client.go @@ -2,10 +2,12 @@ package client import ( "fmt" + "strings" "github.com/ethereum-optimism/optimism/devnet-sdk/contracts/bindings" "github.com/ethereum-optimism/optimism/devnet-sdk/interfaces" "github.com/ethereum-optimism/optimism/devnet-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/ethclient" ) @@ -27,3 +29,20 @@ func (r *ClientRegistry) SuperchainWETH(address types.Address) (interfaces.Super binding: binding, }, nil } + +func (r *ClientRegistry) L2ToL2CrossDomainMessenger(address types.Address) (interfaces.L2ToL2CrossDomainMessenger, error) { + binding, err := bindings.NewL2ToL2CrossDomainMessenger(address, r.Client) + if err != nil { + return nil, fmt.Errorf("failed to create L2ToL2CrossDomainMessenger binding: %w", err) + } + abi, err := abi.JSON(strings.NewReader(bindings.L2ToL2CrossDomainMessengerMetaData.ABI)) + if err != nil { + return nil, fmt.Errorf("failed to create L2ToL2CrossDomainMessenger binding ABI: %w", err) + } + return &L2ToL2CrossDomainMessengerBinding{ + contractAddress: address, + client: r.Client, + binding: binding, + abi: &abi, + }, nil +} diff --git a/devnet-sdk/contracts/registry/client/l2tol2crossdomainmessenger.go b/devnet-sdk/contracts/registry/client/l2tol2crossdomainmessenger.go new file mode 100644 index 0000000000..f2ff3da7ab --- /dev/null +++ b/devnet-sdk/contracts/registry/client/l2tol2crossdomainmessenger.go @@ -0,0 +1,22 @@ +package client + +import ( + "github.com/ethereum-optimism/optimism/devnet-sdk/contracts/bindings" + "github.com/ethereum-optimism/optimism/devnet-sdk/interfaces" + "github.com/ethereum-optimism/optimism/devnet-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/ethclient" +) + +type L2ToL2CrossDomainMessengerBinding struct { + contractAddress types.Address + client *ethclient.Client + binding *bindings.L2ToL2CrossDomainMessenger + abi *abi.ABI +} + +var _ interfaces.L2ToL2CrossDomainMessenger = (*L2ToL2CrossDomainMessengerBinding)(nil) + +func (b *L2ToL2CrossDomainMessengerBinding) ABI() *abi.ABI { + return b.abi +} diff --git a/devnet-sdk/contracts/registry/empty/empty.go b/devnet-sdk/contracts/registry/empty/empty.go index 7aca815222..63b8265516 100644 --- a/devnet-sdk/contracts/registry/empty/empty.go +++ b/devnet-sdk/contracts/registry/empty/empty.go @@ -16,3 +16,10 @@ func (r *EmptyRegistry) SuperchainWETH(address types.Address) (interfaces.Superc Address: address, } } + +func (r *EmptyRegistry) L2ToL2CrossDomainMessenger(address types.Address) (interfaces.L2ToL2CrossDomainMessenger, error) { + return nil, &interfaces.ErrContractNotFound{ + ContractType: "L2ToL2CrossDomainMessenger", + Address: address, + } +} diff --git a/devnet-sdk/interfaces/registry.go b/devnet-sdk/interfaces/registry.go index 5d97484e7f..e5b7928987 100644 --- a/devnet-sdk/interfaces/registry.go +++ b/devnet-sdk/interfaces/registry.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/ethereum-optimism/optimism/devnet-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" ) // ErrContractNotFound indicates that a contract is not available at the requested address @@ -19,9 +20,14 @@ func (e *ErrContractNotFound) Error() string { // ContractsRegistry provides access to all supported contract instances type ContractsRegistry interface { SuperchainWETH(address types.Address) (SuperchainWETH, error) + L2ToL2CrossDomainMessenger(address types.Address) (L2ToL2CrossDomainMessenger, error) } // SuperchainWETH represents the interface for interacting with the SuperchainWETH contract type SuperchainWETH interface { BalanceOf(user types.Address) types.ReadInvocation[types.Balance] } + +type L2ToL2CrossDomainMessenger interface { + ABI() *abi.ABI +} diff --git a/devnet-sdk/proofs/prestate/client.go b/devnet-sdk/proofs/prestate/client.go new file mode 100644 index 0000000000..b48e67d580 --- /dev/null +++ b/devnet-sdk/proofs/prestate/client.go @@ -0,0 +1,230 @@ +package prestate + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net/http" + "path/filepath" +) + +const ( + InteropDepSetName = "depsets.json" +) + +// PrestateManifest maps prestate identifiers to their hashes +type PrestateManifest map[string]string + +// PrestateBuilderClient is a client for the prestate builder service +type PrestateBuilderClient struct { + url string + client *http.Client +} + +// NewPrestateBuilderClient creates a new client for the prestate builder service +func NewPrestateBuilderClient(url string) *PrestateBuilderClient { + return &PrestateBuilderClient{ + url: url, + client: &http.Client{}, + } +} + +// FileInput represents a file to be used in the build process +type FileInput struct { + Name string // Name of the file (used for identification) + Content io.Reader // Content of the file + Type string // Type information (e.g., "rollup-config", "genesis-config", "interop") +} + +// buildContext holds all the inputs for a build operation +type buildContext struct { + chains []string + files []FileInput + generatedInteropDepSet bool +} + +// PrestateBuilderOption is a functional option for configuring a build +type PrestateBuilderOption func(*buildContext) + +// WithInteropDepSet adds an interop dependency set file to the build +func WithInteropDepSet(content io.Reader) PrestateBuilderOption { + return func(c *buildContext) { + c.files = append(c.files, FileInput{ + Name: InteropDepSetName, + Content: content, + Type: "interop", + }) + } +} + +type dependency struct { + ChainIndex uint32 `json:"chainIndex"` + ActivationTime uint64 `json:"activationTime"` + HistoryMinTime uint64 `json:"historyMinTime"` +} + +type dependencySet struct { + Dependencies map[string]dependency `json:"dependencies"` +} + +func generateInteropDepSet(chains []string) ([]byte, error) { + deps := dependencySet{ + Dependencies: make(map[string]dependency), + } + + for i, chain := range chains { + deps.Dependencies[chain] = dependency{ + ChainIndex: uint32(i), + ActivationTime: 0, + HistoryMinTime: 0, + } + } + + json, err := json.Marshal(deps) + if err != nil { + return nil, err + } + return json, nil +} + +func WithGeneratedInteropDepSet() PrestateBuilderOption { + return func(c *buildContext) { + c.generatedInteropDepSet = true + } +} + +// WithChainConfig adds a pair of rollup and genesis config files to the build +func WithChainConfig(chainId string, rollupContent io.Reader, genesisContent io.Reader) PrestateBuilderOption { + return func(c *buildContext) { + c.chains = append(c.chains, chainId) + c.files = append(c.files, + FileInput{ + Name: chainId + "-rollup.json", + Content: rollupContent, + Type: "rollup-config", + }, + FileInput{ + Name: chainId + "-genesis.json", + Content: genesisContent, + Type: "genesis-config", + }, + ) + } +} + +// BuildPrestate sends the files to the prestate builder service and returns a manifest of the built prestates +func (c *PrestateBuilderClient) BuildPrestate(ctx context.Context, opts ...PrestateBuilderOption) (PrestateManifest, error) { + fmt.Println("Starting prestate build...") + + // Apply options to build context + bc := &buildContext{ + files: []FileInput{}, + } + + for _, opt := range opts { + opt(bc) + } + + if bc.generatedInteropDepSet { + depSet, err := generateInteropDepSet(bc.chains) + if err != nil { + return nil, fmt.Errorf("failed to generate interop dependency set: %w", err) + } + bc.files = append(bc.files, FileInput{ + Name: InteropDepSetName, + Content: bytes.NewReader(depSet), + Type: "interop", + }) + } + + fmt.Printf("Preparing to upload %d files\n", len(bc.files)) + + // Create a multipart form + var b bytes.Buffer + w := multipart.NewWriter(&b) + + // Add each file to the form + for _, file := range bc.files { + fmt.Printf("Adding file to form: %s (type: %s)\n", file.Name, file.Type) + // Create a form file with the file's name + fw, err := w.CreateFormFile("files[]", filepath.Base(file.Name)) + if err != nil { + return nil, fmt.Errorf("failed to create form file: %w", err) + } + + // Copy the file content to the form + if _, err := io.Copy(fw, file.Content); err != nil { + return nil, fmt.Errorf("failed to copy file content: %w", err) + } + } + + // Close the multipart writer + if err := w.Close(); err != nil { + return nil, fmt.Errorf("failed to close multipart writer: %w", err) + } + + fmt.Printf("Sending build request to %s\n", c.url) + + // Create the HTTP request + req, err := http.NewRequestWithContext(ctx, "POST", c.url, &b) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + // Set the content type + req.Header.Set("Content-Type", w.FormDataContentType()) + + // Send the request + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + // Check the response status + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNotModified { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) + } + + fmt.Println("Build request successful, fetching build manifest...") + + // If the build was successful, get the info.json file + infoURL := c.url + if infoURL[len(infoURL)-1] != '/' { + infoURL += "/" + } + infoURL += "info.json" + + fmt.Printf("Requesting manifest from %s\n", infoURL) + + infoReq, err := http.NewRequestWithContext(ctx, "GET", infoURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create info request: %w", err) + } + + infoResp, err := c.client.Do(infoReq) + if err != nil { + return nil, fmt.Errorf("failed to get info.json: %w", err) + } + defer infoResp.Body.Close() + + if infoResp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(infoResp.Body) + return nil, fmt.Errorf("unexpected info.json status code: %d, body: %s", infoResp.StatusCode, string(body)) + } + + // Parse the JSON response + var manifest PrestateManifest + if err := json.NewDecoder(infoResp.Body).Decode(&manifest); err != nil { + return nil, fmt.Errorf("failed to decode info.json response: %w", err) + } + + fmt.Printf("Build complete. Generated %d prestate entries\n", len(manifest)) + + return manifest, nil + +} diff --git a/devnet-sdk/proofs/prestate/cmd/main.go b/devnet-sdk/proofs/prestate/cmd/main.go new file mode 100644 index 0000000000..782ac7a642 --- /dev/null +++ b/devnet-sdk/proofs/prestate/cmd/main.go @@ -0,0 +1,99 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log" + "os" + "strings" + + "github.com/ethereum-optimism/optimism/devnet-sdk/proofs/prestate" +) + +type chainConfig struct { + id string + rollupConfig string + genesisConfig string +} + +func parseChainFlag(s string) (*chainConfig, error) { + parts := strings.Split(s, ",") + if len(parts) != 3 { + return nil, fmt.Errorf("chain flag must contain exactly 1 id and 2 files separated by comma") + } + return &chainConfig{ + id: strings.TrimSpace(parts[0]), + rollupConfig: strings.TrimSpace(parts[1]), + genesisConfig: strings.TrimSpace(parts[2]), + }, nil +} + +func main() { + var ( + clientURL = flag.String("url", "http://localhost:8080", "URL of the prestate builder service") + interop = flag.Bool("interop", false, "Generate interop dependency set") + chains = make(chainConfigList, 0) + ) + + flag.Var(&chains, "chain", "Chain configuration files in format: rollup-config.json,genesis-config.json (can be specified multiple times)") + flag.Parse() + + client := prestate.NewPrestateBuilderClient(*clientURL) + ctx := context.Background() + + // Build options list + opts := make([]prestate.PrestateBuilderOption, 0) + + if *interop { + opts = append(opts, prestate.WithGeneratedInteropDepSet()) + } + + // Add chain configs + for i, chain := range chains { + rollupFile, err := os.Open(chain.rollupConfig) + if err != nil { + log.Fatalf("Failed to open rollup config file for chain %d: %v", i, err) + } + defer rollupFile.Close() + + genesisFile, err := os.Open(chain.genesisConfig) + if err != nil { + log.Fatalf("Failed to open genesis config file for chain %d: %v", i, err) + } + defer genesisFile.Close() + + opts = append(opts, prestate.WithChainConfig( + chain.id, + rollupFile, + genesisFile, + )) + } + + // Build prestate + manifest, err := client.BuildPrestate(ctx, opts...) + if err != nil { + log.Fatalf("Failed to build prestate: %v", err) + } + + // Print manifest + for id, hash := range manifest { + fmt.Printf("%s: %s\n", id, hash) + } +} + +// chainConfigList implements flag.Value interface for repeated chain flags +type chainConfigList []*chainConfig + +func (c *chainConfigList) String() string { + return fmt.Sprintf("%v", *c) +} + +func (c *chainConfigList) Set(value string) error { + config, err := parseChainFlag(value) + if err != nil { + return err + } + *c = append(*c, config) + return nil +} diff --git a/devnet-sdk/system/chain.go b/devnet-sdk/system/chain.go index a0bbf810bc..30ae169a28 100644 --- a/devnet-sdk/system/chain.go +++ b/devnet-sdk/system/chain.go @@ -5,34 +5,54 @@ import ( "fmt" "math/big" "sync" + "time" "github.com/ethereum-optimism/optimism/devnet-sdk/contracts" "github.com/ethereum-optimism/optimism/devnet-sdk/descriptors" "github.com/ethereum-optimism/optimism/devnet-sdk/interfaces" "github.com/ethereum-optimism/optimism/devnet-sdk/types" + "github.com/ethereum-optimism/optimism/op-service/client" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/sources" + "github.com/ethereum/go-ethereum/common" coreTypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rpc" ) +// this is to differentiate between op-geth and go-ethereum +type opBlock interface { + WithdrawalsRoot() *common.Hash +} + var ( // This will make sure that we implement the Chain interface _ Chain = (*chain)(nil) + + // Make sure we're using op-geth in place of go-ethereum. + // If you're wondering why this fails at compile time, + // it's most likely because you're not using a "replace" + // directive in your go.mod file. + _ opBlock = (*coreTypes.Block)(nil) ) // clientManager handles ethclient connections type clientManager struct { - mu sync.RWMutex - clients map[string]*ethclient.Client + mu sync.RWMutex + clients map[string]*sources.EthClient + gethClients map[string]*ethclient.Client } func newClientManager() *clientManager { return &clientManager{ - clients: make(map[string]*ethclient.Client), + clients: make(map[string]*sources.EthClient), + gethClients: make(map[string]*ethclient.Client), } } -func (m *clientManager) Client(rpcURL string) (*ethclient.Client, error) { +func (m *clientManager) Client(rpcURL string) (*sources.EthClient, error) { m.mu.RLock() if client, ok := m.clients[rpcURL]; ok { m.mu.RUnlock() @@ -48,11 +68,51 @@ func (m *clientManager) Client(rpcURL string) (*ethclient.Client, error) { return client, nil } + ethClCfg := sources.EthClientConfig{ + MaxRequestsPerBatch: 10, + MaxConcurrentRequests: 10, + ReceiptsCacheSize: 10, + TransactionsCacheSize: 10, + HeadersCacheSize: 10, + PayloadsCacheSize: 10, + TrustRPC: false, + MustBePostMerge: true, + RPCProviderKind: sources.RPCKindStandard, + MethodResetDuration: time.Minute, + } + rpcClient, err := rpc.DialContext(context.Background(), rpcURL) + if err != nil { + return nil, err + } + ethCl, err := sources.NewEthClient(client.NewBaseRPCClient(rpcClient), log.Root(), nil, ðClCfg) + if err != nil { + return nil, err + } + m.clients[rpcURL] = ethCl + return ethCl, nil +} + +func (m *clientManager) GethClient(rpcURL string) (*ethclient.Client, error) { + m.mu.RLock() + if client, ok := m.gethClients[rpcURL]; ok { + m.mu.RUnlock() + return client, nil + } + m.mu.RUnlock() + + m.mu.Lock() + defer m.mu.Unlock() + + // Double-check after acquiring write lock + if client, ok := m.gethClients[rpcURL]; ok { + return client, nil + } + client, err := ethclient.Dial(rpcURL) if err != nil { - return nil, fmt.Errorf("failed to connect to ethereum client: %w", err) + return nil, err } - m.clients[rpcURL] = client + m.gethClients[rpcURL] = client return client, nil } @@ -72,10 +132,14 @@ func (c *chain) Node() Node { return c.node } -func (c *chain) Client() (*ethclient.Client, error) { +func (c *chain) Client() (*sources.EthClient, error) { return c.clients.Client(c.rpcUrl) } +func (c *chain) GethClient() (*ethclient.Client, error) { + return c.clients.GethClient(c.rpcUrl) +} + func newChain(chainID string, rpcUrl string, users map[string]Wallet, chainConfig *params.ChainConfig, addresses descriptors.AddressMap) *chain { clients := newClientManager() chain := &chain{ @@ -97,8 +161,7 @@ func (c *chain) ContractsRegistry() interfaces.ContractsRegistry { if c.registry != nil { return c.registry } - - client, err := c.Client() + client, err := c.clients.GethClient(c.rpcUrl) if err != nil { return contracts.NewEmptyRegistry() } @@ -136,12 +199,12 @@ func (c *chain) ID() types.ChainID { return types.ChainID(id) } -func checkHeader(ctx context.Context, client *ethclient.Client, check func(*coreTypes.Header) bool) bool { - head, err := client.HeaderByNumber(ctx, nil) +func checkHeader(ctx context.Context, client *sources.EthClient, check func(eth.BlockInfo) bool) bool { + info, err := client.InfoByLabel(ctx, eth.Unsafe) if err != nil { return false } - return check(head) + return check(info) } func (c *chain) SupportsEIP(ctx context.Context, eip uint64) bool { @@ -152,12 +215,12 @@ func (c *chain) SupportsEIP(ctx context.Context, eip uint64) bool { switch eip { case 1559: - return checkHeader(ctx, client, func(h *coreTypes.Header) bool { - return h.BaseFee != nil + return checkHeader(ctx, client, func(h eth.BlockInfo) bool { + return h.BaseFee() != nil }) case 4844: - return checkHeader(ctx, client, func(h *coreTypes.Header) bool { - return h.ExcessBlobGas != nil + return checkHeader(ctx, client, func(h eth.BlockInfo) bool { + return h.ExcessBlobGas() != nil }) } return false diff --git a/devnet-sdk/system/interfaces.go b/devnet-sdk/system/interfaces.go index 33628c5bfc..61a6d12234 100644 --- a/devnet-sdk/system/interfaces.go +++ b/devnet-sdk/system/interfaces.go @@ -4,10 +4,15 @@ import ( "context" "math/big" + "github.com/ethereum-optimism/optimism/devnet-sdk/contracts/bindings" "github.com/ethereum-optimism/optimism/devnet-sdk/descriptors" "github.com/ethereum-optimism/optimism/devnet-sdk/interfaces" "github.com/ethereum-optimism/optimism/devnet-sdk/types" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/sources" + supervisorTypes "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" coreTypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/params" @@ -52,14 +57,15 @@ type Node interface { GasPrice(ctx context.Context) (*big.Int, error) GasLimit(ctx context.Context, tx TransactionData) (uint64, error) PendingNonceAt(ctx context.Context, address common.Address) (uint64, error) - BlockByNumber(ctx context.Context, number *big.Int) (*coreTypes.Block, error) + BlockByNumber(ctx context.Context, number *big.Int) (eth.BlockInfo, error) } // LowLevelChain is a Chain that gives direct access to the low level RPC client. type LowLevelChain interface { Chain RPCURL() string - Client() (*ethclient.Client, error) + Client() (*sources.EthClient, error) + GethClient() (*ethclient.Client, error) } // Wallet represents a chain wallet. @@ -68,6 +74,8 @@ type Wallet interface { PrivateKey() types.Key Address() types.Address SendETH(to types.Address, amount types.Balance) types.WriteInvocation[any] + InitiateMessage(chainID types.ChainID, target common.Address, message []byte) types.WriteInvocation[any] + ExecuteMessage(identifier bindings.Identifier, sentMessage []byte) types.WriteInvocation[any] Balance() types.Balance Nonce() uint64 @@ -97,6 +105,12 @@ type Transaction interface { TransactionData } +type Receipt interface { + BlockNumber() *big.Int + Logs() []*coreTypes.Log + TxHash() common.Hash +} + // RawTransaction is an optional interface that can be implemented by a Transaction // to provide access to the raw transaction data. // It is currently necessary to perform processing operations (signing, sending) @@ -111,9 +125,24 @@ type RawTransaction interface { type InteropSystem interface { System InteropSet() InteropSet + Supervisor(context.Context) (Supervisor, error) } // InteropSet provides access to L2 chains in an interop environment type InteropSet interface { L2s() []Chain } + +// Supervisor provides access to the query interface of the supervisor +type Supervisor interface { + LocalUnsafe(context.Context, eth.ChainID) (eth.BlockID, error) + CrossSafe(context.Context, eth.ChainID) (supervisorTypes.DerivedIDPair, error) + Finalized(context.Context, eth.ChainID) (eth.BlockID, error) + FinalizedL1(context.Context) (eth.BlockRef, error) + CrossDerivedFrom(context.Context, eth.ChainID, eth.BlockID) (eth.BlockRef, error) + UpdateLocalUnsafe(context.Context, eth.ChainID, eth.BlockRef) error + UpdateLocalSafe(context.Context, eth.ChainID, eth.L1BlockRef, eth.BlockRef) error + SuperRootAtTimestamp(context.Context, hexutil.Uint64) (eth.SuperRootResponse, error) + AllSafeDerivedAt(context.Context, eth.BlockID) (derived map[eth.ChainID]eth.BlockID, err error) + SyncStatus(context.Context) (eth.SupervisorSyncStatus, error) +} diff --git a/devnet-sdk/system/node.go b/devnet-sdk/system/node.go index 5c9c12b7b2..b46333c07d 100644 --- a/devnet-sdk/system/node.go +++ b/devnet-sdk/system/node.go @@ -5,9 +5,9 @@ import ( "fmt" "math/big" + "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" - coreTypes "github.com/ethereum/go-ethereum/core/types" ) var ( @@ -61,10 +61,19 @@ func (n *node) PendingNonceAt(ctx context.Context, address common.Address) (uint return client.PendingNonceAt(ctx, address) } -func (n *node) BlockByNumber(ctx context.Context, number *big.Int) (*coreTypes.Block, error) { +func (n *node) BlockByNumber(ctx context.Context, number *big.Int) (eth.BlockInfo, error) { client, err := n.clients.Client(n.rpcUrl) if err != nil { return nil, fmt.Errorf("failed to get client: %w", err) } - return client.BlockByNumber(ctx, number) + var block eth.BlockInfo + if number != nil { + block, err = client.InfoByNumber(ctx, number.Uint64()) + } else { + block, err = client.InfoByLabel(ctx, eth.Unsafe) + } + if err != nil { + return nil, err + } + return block, nil } diff --git a/devnet-sdk/system/periphery/go-ethereum/fees.go b/devnet-sdk/system/periphery/go-ethereum/fees.go new file mode 100644 index 0000000000..0508d195e7 --- /dev/null +++ b/devnet-sdk/system/periphery/go-ethereum/fees.go @@ -0,0 +1,134 @@ +package goethereum + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" +) + +var ( + // Ensure that the feeEstimator implements the FeeEstimator interface + _ FeeEstimator = (*EIP1559FeeEstimator)(nil) + + // Ensure that the EIP1159FeeEthClient implements the EIP1159FeeEthClient interface + _ EIP1159FeeEthClient = (*ethclient.Client)(nil) +) + +// FeeEstimator is a generic fee estimation interface (not specific to EIP-1559) +type FeeEstimator interface { + EstimateFees(ctx context.Context, opts *bind.TransactOpts) (*bind.TransactOpts, error) +} + +// EIP1559FeeEstimator is a fee estimator that uses EIP-1559 fee estimation +type EIP1559FeeEstimator struct { + // Access to the Ethereum client is needed to get the fee information from the chain + client EIP1159FeeEthClient + + options eip1559FeeEstimatorOptions +} + +type eip1559FeeEstimatorOptions struct { + // The base multiplier is used to increase the maxFeePerGas (GasFeeCap) by a factor + baseMultiplier float64 + + // The tip multiplier is used to increase the maxPriorityFeePerGas (GasTipCap) by a factor + tipMultiplier float64 +} + +type EIP1559FeeEstimatorOption interface { + apply(*eip1559FeeEstimatorOptions) +} + +type eip1559FeeEstimatorOptionBaseMultiplier float64 + +func (o eip1559FeeEstimatorOptionBaseMultiplier) apply(opts *eip1559FeeEstimatorOptions) { + opts.baseMultiplier = float64(o) +} + +func WithEIP1559BaseMultiplier(multiplier float64) EIP1559FeeEstimatorOption { + return eip1559FeeEstimatorOptionBaseMultiplier(multiplier) +} + +type eip1559FeeEstimatorOptionTipMultiplier float64 + +func (o eip1559FeeEstimatorOptionTipMultiplier) apply(opts *eip1559FeeEstimatorOptions) { + opts.tipMultiplier = float64(o) +} + +func WithEIP1559TipMultiplier(multiplier float64) EIP1559FeeEstimatorOption { + return eip1559FeeEstimatorOptionTipMultiplier(multiplier) +} + +func NewEIP1559FeeEstimator(client EIP1159FeeEthClient, opts ...EIP1559FeeEstimatorOption) *EIP1559FeeEstimator { + options := eip1559FeeEstimatorOptions{ + baseMultiplier: 1.0, + tipMultiplier: 1.0, + } + + for _, o := range opts { + o.apply(&options) + } + + return &EIP1559FeeEstimator{ + client: client, + options: options, + } +} + +func (f *EIP1559FeeEstimator) EstimateFees(ctx context.Context, opts *bind.TransactOpts) (*bind.TransactOpts, error) { + newOpts := *opts + + // Add a gas tip cap if needed + if newOpts.GasTipCap == nil { + tipCap, err := f.client.SuggestGasTipCap(ctx) + + if err != nil { + return nil, err + } + + // GasTipCap represents the maxPriorityFeePerGas + newOpts.GasTipCap = multiplyBigInt(tipCap, f.options.tipMultiplier) + } + + // Add a gas fee cap if needed + if newOpts.GasFeeCap == nil { + block, err := f.client.BlockByNumber(ctx, nil) + if err != nil { + return nil, err + } + + baseFee := block.BaseFee() + if baseFee != nil { + // The adjusted base fee takes the multiplier into account + adjustedBaseFee := multiplyBigInt(baseFee, f.options.baseMultiplier) + + // The total fee (maxFeePerGas) is the sum of the base fee and the tip + newOpts.GasFeeCap = big.NewInt(0).Add(adjustedBaseFee, newOpts.GasTipCap) + } + } + + return &newOpts, nil +} + +// EIP1159FeeEthClient is a subset of the ethclient.Client interface required for fee estimation +type EIP1159FeeEthClient interface { + BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) + SuggestGasTipCap(ctx context.Context) (*big.Int, error) +} + +// multiplyBigInt is a little helper for a messy big.Int x float64 multiplication +// +// It rounds the result away from zero since that's the lower risk behavior for fee estimation +func multiplyBigInt(b *big.Int, m float64) *big.Int { + bFloat := big.NewFloat(0).SetInt(b) + mFloat := big.NewFloat(m) + ceiled, accuracy := big.NewFloat(0).Mul(bFloat, mFloat).Int(nil) + if accuracy == big.Below { + ceiled = ceiled.Add(ceiled, big.NewInt(1)) + } + + return ceiled +} diff --git a/devnet-sdk/system/periphery/go-ethereum/fees_test.go b/devnet-sdk/system/periphery/go-ethereum/fees_test.go new file mode 100644 index 0000000000..ffe375bf93 --- /dev/null +++ b/devnet-sdk/system/periphery/go-ethereum/fees_test.go @@ -0,0 +1,276 @@ +package goethereum + +import ( + "context" + "fmt" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMultiplyBigInt(t *testing.T) { + type TestCase struct { + value *big.Int + multiplier float64 + expected *big.Int + } + + testCases := []TestCase{ + { + value: big.NewInt(10), + multiplier: 1.0, + expected: big.NewInt(10), + }, + { + value: big.NewInt(7), + multiplier: 0.0, + expected: big.NewInt(0), + }, + { + value: big.NewInt(10), + multiplier: 1.01, + expected: big.NewInt(11), + }, + { + value: big.NewInt(10), + multiplier: 1.11, + expected: big.NewInt(12), + }, + { + value: big.NewInt(5), + multiplier: 1.2, + expected: big.NewInt(6), + }, + } + + for _, testCase := range testCases { + t.Run(fmt.Sprintf("should return %d for %d multplied by %f", testCase.expected.Int64(), testCase.value.Int64(), testCase.multiplier), func(t *testing.T) { + result := multiplyBigInt(testCase.value, testCase.multiplier) + require.Equal(t, testCase.expected, result) + }) + } +} + +func TestEstimateEIP1559Fees(t *testing.T) { + t.Run("if GasFeeCap and GasTipCap are not nil", func(t *testing.T) { + opts := &bind.TransactOpts{ + GasFeeCap: big.NewInt(1), + GasTipCap: big.NewInt(2), + } + + t.Run("should not modify the options", func(t *testing.T) { + feeEstimator := NewEIP1559FeeEstimator(&mockFeeEthClientImpl{}) + newOpts, err := feeEstimator.EstimateFees(context.Background(), opts) + require.NoError(t, err) + + require.Equal(t, opts, newOpts) + + // We make sure that we get a copy of the object to prevent mutating the original + assert.NotSame(t, opts, newOpts) + }) + }) + + t.Run("if the GasTipCap is nil", func(t *testing.T) { + defaultOpts := &bind.TransactOpts{ + GasFeeCap: big.NewInt(1), + From: common.Address{}, + Nonce: big.NewInt(64), + } + + t.Run("should return an error if the client returns an error", func(t *testing.T) { + tipCapErr := fmt.Errorf("tip cap error") + feeEstimator := NewEIP1559FeeEstimator(&mockFeeEthClientImpl{ + tipCapErr: tipCapErr, + }) + _, err := feeEstimator.EstimateFees(context.Background(), defaultOpts) + require.Equal(t, tipCapErr, err) + }) + + t.Run("with default tip multiplier", func(t *testing.T) { + t.Run("should set the GasTipCap to the client's suggested tip cap", func(t *testing.T) { + tipCapValue := big.NewInt(5) + feeEstimator := NewEIP1559FeeEstimator(&mockFeeEthClientImpl{ + tipCapValue: tipCapValue, + }) + + newOpts, err := feeEstimator.EstimateFees(context.Background(), defaultOpts) + require.NoError(t, err) + + // We create a new opts with the expected tip cap added + expectedOpts := *defaultOpts + expectedOpts.GasTipCap = tipCapValue + + // We check that the tip has been added + require.Equal(t, &expectedOpts, newOpts) + + // We make sure that we get a copy of the object to prevent mutating the original + assert.NotSame(t, defaultOpts, newOpts) + }) + }) + + t.Run("with custom tip multiplier", func(t *testing.T) { + t.Run("should set the GasTipCap to the client's suggested tip cap multplied by the tip multiplier", func(t *testing.T) { + tipCapValue := big.NewInt(5) + tipMultiplier := 10.0 + // The expected tip is a product of the tip cap and the tip multiplier + expectedTip := big.NewInt(50) + + // We create a fee estimator with a custom tip multiplier + feeEstimator := NewEIP1559FeeEstimator(&mockFeeEthClientImpl{ + tipCapValue: tipCapValue, + }, WithEIP1559TipMultiplier(tipMultiplier)) + + newOpts, err := feeEstimator.EstimateFees(context.Background(), defaultOpts) + require.NoError(t, err) + + // We create a new opts with the expected tip cap added + expectedOpts := *defaultOpts + expectedOpts.GasTipCap = expectedTip + + // We check that the tip has been added + require.Equal(t, &expectedOpts, newOpts) + + // We make sure that we get a copy of the object to prevent mutating the original + assert.NotSame(t, defaultOpts, newOpts) + }) + }) + }) + + t.Run("if the GasFeeCap is nil", func(t *testing.T) { + defaultOpts := &bind.TransactOpts{ + GasTipCap: big.NewInt(1), + From: common.Address{}, + Nonce: big.NewInt(64), + } + + t.Run("should return an error if the client returns an error", func(t *testing.T) { + blockErr := fmt.Errorf("tip cap error") + feeEstimator := NewEIP1559FeeEstimator(&mockFeeEthClientImpl{ + blockErr: blockErr, + }) + _, err := feeEstimator.EstimateFees(context.Background(), defaultOpts) + require.Equal(t, blockErr, err) + }) + + t.Run("should set the GasFeeCap to the sum of block base fee and tip", func(t *testing.T) { + baseFeeValue := big.NewInt(5) + blockValue := types.NewBlock(&types.Header{ + BaseFee: baseFeeValue, + Time: 0, + }, nil, nil, nil, &mockBlockType{}) + + // We expect the total gas cap to be the base fee plus the tip cap + expectedGas := big.NewInt(0).Add(baseFeeValue, defaultOpts.GasTipCap) + + feeEstimator := NewEIP1559FeeEstimator(&mockFeeEthClientImpl{ + blockValue: blockValue, + }) + + newOpts, err := feeEstimator.EstimateFees(context.Background(), defaultOpts) + require.NoError(t, err) + + // We create a new opts with the expected fee cap added + expectedOpts := *defaultOpts + expectedOpts.GasFeeCap = expectedGas + + // We check that the tip has been added + require.Equal(t, &expectedOpts, newOpts) + + // We make sure that we get a copy of the object to prevent mutating the original + assert.NotSame(t, defaultOpts, newOpts) + }) + + t.Run("should set the GasFeeCap to nil if the base fee is nil", func(t *testing.T) { + blockValue := types.NewBlock(&types.Header{ + BaseFee: nil, + Time: 0, + }, nil, nil, nil, &mockBlockType{}) + + feeEstimator := NewEIP1559FeeEstimator(&mockFeeEthClientImpl{ + blockValue: blockValue, + }) + + newOpts, err := feeEstimator.EstimateFees(context.Background(), defaultOpts) + require.NoError(t, err) + + // We create a new opts with the expected fee cap added + expectedOpts := *defaultOpts + expectedOpts.GasFeeCap = nil + + // We check that the tip has been added + require.Equal(t, &expectedOpts, newOpts) + + // We make sure that we get a copy of the object to prevent mutating the original + assert.NotSame(t, defaultOpts, newOpts) + }) + + t.Run("with custom base multiplier", func(t *testing.T) { + t.Run("should set the GasFeeCap to the block base fee multplied by the base multiplier", func(t *testing.T) { + baseMultiplier := 1.2 + baseFeeValue := big.NewInt(9) + blockValue := types.NewBlock(&types.Header{ + BaseFee: baseFeeValue, + Time: 0, + }, nil, nil, nil, &mockBlockType{}) + + // We expect the total gas cap to be the base fee (9) multiplied by 1.2 (= 10.8, rounded up to 11) plus the tip cap (1) + expectedGas := big.NewInt(0).Add(big.NewInt(11), defaultOpts.GasTipCap) + + // We create a fee estimator with a custom tip multiplier + feeEstimator := NewEIP1559FeeEstimator(&mockFeeEthClientImpl{ + blockValue: blockValue, + }, WithEIP1559BaseMultiplier(baseMultiplier)) + + newOpts, err := feeEstimator.EstimateFees(context.Background(), defaultOpts) + require.NoError(t, err) + + // We create a new opts with the expected tip cap added + expectedOpts := *defaultOpts + expectedOpts.GasFeeCap = expectedGas + + // We check that the tip has been added + require.Equal(t, &expectedOpts, newOpts) + + // We make sure that we get a copy of the object to prevent mutating the original + assert.NotSame(t, defaultOpts, newOpts) + }) + }) + }) +} + +var ( + _ EIP1159FeeEthClient = (*mockFeeEthClientImpl)(nil) + + _ types.BlockType = (*mockBlockType)(nil) +) + +type mockFeeEthClientImpl struct { + blockValue *types.Block + blockErr error + + tipCapValue *big.Int + tipCapErr error +} + +func (m *mockFeeEthClientImpl) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) { + return m.blockValue, m.blockErr +} + +func (m *mockFeeEthClientImpl) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { + return m.tipCapValue, m.tipCapErr +} + +type mockBlockType struct{} + +func (m *mockBlockType) HasOptimismWithdrawalsRoot(blkTime uint64) bool { + return false +} + +func (m *mockBlockType) IsIsthmus(blkTime uint64) bool { + return false +} diff --git a/devnet-sdk/system/system.go b/devnet-sdk/system/system.go index 8290a90326..90e94209f9 100644 --- a/devnet-sdk/system/system.go +++ b/devnet-sdk/system/system.go @@ -1,11 +1,15 @@ package system import ( + "context" "fmt" "slices" + "sync" "github.com/ethereum-optimism/optimism/devnet-sdk/descriptors" "github.com/ethereum-optimism/optimism/devnet-sdk/shell/env" + "github.com/ethereum-optimism/optimism/op-service/client" + "github.com/ethereum-optimism/optimism/op-service/sources" ) type system struct { @@ -69,13 +73,23 @@ func systemFromDevnet(dn descriptors.DevnetEnvironment, identifier string) (Syst } if slices.Contains(dn.Features, "interop") { - return &interopSystem{system: sys}, nil + // TODO(14849): this will break as soon as we have a dependency set that + // doesn't include all L2s. + supervisorRPC := dn.L2[0].Services["supervisor"].Endpoints["rpc"] + return &interopSystem{ + system: sys, + supervisorRPC: fmt.Sprintf("http://%s:%d", supervisorRPC.Host, supervisorRPC.Port), + }, nil } return sys, nil } type interopSystem struct { *system + + supervisorRPC string + supervisor Supervisor + mu sync.Mutex } // interopSystem implements InteropSystem @@ -84,3 +98,20 @@ var _ InteropSystem = (*interopSystem)(nil) func (i *interopSystem) InteropSet() InteropSet { return i.system // TODO: the interop set might not contain all L2s } + +func (i *interopSystem) Supervisor(ctx context.Context) (Supervisor, error) { + i.mu.Lock() + defer i.mu.Unlock() + + if i.supervisor != nil { + return i.supervisor, nil + } + + cl, err := client.NewRPC(ctx, nil, i.supervisorRPC) + if err != nil { + return nil, fmt.Errorf("failed to dial supervisor RPC: %w", err) + } + supervisor := sources.NewSupervisorClient(cl) + i.supervisor = supervisor + return supervisor, nil +} diff --git a/devnet-sdk/system/tx.go b/devnet-sdk/system/tx.go index abe32944f7..418e0c90da 100644 --- a/devnet-sdk/system/tx.go +++ b/devnet-sdk/system/tx.go @@ -198,3 +198,22 @@ func (t *EthTx) Type() uint8 { func (t *EthTx) Raw() *types.Transaction { return t.tx } + +// EthReceipt is the default implementation of Receipt that wraps types.Receipt +type EthReceipt struct { + blockNumber *big.Int + logs []*types.Log + txHash common.Hash +} + +func (t *EthReceipt) BlockNumber() *big.Int { + return t.blockNumber +} + +func (t *EthReceipt) Logs() []*types.Log { + return t.logs +} + +func (t *EthReceipt) TxHash() common.Hash { + return t.txHash +} diff --git a/devnet-sdk/system/txbuilder_test.go b/devnet-sdk/system/txbuilder_test.go index 64fcf90a62..cd21e32f17 100644 --- a/devnet-sdk/system/txbuilder_test.go +++ b/devnet-sdk/system/txbuilder_test.go @@ -6,14 +6,16 @@ import ( "math/big" "testing" + "github.com/ethereum-optimism/optimism/devnet-sdk/contracts/bindings" "github.com/ethereum-optimism/optimism/devnet-sdk/descriptors" "github.com/ethereum-optimism/optimism/devnet-sdk/interfaces" "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" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto/kzg4844" - "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/params" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -53,6 +55,16 @@ func (m *mockWallet) SendETH(to types.Address, amount types.Balance) types.Write return args.Get(0).(types.WriteInvocation[any]) } +func (m *mockWallet) InitiateMessage(chainID types.ChainID, target common.Address, message []byte) types.WriteInvocation[any] { + args := m.Called(chainID, target, message) + return args.Get(0).(types.WriteInvocation[any]) +} + +func (m *mockWallet) ExecuteMessage(identifier bindings.Identifier, sentMessage []byte) types.WriteInvocation[any] { + args := m.Called(identifier, sentMessage) + return args.Get(0).(types.WriteInvocation[any]) +} + func (m *mockWallet) Balance() types.Balance { args := m.Called() return args.Get(0).(types.Balance) @@ -104,9 +116,9 @@ func (m *mockChain) RPCURL() string { return args.String(0) } -func (m *mockChain) Client() (*ethclient.Client, error) { +func (m *mockChain) Client() (*sources.EthClient, error) { args := m.Called() - return args.Get(0).(*ethclient.Client), nil + return args.Get(0).(*sources.EthClient), nil } func (m *mockChain) Wallets(ctx context.Context) ([]Wallet, error) { @@ -145,9 +157,9 @@ func (m *mockNode) PendingNonceAt(ctx context.Context, addr common.Address) (uin return args.Get(0).(uint64), args.Error(1) } -func (m *mockNode) BlockByNumber(ctx context.Context, number *big.Int) (*ethtypes.Block, error) { +func (m *mockNode) BlockByNumber(ctx context.Context, number *big.Int) (eth.BlockInfo, error) { args := m.Called(ctx, number) - return args.Get(0).(*ethtypes.Block), args.Error(1) + return args.Get(0).(eth.BlockInfo), args.Error(1) } func TestNewTxBuilder(t *testing.T) { diff --git a/devnet-sdk/system/txprocessor.go b/devnet-sdk/system/txprocessor.go index c6c4438fe6..1279aadffc 100644 --- a/devnet-sdk/system/txprocessor.go +++ b/devnet-sdk/system/txprocessor.go @@ -44,6 +44,8 @@ func (p *transactionProcessor) Sign(tx Transaction) (Transaction, error) { var signer types.Signer switch tx.Type() { + case types.SetCodeTxType: + signer = types.NewIsthmusSigner(p.chainID) case types.DynamicFeeTxType: signer = types.NewLondonSigner(p.chainID) case types.AccessListTxType: diff --git a/devnet-sdk/system/wallet.go b/devnet-sdk/system/wallet.go index d6a9035294..1a70faa618 100644 --- a/devnet-sdk/system/wallet.go +++ b/devnet-sdk/system/wallet.go @@ -4,10 +4,16 @@ import ( "context" "encoding/hex" "fmt" + "math/big" "strings" + "github.com/ethereum-optimism/optimism/devnet-sdk/contracts/bindings" + "github.com/ethereum-optimism/optimism/devnet-sdk/contracts/constants" "github.com/ethereum-optimism/optimism/devnet-sdk/types" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait" + "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/crypto" "github.com/ethereum/go-ethereum/ethclient" @@ -22,7 +28,8 @@ var ( // internalChain provides access to internal chain functionality type internalChain interface { Chain - Client() (*ethclient.Client, error) + Client() (*sources.EthClient, error) + GethClient() (*ethclient.Client, error) } type wallet struct { @@ -97,6 +104,134 @@ func (w *wallet) Balance() types.Balance { return types.NewBalance(balance) } +func (w *wallet) InitiateMessage(chainID types.ChainID, target common.Address, message []byte) types.WriteInvocation[any] { + return &initiateMessageImpl{ + chain: w.chain, + processor: w, + from: w.address, + target: target, + chainID: chainID, + message: message, + } +} + +func (w *wallet) ExecuteMessage(identifier bindings.Identifier, sentMessage []byte) types.WriteInvocation[any] { + return &executeMessageImpl{ + chain: w.chain, + processor: w, + from: w.address, + identifier: identifier, + sentMessage: sentMessage, + } +} + +type initiateMessageImpl struct { + chain internalChain + processor TransactionProcessor + from types.Address + + target types.Address + chainID types.ChainID + message []byte +} + +func (i *initiateMessageImpl) Call(ctx context.Context) (any, error) { + builder := NewTxBuilder(ctx, i.chain) + messenger, err := i.chain.ContractsRegistry().L2ToL2CrossDomainMessenger(constants.L2ToL2CrossDomainMessenger) + if err != nil { + return nil, fmt.Errorf("failed to init transaction: %w", err) + } + data, err := messenger.ABI().Pack("sendMessage", i.chainID, i.target, i.message) + if err != nil { + return nil, fmt.Errorf("failed to build calldata: %w", err) + } + tx, err := builder.BuildTx( + WithFrom(i.from), + WithTo(constants.L2ToL2CrossDomainMessenger), + WithValue(big.NewInt(0)), + WithData(data), + ) + if err != nil { + return nil, fmt.Errorf("failed to build transaction: %w", err) + } + + tx, err = i.processor.Sign(tx) + if err != nil { + return nil, fmt.Errorf("failed to sign transaction: %w", err) + } + + return tx, nil +} + +func (i *initiateMessageImpl) Send(ctx context.Context) types.InvocationResult { + result, err := i.Call(ctx) + if err != nil { + return &sendResult{chain: i.chain, tx: nil, err: err} + } + tx, ok := result.(Transaction) + if !ok { + return &sendResult{chain: i.chain, tx: nil, err: fmt.Errorf("unexpected return type")} + } + err = i.processor.Send(ctx, tx) + return &sendResult{ + chain: i.chain, + tx: tx, + err: err, + } +} + +type executeMessageImpl struct { + chain internalChain + processor TransactionProcessor + from types.Address + + identifier bindings.Identifier + sentMessage []byte +} + +func (i *executeMessageImpl) Call(ctx context.Context) (any, error) { + builder := NewTxBuilder(ctx, i.chain) + messenger, err := i.chain.ContractsRegistry().L2ToL2CrossDomainMessenger(constants.L2ToL2CrossDomainMessenger) + if err != nil { + return nil, fmt.Errorf("failed to init transaction: %w", err) + } + data, err := messenger.ABI().Pack("relayMessage", i.identifier, i.sentMessage) + if err != nil { + return nil, fmt.Errorf("failed to build calldata: %w", err) + } + tx, err := builder.BuildTx( + WithFrom(i.from), + WithTo(constants.L2ToL2CrossDomainMessenger), + WithValue(big.NewInt(0)), + WithData(data), + ) + if err != nil { + return nil, fmt.Errorf("failed to build transaction: %w", err) + } + tx, err = i.processor.Sign(tx) + if err != nil { + return nil, fmt.Errorf("failed to sign transaction: %w", err) + } + return tx, nil +} + +func (i *executeMessageImpl) Send(ctx context.Context) types.InvocationResult { + result, err := i.Call(ctx) + if err != nil { + return &sendResult{chain: i.chain, tx: nil, err: err} + } + tx, ok := result.(Transaction) + if !ok { + return &sendResult{chain: i.chain, tx: nil, err: fmt.Errorf("unexpected return type")} + } + err = i.processor.Send(ctx, tx) + return &sendResult{ + chain: i.chain, + tx: tx, + err: err, + } +} + func (w *wallet) Nonce() uint64 { client, err := w.chain.Client() if err != nil { @@ -125,6 +260,8 @@ func (w *wallet) Sign(tx Transaction) (Transaction, error) { var signer coreTypes.Signer switch tx.Type() { + case coreTypes.SetCodeTxType: + signer = coreTypes.NewIsthmusSigner(w.chain.ID()) case coreTypes.DynamicFeeTxType: signer = coreTypes.NewLondonSigner(w.chain.ID()) case coreTypes.AccessListTxType: @@ -219,9 +356,10 @@ func (i *sendImpl) Send(ctx context.Context) types.InvocationResult { } type sendResult struct { - chain internalChain - tx Transaction - err error + chain internalChain + tx Transaction + receipt Receipt + err error } func (r *sendResult) Error() error { @@ -229,7 +367,7 @@ func (r *sendResult) Error() error { } func (r *sendResult) Wait() error { - client, err := r.chain.Client() + client, err := r.chain.GethClient() if err != nil { return fmt.Errorf("failed to get client: %w", err) } @@ -242,11 +380,11 @@ func (r *sendResult) Wait() error { } if tx, ok := r.tx.(RawTransaction); ok { - receipt, err := bind.WaitMined(context.Background(), client, tx.Raw()) + receipt, err := wait.ForReceiptOK(context.Background(), client, tx.Raw().Hash()) if err != nil { return fmt.Errorf("failed waiting for transaction confirmation: %w", err) } - + r.receipt = &EthReceipt{blockNumber: receipt.BlockNumber, logs: receipt.Logs, txHash: receipt.TxHash} if receipt.Status == 0 { return fmt.Errorf("transaction failed") } @@ -254,3 +392,7 @@ func (r *sendResult) Wait() error { return nil } + +func (r *sendResult) Info() any { + return r.receipt +} diff --git a/devnet-sdk/system/wallet_test.go b/devnet-sdk/system/wallet_test.go index 80a8a27b69..6c1fcfad77 100644 --- a/devnet-sdk/system/wallet_test.go +++ b/devnet-sdk/system/wallet_test.go @@ -6,9 +6,13 @@ import ( "testing" "github.com/ethereum-optimism/optimism/devnet-sdk/types" + "github.com/ethereum-optimism/optimism/op-service/client" + "github.com/ethereum-optimism/optimism/op-service/sources" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rpc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -103,7 +107,15 @@ type internalMockChain struct { *mockChain } -func (m *internalMockChain) Client() (*ethclient.Client, error) { +func (m *internalMockChain) Client() (*sources.EthClient, error) { + args := m.Called() + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*sources.EthClient), args.Error(1) +} + +func (m *internalMockChain) GethClient() (*ethclient.Client, error) { args := m.Called() if args.Get(0) == nil { return nil, args.Error(1) @@ -173,9 +185,12 @@ func TestWallet_SendETH(t *testing.T) { mockNode.On("PendingNonceAt", ctx, fromAddr).Return(uint64(0), nil) // Mock client access - client, err := ethclient.Dial("http://this.domain.definitely.does.not.exist:8545") + rpcClient, err := rpc.DialContext(context.Background(), "http://this.domain.definitely.does.not.exist:8545") + assert.NoError(t, err) + ethClCfg := sources.EthClientConfig{MaxConcurrentRequests: 1, MaxRequestsPerBatch: 1, RPCProviderKind: sources.RPCKindStandard} + ethCl, err := sources.NewEthClient(client.NewBaseRPCClient(rpcClient), log.Root(), nil, ðClCfg) assert.NoError(t, err) - mockChain.On("Client").Return(client, nil) + mockChain.On("Client").Return(ethCl, nil) // Create the send invocation invocation := w.SendETH(toAddr, amount) diff --git a/devnet-sdk/testing/systest/testing_test.go b/devnet-sdk/testing/systest/testing_test.go index aeb31d94ba..86e8418a15 100644 --- a/devnet-sdk/testing/systest/testing_test.go +++ b/devnet-sdk/testing/systest/testing_test.go @@ -12,7 +12,11 @@ import ( "github.com/ethereum-optimism/optimism/devnet-sdk/shell/env" "github.com/ethereum-optimism/optimism/devnet-sdk/system" "github.com/ethereum-optimism/optimism/devnet-sdk/types" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/sources" + supervisorTypes "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/params" "github.com/stretchr/testify/require" @@ -82,7 +86,8 @@ type mockChain struct{} func (m *mockChain) Node() system.Node { return nil } func (m *mockChain) RPCURL() string { return "http://localhost:8545" } -func (m *mockChain) Client() (*ethclient.Client, error) { return ethclient.Dial(m.RPCURL()) } +func (m *mockChain) Client() (*sources.EthClient, error) { return nil, nil } +func (m *mockChain) GethClient() (*ethclient.Client, error) { return nil, nil } func (m *mockChain) ID() types.ChainID { return types.ChainID(big.NewInt(1)) } func (m *mockChain) ContractsRegistry() interfaces.ContractsRegistry { return nil } func (m *mockChain) Wallets(ctx context.Context) ([]system.Wallet, error) { @@ -120,6 +125,49 @@ type mockInteropSet struct{} func (m *mockInteropSet) L2s() []system.Chain { return []system.Chain{&mockChain{}} } +// mockSupervisor implements the system.Supervisor interface for testing +type mockSupervisor struct{} + +func (m *mockSupervisor) LocalUnsafe(ctx context.Context, chainID eth.ChainID) (eth.BlockID, error) { + return eth.BlockID{}, nil +} + +func (m *mockSupervisor) CrossSafe(ctx context.Context, chainID eth.ChainID) (supervisorTypes.DerivedIDPair, error) { + return supervisorTypes.DerivedIDPair{}, nil +} + +func (m *mockSupervisor) Finalized(ctx context.Context, chainID eth.ChainID) (eth.BlockID, error) { + return eth.BlockID{}, nil +} + +func (m *mockSupervisor) FinalizedL1(ctx context.Context) (eth.BlockRef, error) { + return eth.BlockRef{}, nil +} + +func (m *mockSupervisor) CrossDerivedFrom(ctx context.Context, chainID eth.ChainID, blockID eth.BlockID) (eth.BlockRef, error) { + return eth.BlockRef{}, nil +} + +func (m *mockSupervisor) UpdateLocalUnsafe(ctx context.Context, chainID eth.ChainID, blockRef eth.BlockRef) error { + return nil +} + +func (m *mockSupervisor) UpdateLocalSafe(ctx context.Context, chainID eth.ChainID, l1BlockRef eth.L1BlockRef, blockRef eth.BlockRef) error { + return nil +} + +func (m *mockSupervisor) SuperRootAtTimestamp(ctx context.Context, timestamp hexutil.Uint64) (eth.SuperRootResponse, error) { + return eth.SuperRootResponse{}, nil +} + +func (m *mockSupervisor) AllSafeDerivedAt(ctx context.Context, blockID eth.BlockID) (map[eth.ChainID]eth.BlockID, error) { + return nil, nil +} + +func (m *mockSupervisor) SyncStatus(ctx context.Context) (eth.SupervisorSyncStatus, error) { + return eth.SupervisorSyncStatus{}, nil +} + // mockInteropSystem implements a minimal system.InteropSystem for testing type mockInteropSystem struct { mockSystem @@ -127,6 +175,11 @@ type mockInteropSystem struct { func (m *mockInteropSystem) InteropSet() system.InteropSet { return &mockInteropSet{} } +// Supervisor implements the system.InteropSystem interface +func (m *mockInteropSystem) Supervisor(ctx context.Context) (system.Supervisor, error) { + return &mockSupervisor{}, nil +} + // newMockSystem creates a new mock system for testing func newMockSystem() system.System { return &mockSystem{} diff --git a/devnet-sdk/testing/testlib/validators/validators_test.go b/devnet-sdk/testing/testlib/validators/validators_test.go index 8313a64932..fca524c436 100644 --- a/devnet-sdk/testing/testlib/validators/validators_test.go +++ b/devnet-sdk/testing/testlib/validators/validators_test.go @@ -6,19 +6,23 @@ import ( "math/big" "testing" + "github.com/ethereum-optimism/optimism/devnet-sdk/contracts/bindings" "github.com/ethereum-optimism/optimism/devnet-sdk/descriptors" "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-node/rollup" + "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" - ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/params" "github.com/stretchr/testify/require" + + ethtypes "github.com/ethereum/go-ethereum/core/types" ) func Uint64Ptr(x uint64) *uint64 { @@ -238,7 +242,8 @@ type mockChain struct { } func (m *mockChain) RPCURL() string { return "http://localhost:8545" } -func (m *mockChain) Client() (*ethclient.Client, error) { return ethclient.Dial(m.RPCURL()) } +func (m *mockChain) Client() (*sources.EthClient, error) { return nil, nil } +func (m *mockChain) GethClient() (*ethclient.Client, error) { return nil, nil } func (m *mockChain) ID() types.ChainID { return types.ChainID(big.NewInt(1)) } func (m *mockChain) ContractsRegistry() interfaces.ContractsRegistry { return nil } func (m *mockChain) Wallets(ctx context.Context) ([]system.Wallet, error) { @@ -292,21 +297,10 @@ func (m *mockNode) PendingNonceAt(ctx context.Context, addr common.Address) (uin return 0, fmt.Errorf("not implemented") } -func (m *mockNode) BlockByNumber(ctx context.Context, number *big.Int) (*ethtypes.Block, error) { - isIsthmusActivated, err := IsForkActivated(m.chainConfig, rollup.Isthmus, 100) - if err != nil { - return nil, err - } - var blockConfig *ethtypes.BlockConfig - if isIsthmusActivated { - blockConfig = ethtypes.DefaultBlockConfig - } else { - blockConfig = ethtypes.IsthmusBlockConfig - } - - return ethtypes.NewBlock(ðtypes.Header{ - Time: 100, - }, ðtypes.Body{Withdrawals: []*ethtypes.Withdrawal{}}, nil, nil, blockConfig), nil +func (m *mockNode) BlockByNumber(ctx context.Context, number *big.Int) (eth.BlockInfo, error) { + header := ethtypes.Header{Time: 100} + info := eth.HeaderBlockInfo(&header) + return info, nil } type mockWallet struct { @@ -331,6 +325,14 @@ func (m mockWallet) SendETH(to types.Address, amount types.Balance) types.WriteI panic("not implemented") } +func (m mockWallet) InitiateMessage(chainID types.ChainID, target common.Address, message []byte) types.WriteInvocation[any] { + panic("not implemented") +} + +func (m mockWallet) ExecuteMessage(identifier bindings.Identifier, sentMessage []byte) types.WriteInvocation[any] { + panic("not implemented") +} + func (m mockWallet) Nonce() uint64 { return 0 } diff --git a/devnet-sdk/types/types.go b/devnet-sdk/types/types.go index aefea07641..e251710ae7 100644 --- a/devnet-sdk/types/types.go +++ b/devnet-sdk/types/types.go @@ -24,6 +24,7 @@ type WriteInvocation[T any] interface { type InvocationResult interface { Error() error Wait() error + Info() any } type Key = *ecdsa.PrivateKey diff --git a/go.mod b/go.mod index 993b5b6cb2..41d79b657b 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,7 @@ module github.com/ethereum-optimism/optimism go 1.22.0 - -toolchain go1.22.7 +toolchain go1.24.1 require ( github.com/BurntSushi/toml v1.4.0 @@ -14,14 +13,15 @@ require ( github.com/consensys/gnark-crypto v0.16.0 github.com/crate-crypto/go-kzg-4844 v1.1.0 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 + github.com/docker/docker v27.5.1+incompatible github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.3 - github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20250228185245-d4bb112dc979 + github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20250314162817-2c60e5723c64 github.com/ethereum/go-ethereum v1.15.3 github.com/fatih/color v1.18.0 github.com/fsnotify/fsnotify v1.8.0 github.com/go-task/slim-sprig/v3 v3.0.0 github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb - github.com/google/go-cmp v0.6.0 + github.com/google/go-cmp v0.7.0 github.com/google/gofuzz v1.2.1-0.20220503160820-4a35382e8fc8 github.com/google/uuid v1.6.0 github.com/hashicorp/go-multierror v1.1.1 @@ -47,7 +47,7 @@ require ( github.com/olekukonko/tablewriter v0.0.5 github.com/pkg/errors v0.9.1 github.com/pkg/profile v1.7.0 - github.com/prometheus/client_golang v1.20.5 + github.com/prometheus/client_golang v1.21.1 github.com/protolambda/ctxlock v0.1.0 github.com/schollz/progressbar/v3 v3.18.0 github.com/stretchr/testify v1.10.0 @@ -55,7 +55,7 @@ require ( golang.org/x/crypto v0.32.0 golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c golang.org/x/sync v0.10.0 - golang.org/x/term v0.28.0 + golang.org/x/term v0.31.0 golang.org/x/time v0.10.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -83,6 +83,7 @@ require ( github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect github.com/consensys/bavard v0.1.27 // indirect github.com/containerd/cgroups v1.1.0 // indirect + github.com/containerd/log v0.1.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect @@ -92,7 +93,9 @@ require ( github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect github.com/deepmap/oapi-codegen v1.8.2 // indirect github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de // indirect + github.com/distribution/reference v0.6.0 // indirect github.com/dlclark/regexp2 v1.7.0 // indirect + github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dop251/goja v0.0.0-20230806174421-c933cf95e127 // indirect github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect @@ -101,6 +104,7 @@ require ( github.com/ethereum/c-kzg-4844 v1.0.0 // indirect github.com/ethereum/go-verkle v0.2.2 // indirect github.com/felixge/fgprof v0.9.5 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/ferranbt/fastssz v0.1.2 // indirect github.com/flynn/noise v1.1.0 // indirect github.com/francoispqt/gojay v1.2.13 // indirect @@ -108,6 +112,8 @@ require ( github.com/getsentry/sentry-go v0.27.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/go-ini/ini v1.67.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/go-yaml/yaml v2.1.0+incompatible // indirect @@ -116,7 +122,6 @@ require ( github.com/gofrs/flock v0.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.1 // indirect - github.com/golang/protobuf v1.5.4 // indirect github.com/google/gopacket v1.1.19 // indirect github.com/google/pprof v0.0.0-20241009165004-a3522334989c // indirect github.com/gorilla/websocket v1.5.3 // indirect @@ -171,6 +176,9 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/pointerstructure v1.2.1 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/morikuni/aec v1.0.0 // indirect github.com/mr-tron/base58 v1.2.0 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect @@ -184,6 +192,8 @@ require ( github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416 // indirect github.com/nwaples/rardecode v1.1.3 // indirect github.com/onsi/ginkgo/v2 v2.20.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opencontainers/runtime-spec v1.2.0 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect @@ -209,14 +219,14 @@ require ( github.com/pion/webrtc/v3 v3.3.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/quic-go v0.46.0 // indirect github.com/quic-go/webtransport-go v0.8.0 // indirect github.com/raulk/go-watchdog v1.3.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/rs/cors v1.11.0 // indirect github.com/rs/xid v1.6.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect @@ -235,6 +245,13 @@ require ( github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect go.etcd.io/bbolt v1.3.5 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect + go.opentelemetry.io/otel v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/sdk v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect go.uber.org/dig v1.18.0 // indirect go.uber.org/fx v1.22.2 // indirect go.uber.org/mock v0.4.0 // indirect @@ -242,21 +259,21 @@ require ( go.uber.org/zap v1.27.0 // indirect golang.org/x/mod v0.22.0 // indirect golang.org/x/net v0.34.0 // indirect - golang.org/x/sys v0.29.0 // indirect + golang.org/x/sys v0.32.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/tools v0.29.0 // indirect - google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230803162519-f966b187b2e5 // indirect - google.golang.org/grpc v1.57.1 // indirect - google.golang.org/protobuf v1.34.2 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect + google.golang.org/grpc v1.69.4 // indirect + google.golang.org/protobuf v1.36.3 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + gotest.tools/v3 v3.5.2 // indirect lukechampine.com/blake3 v1.3.0 // indirect rsc.io/tmplfunc v0.0.3 // indirect ) -replace github.com/ethereum/go-ethereum => github.com/ethereum-optimism/op-geth v1.101503.0-rc.1 +replace github.com/ethereum/go-ethereum => github.com/ethereum-optimism/op-geth v1.101503.2-rc.1 //replace github.com/ethereum/go-ethereum => ../op-geth diff --git a/go.sum b/go.sum index 7f9ad6f0c9..57414a5d3d 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,8 @@ dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= @@ -84,6 +86,8 @@ github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= @@ -131,6 +135,8 @@ github.com/consensys/gnark-crypto v0.16.0/go.mod h1:Ke3j06ndtPTVvo++PhGNgvm+lgpL github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= @@ -170,9 +176,15 @@ github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de h1:t0UHb5vdo github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/docker v27.5.1+incompatible h1:4PYU5dnBYqRQi0294d1FBECqT9ECWeQAIfE8q4YnPY8= +github.com/docker/docker v27.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= @@ -192,10 +204,10 @@ github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/u github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.3 h1:RWHKLhCrQThMfch+QJ1Z8veEq5ZO3DfIhZ7xgRP9WTc= github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.3/go.mod h1:QziizLAiF0KqyLdNJYD7O5cpDlaFMNZzlxYNcWsJUxs= -github.com/ethereum-optimism/op-geth v1.101503.0-rc.1 h1:8qSXpCfLvLIPlkmwO4J+J9chi9NgZlhRckVJEjxS5Lg= -github.com/ethereum-optimism/op-geth v1.101503.0-rc.1/go.mod h1:QUo3fn+45vWqJWzJW+rIzRHUV7NmhhHLPdI87mAn1M8= -github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20250228185245-d4bb112dc979 h1:P37l7EFCz5KxE20+yPa3LWiH2Cg+xx6lQ4mOKYCZFPs= -github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20250228185245-d4bb112dc979/go.mod h1:NZ816PzLU1TLv1RdAvYAb6KWOj4Zm5aInT0YpDVml2Y= +github.com/ethereum-optimism/op-geth v1.101503.2-rc.1 h1:a//Sc0necznugHkwS4gey4uS9qDoSk8g3TazLDVnQ/8= +github.com/ethereum-optimism/op-geth v1.101503.2-rc.1/go.mod h1:QUo3fn+45vWqJWzJW+rIzRHUV7NmhhHLPdI87mAn1M8= +github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20250314162817-2c60e5723c64 h1:teDhU4h4ryaE8rSBl+vJJiwKHjxdnnHPkKZ9iNr2R8k= +github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20250314162817-2c60e5723c64/go.mod h1:NZ816PzLU1TLv1RdAvYAb6KWOj4Zm5aInT0YpDVml2Y= github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA= github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= @@ -206,6 +218,8 @@ github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/ github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/ferranbt/fastssz v0.1.2 h1:Dky6dXlngF6Qjc+EfDipAkE83N5I5DE68bY6O0VLNPk= github.com/ferranbt/fastssz v0.1.2/go.mod h1:X5UPrE2u1UJjxHA8X54u04SBwdAQjG2sFtWs39YxyWs= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= @@ -241,8 +255,11 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= @@ -304,8 +321,8 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -337,7 +354,10 @@ github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY41SORZyNJ0= github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.5.0 h1:WcmKMm43DR7RdtlkEXQJyo5ws8iTp98CyhCCbOHMvNI= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -557,10 +577,16 @@ github.com/mitchellh/pointerstructure v1.2.1/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8oh github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= @@ -622,6 +648,10 @@ github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAl github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= @@ -701,8 +731,8 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= -github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= +github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -714,8 +744,8 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= -github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= @@ -743,8 +773,8 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= @@ -852,6 +882,26 @@ github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQ go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= +go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw= @@ -1023,8 +1073,8 @@ golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1033,8 +1083,8 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= -golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= -golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -1093,18 +1143,16 @@ google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e h1:xIXmWJ303kJCuogpj0bHq+dcjcZHU+XFyc1I0Yl9cRg= -google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e/go.mod h1:0ggbjUrZYpy1q+ANUS30SEoGZ53cdfwtbuG7Ptgy108= -google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw= -google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230803162519-f966b187b2e5 h1:eSaPbMR4T7WfH9FvABk36NBMacoTUKdWCvV0dx+KfOg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230803162519-f966b187b2e5/go.mod h1:zBEcrKX2ZOcEkHWxBPAIvYUWOKKMIhYcmNiUIu2ji3I= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.57.1 h1:upNTNqv0ES+2ZOOqACwVtS3Il8M12/+Hz41RCPzAjQg= -google.golang.org/grpc v1.57.1/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= +google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= +google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1113,8 +1161,8 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1140,6 +1188,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/kurtosis-devnet/fileserver/main.star b/kurtosis-devnet/fileserver/main.star index c26357f1a3..94b5dff887 100644 --- a/kurtosis-devnet/fileserver/main.star +++ b/kurtosis-devnet/fileserver/main.star @@ -12,18 +12,22 @@ def get_used_ports(): return used_ports -def run(plan, source_path): +def run(plan, source_path, server_image=FILESERVER_IMAGE): service_name = "fileserver" config = get_fileserver_config( - plan, - service_name, - source_path, + plan = plan, + service_name = service_name, + source_path = source_path, + server_image = server_image, + ) + plan.add_service( + name = service_name, + config = config, ) - service = plan.add_service(service_name, config) return service_name -def get_fileserver_config(plan, service_name, source_path): +def get_fileserver_config(plan, service_name, source_path, server_image): files = {} # Upload content to container @@ -42,7 +46,7 @@ def get_fileserver_config(plan, service_name, source_path): ports = get_used_ports() return ServiceConfig( - image=FILESERVER_IMAGE, + image=server_image, ports=ports, cmd=["nginx", "-g", "daemon off;"], files=files, diff --git a/kurtosis-devnet/interop.yaml b/kurtosis-devnet/interop.yaml index 97899b4084..0a0b4d08b5 100644 --- a/kurtosis-devnet/interop.yaml +++ b/kurtosis-devnet/interop.yaml @@ -37,7 +37,6 @@ optimism_package: } extra_params: - {{ $flags.log_level }} - network: "kurtosis" chains: - participants: - el_type: op-geth diff --git a/kurtosis-devnet/justfile b/kurtosis-devnet/justfile index 8404bf4ebf..5f8866331e 100644 --- a/kurtosis-devnet/justfile +++ b/kurtosis-devnet/justfile @@ -1,5 +1,7 @@ set shell := ["/bin/bash", "-c"] +KURTOSIS_PACKAGE := "./optimism-package-trampoline/" + test: go test --tags=testonly ./... @@ -68,10 +70,8 @@ op-proposer-image TAG='op-proposer:devnet': (_docker_build_stack TAG "op-propose op-supervisor-image TAG='op-supervisor:devnet': (_docker_build_stack TAG "op-supervisor-target") op-wheel-image TAG='op-wheel:devnet': (_docker_build_stack TAG "op-wheel-target") -KURTOSIS_PACKAGE := "github.com/ethpandaops/optimism-package" - # Devnet template recipe -devnet TEMPLATE_FILE DATA_FILE="" NAME="": +devnet TEMPLATE_FILE DATA_FILE="" NAME="" PACKAGE=KURTOSIS_PACKAGE: #!/usr/bin/env bash export DEVNET_NAME={{NAME}} if [ -z "{{NAME}}" ]; then @@ -82,7 +82,7 @@ devnet TEMPLATE_FILE DATA_FILE="" NAME="": fi fi export ENCL_NAME="$DEVNET_NAME"-devnet - go run cmd/main.go -kurtosis-package {{KURTOSIS_PACKAGE}} \ + go run cmd/main.go -kurtosis-package {{PACKAGE}} \ -environment "tests/$ENCL_NAME.json" \ -template "{{TEMPLATE_FILE}}" \ -data "{{DATA_FILE}}" \ diff --git a/kurtosis-devnet/op-program-svc/Dockerfile b/kurtosis-devnet/op-program-svc/Dockerfile index f23e8ec51c..1cf559a761 100644 --- a/kurtosis-devnet/op-program-svc/Dockerfile +++ b/kurtosis-devnet/op-program-svc/Dockerfile @@ -1,4 +1,4 @@ -ARG BASE_VERSION=latest +ARG BASE_IMAGE=op-program-base:latest FROM golang:1.22.7-alpine3.20 AS builder @@ -9,7 +9,7 @@ RUN go mod init op-program-svc RUN go build -o op-program-svc . -FROM op-program-base:${BASE_VERSION} AS svc +FROM ${BASE_IMAGE} AS svc ARG GIT_COMMIT ARG GIT_DATE diff --git a/kurtosis-devnet/op-program-svc/README.md b/kurtosis-devnet/op-program-svc/README.md index 4e59ff1a19..5ffca686b0 100644 --- a/kurtosis-devnet/op-program-svc/README.md +++ b/kurtosis-devnet/op-program-svc/README.md @@ -1,4 +1,24 @@ -# Trigger new build: +# op-program-svc + +This small service is a temporary measure until we come up with a better way +of generating/serving prestate files based on chain information. + +# API + +The API is intentionally extremely simple: +- `POST /`: generate new prestates from provided inputs +- `GET /HASH.(bin.gz|json)`: get prestate data +- `GET /info.json`: get prestates mapping + +The idea is for this service to be basically a function +(chains_specs, deptsets) -> prestates. + +In the future, we definitely want to replace the implementation of that +function (see implementation notes below) + +## Trigger new build: + +Example using curl ``` $ curl -X POST -H "Content-Type: multipart/form-data" \ @@ -9,3 +29,30 @@ $ curl -X POST -H "Content-Type: multipart/form-data" \ -F "files[]=@depsets.json" \ http://localhost:8080 ``` + +## Retrieve prestates mapping + +``` +$ curl -q http://localhost:8080/info.json +{ + "prestate": "0x03f4b7435fec731578c72635d8e8180f7b48703073d038fc7f8c494eeed1ce19", + "prestate_interop": "0x034731331d519c93fc0562643e0728c43f8e45a0af1160ad4c57c4e5141d2bbb", + "prestate_mt64": "0x0325bb0ca8521b468bb8234d8ba54b1b74db60e2b5bc75d0077a0fe2098b6b45" +} +``` + +## Implementation notes + +Unfortunately, op-program-client relies on embedded (using `//go:embed`) +configuration files to store unannounced chain configs. + +This means that in the context of devnets, we need to store the configs +(which are available only mid-deployment) into the **source tree** and +trigger a late build step. + +So effectively, we need to package the relevant part of the sources into +a container, deploy that one alongside the devnet, and run that build step +on demand. + +This is ugly, unsafe, easy to run a DOS against,... we need to do better. +But for now this is what we have. \ No newline at end of file diff --git a/kurtosis-devnet/op-program-svc/build.go b/kurtosis-devnet/op-program-svc/build.go index f4e3582ab3..7da7a4a23d 100644 --- a/kurtosis-devnet/op-program-svc/build.go +++ b/kurtosis-devnet/op-program-svc/build.go @@ -147,6 +147,7 @@ func (b *Builder) ExecuteBuild() ([]byte, error) { return output.Bytes(), nil } +// This is a convenience hack to natively support the file format of op-deployer func (b *Builder) normalizeFilename(filename string) string { // Get just the filename without directories filename = filepath.Base(filename) @@ -156,8 +157,15 @@ func (b *Builder) normalizeFilename(filename string) string { if numStr := strings.TrimSuffix(parts[1], ".json"); numStr != parts[1] { // Check if the number part is actually numeric if _, err := strconv.Atoi(numStr); err == nil { - // It matches the pattern and has a valid number, reorder to NUMBER-PREFIX.json - return numStr + "-" + parts[0] + ".json" + // Handle specific cases + switch parts[0] { + case "genesis": + return fmt.Sprintf("%s-genesis-l2.json", numStr) + case "rollup": + return fmt.Sprintf("%s-rollup.json", numStr) + + } + // For all other cases, leave the filename unchanged } } } diff --git a/kurtosis-devnet/op-program-svc/build_test.go b/kurtosis-devnet/op-program-svc/build_test.go index df3b6f879f..70a813f1f3 100644 --- a/kurtosis-devnet/op-program-svc/build_test.go +++ b/kurtosis-devnet/op-program-svc/build_test.go @@ -167,9 +167,19 @@ func TestNormalizeFilename(t *testing.T) { expected string }{ { - name: "standard format", + name: "standard format - unchanged", input: "prefix-123.json", - expected: "123-prefix.json", + expected: "prefix-123.json", + }, + { + name: "genesis format", + input: "genesis-123.json", + expected: "123-genesis-l2.json", + }, + { + name: "rollup format", + input: "rollup-456.json", + expected: "456-rollup.json", }, { name: "no number", diff --git a/kurtosis-devnet/op-program-svc/defaults.go b/kurtosis-devnet/op-program-svc/defaults.go index aba44060ae..baf756a0d4 100644 --- a/kurtosis-devnet/op-program-svc/defaults.go +++ b/kurtosis-devnet/op-program-svc/defaults.go @@ -48,6 +48,10 @@ func (fs *DefaultFileSystem) Join(elem ...string) string { return filepath.Join(elem...) } +func (fs *DefaultFileSystem) Stat(name string) (fs.FileInfo, error) { + return os.Stat(name) +} + // commandWrapper wraps exec.Cmd to implement CommandRunner type commandWrapper struct { *exec.Cmd diff --git a/kurtosis-devnet/op-program-svc/fs.go b/kurtosis-devnet/op-program-svc/fs.go index ca7b04f132..c79ef00773 100644 --- a/kurtosis-devnet/op-program-svc/fs.go +++ b/kurtosis-devnet/op-program-svc/fs.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "encoding/json" "errors" "fmt" @@ -16,6 +17,11 @@ import ( "time" ) +const ( + depsetsFilename = "depsets.json" + infoFilename = "info.json" +) + // proofFileSystem implements http.FileSystem, mapping hash-based virtual paths to actual files type proofFileSystem struct { root string @@ -29,6 +35,53 @@ type proofFile struct { file File } +// infoFile implements http.File for the virtual info.json file +type infoFile struct { + *bytes.Reader + content []byte +} + +func newInfoFile(proofFiles map[string]string) *infoFile { + // Create inverted map + invertedMap := make(map[string]string) + for hash, variablePart := range proofFiles { + // Replace dashes with underscores in the key + key := fmt.Sprintf("prestate%s", variablePart) + key = strings.ReplaceAll(key, "-", "_") + invertedMap[key] = hash + } + + // Convert to JSON + content, err := json.MarshalIndent(invertedMap, "", " ") + if err != nil { + // Fallback to empty JSON object if marshaling fails + content = []byte("{}") + } + + return &infoFile{ + Reader: bytes.NewReader(content), + content: content, + } +} + +func (f *infoFile) Close() error { + return nil +} + +func (f *infoFile) Readdir(count int) ([]fs.FileInfo, error) { + return nil, fmt.Errorf("not a directory") +} + +func (f *infoFile) Stat() (fs.FileInfo, error) { + return virtualFileInfo{ + name: infoFilename, + size: int64(len(f.content)), + mode: 0644, + modTime: time.Now(), + isDir: false, + }, nil +} + func (f *proofFile) Close() error { return f.file.Close() } @@ -72,8 +125,11 @@ func (d *proofDir) Readdir(count int) ([]fs.FileInfo, error) { d.proofMutex.RLock() defer d.proofMutex.RUnlock() + // Calculate total number of entries + totalEntries := len(d.proofFiles)*2 + 1 // hash.json, hash.bin.gz files + info.json + // If we've already read all entries - if d.pos >= len(d.proofFiles)*2 { + if d.pos >= totalEntries { if count <= 0 { return nil, nil } @@ -89,11 +145,23 @@ func (d *proofDir) Readdir(count int) ([]fs.FileInfo, error) { start := d.pos end := start + count - if count <= 0 || end > len(d.proofFiles)*2 { - end = len(d.proofFiles) * 2 + if count <= 0 || end > totalEntries { + end = totalEntries } for i := start; i < end; i++ { + // Special case for info.json (second to last entry) + if i == len(d.proofFiles)*2 { + entries = append(entries, virtualFileInfo{ + name: infoFilename, + size: 0, // Size will be determined when actually opening the file + mode: 0644, + modTime: time.Now(), + isDir: false, + }) + continue + } + hash := hashes[i/2] isJSON := i%2 == 0 @@ -167,6 +235,13 @@ func (fs *proofFileSystem) Open(name string) (http.File, error) { // Clean the path and remove leading slash name = strings.TrimPrefix(filepath.Clean(name), "/") + // Special case for info.json + if name == infoFilename { + fs.proofMutex.RLock() + defer fs.proofMutex.RUnlock() + return newInfoFile(fs.proofFiles), nil + } + fs.proofMutex.RLock() defer fs.proofMutex.RUnlock() @@ -212,12 +287,15 @@ func (fs *proofFileSystem) scanProofFiles() error { proofRegexp := regexp.MustCompile(`^prestate-proof(.*)\.json$`) for _, entry := range entries { + log.Printf("entry: %s", entry.Name()) if entry.IsDir() { + log.Printf("entry is a directory: %s", entry.Name()) continue } matches := proofRegexp.FindStringSubmatch(entry.Name()) if matches == nil { + log.Printf("Warning: ignoring non-proof file %s", entry.Name()) continue } diff --git a/kurtosis-devnet/op-program-svc/fs_test.go b/kurtosis-devnet/op-program-svc/fs_test.go index 3f61b1a3df..67a1f204df 100644 --- a/kurtosis-devnet/op-program-svc/fs_test.go +++ b/kurtosis-devnet/op-program-svc/fs_test.go @@ -78,6 +78,33 @@ func TestProofFileSystem(t *testing.T) { } }) + t.Run("OpenInfoJSONFile", func(t *testing.T) { + file, err := pfs.Open("/info.json") + if err != nil { + t.Errorf("Failed to open info.json file: %v", err) + } + defer file.Close() + + // Read contents + contents, err := io.ReadAll(file) + if err != nil { + t.Errorf("Failed to read file contents: %v", err) + } + + // Verify the contents contain the inverted map + var infoData map[string]string + err = json.Unmarshal(contents, &infoData) + if err != nil { + t.Errorf("Failed to parse info.json contents: %v", err) + } + + // Check that the key has dashes replaced with underscores + expectedKey := "prestate_test" + if hash, ok := infoData[expectedKey]; !ok || hash != "hash123" { + t.Errorf("Expected info.json to contain mapping from %s to hash123, got %v", expectedKey, hash) + } + }) + t.Run("OpenNonExistentFile", func(t *testing.T) { _, err := pfs.Open("/nonexistent.json") if err == nil { @@ -97,8 +124,17 @@ func TestProofFileSystem(t *testing.T) { t.Errorf("Failed to read directory: %v", err) } - if len(files) != 2 { // We expect both .json and .bin.gz files - t.Errorf("Expected 2 files, got %d", len(files)) + // We expect both .json and .bin.gz files for hash123, plus info.json + if len(files) != 3 { + t.Errorf("Expected 3 files, got %d", len(files)) + } + + // Verify info.json is included in the directory listing + for _, file := range files { + if file.Name() == "info.json" { + return + } } + t.Error("info.json not found in directory listing") }) } diff --git a/kurtosis-devnet/op-program-svc/interfaces.go b/kurtosis-devnet/op-program-svc/interfaces.go index 701890459c..0d93ecb834 100644 --- a/kurtosis-devnet/op-program-svc/interfaces.go +++ b/kurtosis-devnet/op-program-svc/interfaces.go @@ -20,6 +20,7 @@ type FS interface { ReadDir(name string) ([]fs.DirEntry, error) ReadFile(name string) ([]byte, error) Join(elem ...string) string + Stat(name string) (fs.FileInfo, error) // Additional FileSystem operations MkdirAll(path string, perm os.FileMode) error diff --git a/kurtosis-devnet/op-program-svc/main.go b/kurtosis-devnet/op-program-svc/main.go index 53375f594c..d2cc28f4b8 100644 --- a/kurtosis-devnet/op-program-svc/main.go +++ b/kurtosis-devnet/op-program-svc/main.go @@ -21,9 +21,18 @@ func main() { srv := createServer() + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + http.FileServer(srv.proofFS).ServeHTTP(w, r) + case http.MethodPost: + srv.handleUpload(w, r) + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } + }) // Set up routes - http.HandleFunc("/", srv.handleUpload) - http.Handle("/proofs/", http.StripPrefix("/proofs/", http.FileServer(srv.proofFS))) + http.HandleFunc("/", handler) log.Printf("Starting server on :%d with:", srv.port) log.Printf(" app-root: %s", srv.appRoot) diff --git a/kurtosis-devnet/op-program-svc/mocks.go b/kurtosis-devnet/op-program-svc/mocks.go index 6d52f31009..8fa108cb0c 100644 --- a/kurtosis-devnet/op-program-svc/mocks.go +++ b/kurtosis-devnet/op-program-svc/mocks.go @@ -67,21 +67,23 @@ func (m *MockFile) Readdir(count int) ([]fs.FileInfo, error) { return nil, fmt.Errorf("not implemented") } -// MockFS implements both FS and FileSystem interfaces for testing +// MockFS implements FS interface for testing type MockFS struct { - Files map[string]*MockFile - MkdirCalls []string - CreateCalls []string - JoinCalls [][]string - ShouldFail bool + Files map[string]*MockFile + ShouldFail bool + StatFailPaths map[string]bool // Paths that should fail for Stat + JoinCalls [][]string + MkdirCalls []string + CreateCalls []string } func NewMockFS() *MockFS { return &MockFS{ - Files: make(map[string]*MockFile), - MkdirCalls: make([]string, 0), - CreateCalls: make([]string, 0), - JoinCalls: make([][]string, 0), + Files: make(map[string]*MockFile), + StatFailPaths: make(map[string]bool), + JoinCalls: make([][]string, 0), + MkdirCalls: make([]string, 0), + CreateCalls: make([]string, 0), } } @@ -142,6 +144,16 @@ func (m *MockFS) Join(elem ...string) string { return filepath.Join(elem...) } +func (m *MockFS) Stat(name string) (fs.FileInfo, error) { + if m.ShouldFail { + return nil, fmt.Errorf("mock stat error") + } + if m.StatFailPaths[name] { + return nil, fmt.Errorf("file not found: %s", name) + } + return m.Files[name], nil +} + // MockWriteCloser implements io.WriteCloser for testing type MockWriteCloser struct { bytes.Buffer diff --git a/kurtosis-devnet/op-program-svc/server.go b/kurtosis-devnet/op-program-svc/server.go index 401e92ff9e..ad8e16a15b 100644 --- a/kurtosis-devnet/op-program-svc/server.go +++ b/kurtosis-devnet/op-program-svc/server.go @@ -77,7 +77,7 @@ func (s *server) handleUpload(w http.ResponseWriter, r *http.Request) { // Check if we need to rebuild if currentHash == s.lastBuildHash { log.Printf("Hash matches last build, skipping") - w.WriteHeader(http.StatusOK) + w.WriteHeader(http.StatusNotModified) fmt.Fprintf(w, "Files unchanged, skipping build") return } @@ -111,8 +111,8 @@ func (s *server) handleUpload(w http.ResponseWriter, r *http.Request) { // Update the last successful build hash s.lastBuildHash = currentHash - // Redirect to the proofs endpoint - http.Redirect(w, r, "/proofs", http.StatusSeeOther) + log.Printf("Build successful, last build hash: %s", currentHash) + w.WriteHeader(http.StatusOK) } func (s *server) processMultipartForm(r *http.Request) ([]*multipart.FileHeader, string, error) { diff --git a/kurtosis-devnet/op-program-svc/server_test.go b/kurtosis-devnet/op-program-svc/server_test.go index f66cea90bd..5831e37cfa 100644 --- a/kurtosis-devnet/op-program-svc/server_test.go +++ b/kurtosis-devnet/op-program-svc/server_test.go @@ -108,12 +108,9 @@ func TestHandleUpload_Success(t *testing.T) { w := httptest.NewRecorder() srv.handleUpload(w, req) - if w.Code != http.StatusSeeOther { - t.Errorf("Expected status code %d, got %d", http.StatusSeeOther, w.Code) - } - - if location := w.Header().Get("Location"); location != "/proofs" { - t.Errorf("Expected redirect to /proofs, got %s", location) + // We now expect 200 OK instead of a redirect + if w.Code != http.StatusOK { + t.Errorf("Expected status code %d, got %d", http.StatusOK, w.Code) } } @@ -206,9 +203,9 @@ func TestHandleUpload_ScanError(t *testing.T) { w := httptest.NewRecorder() srv.handleUpload(w, req) - // Even with scan error, we should still redirect - if w.Code != http.StatusSeeOther { - t.Errorf("Expected status code %d, got %d", http.StatusSeeOther, w.Code) + // Even with scan error, we should still return 200 OK + if w.Code != http.StatusOK { + t.Errorf("Expected status code %d, got %d", http.StatusOK, w.Code) } } @@ -229,8 +226,9 @@ func TestHandleUpload_UnchangedFiles(t *testing.T) { w1 := httptest.NewRecorder() srv.handleUpload(w1, req1) - if w1.Code != http.StatusSeeOther { - t.Errorf("Expected status code %d, got %d", http.StatusSeeOther, w1.Code) + // First request should return 200 OK + if w1.Code != http.StatusOK { + t.Errorf("Expected status code %d, got %d", http.StatusOK, w1.Code) } // Second request with same files @@ -242,8 +240,9 @@ func TestHandleUpload_UnchangedFiles(t *testing.T) { w2 := httptest.NewRecorder() srv.handleUpload(w2, req2) - if w2.Code != http.StatusOK { - t.Errorf("Expected status code %d, got %d", http.StatusOK, w2.Code) + // Second request with unchanged files should return 304 Not Modified + if w2.Code != http.StatusNotModified { + t.Errorf("Expected status code %d, got %d", http.StatusNotModified, w2.Code) } if !strings.Contains(w2.Body.String(), "Files unchanged") { diff --git a/kurtosis-devnet/optimism-package-trampoline/README.md b/kurtosis-devnet/optimism-package-trampoline/README.md new file mode 100644 index 0000000000..41853ea712 --- /dev/null +++ b/kurtosis-devnet/optimism-package-trampoline/README.md @@ -0,0 +1,7 @@ +This package contains a mostly empty Kurtosis package, +that trampolines to github.com/ethpandas/optimism-package. + +The goal here is to use the `replace` section of `kurtosis.yml` +as a "lockfile" for our package dependencies. + +This way, we achieve reproducibility for our devnet deployments. \ No newline at end of file diff --git a/kurtosis-devnet/optimism-package-trampoline/kurtosis.yml b/kurtosis-devnet/optimism-package-trampoline/kurtosis.yml new file mode 100644 index 0000000000..9b88582613 --- /dev/null +++ b/kurtosis-devnet/optimism-package-trampoline/kurtosis.yml @@ -0,0 +1,8 @@ +name: github.com/ethereum-optimism/optimism/kurtosis-devnet/optimism-package-trampoline +description: |- + A trampoline package for optimism-package. This one is reproducible, due to the replace directives below. +replace: + github.com/ethpandaops/optimism-package: github.com/ethpandaops/optimism-package@0b7814012ba6b0a60ea3cf23f9e54a4cc763aa43 + github.com/ethpandaops/ethereum-package: github.com/ethpandaops/ethereum-package@4.5.0 + github.com/kurtosis-tech/prometheus-package: github.com/kurtosis-tech/prometheus-package@f5ce159aec728898e3deb827f6b921f8ecfc527f + github.com/kurtosis-tech/postgres-package: github.com/kurtosis-tech/postgres-package@2d363be1bc42524f6b0575cac0bbc0fd194ae173 diff --git a/kurtosis-devnet/optimism-package-trampoline/main.star b/kurtosis-devnet/optimism-package-trampoline/main.star new file mode 100644 index 0000000000..d1b2d34b03 --- /dev/null +++ b/kurtosis-devnet/optimism-package-trampoline/main.star @@ -0,0 +1,5 @@ +optimism_package = import_module("github.com/ethpandaops/optimism-package/main.star") + +def run(plan, args): + # just delegate to optimism-package + optimism_package.run(plan, args) diff --git a/kurtosis-devnet/pkg/build/docker.go b/kurtosis-devnet/pkg/build/docker.go index bbfb1ddca5..59786af539 100644 --- a/kurtosis-devnet/pkg/build/docker.go +++ b/kurtosis-devnet/pkg/build/docker.go @@ -2,12 +2,89 @@ package build import ( "bytes" + "context" + "errors" "fmt" "log" + "net/url" + "os" "os/exec" + "strings" "text/template" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" ) +// cmdRunner abstracts command execution for testing +type cmdRunner interface { + CombinedOutput() ([]byte, error) +} + +// defaultCmdRunner is the default implementation that uses exec.Command +type defaultCmdRunner struct { + *exec.Cmd +} + +func (r *defaultCmdRunner) CombinedOutput() ([]byte, error) { + return r.Cmd.CombinedOutput() +} + +// cmdFactory creates commands +type cmdFactory func(name string, arg ...string) cmdRunner + +// defaultCmdFactory is the default implementation that uses exec.Command +func defaultCmdFactory(name string, arg ...string) cmdRunner { + return &defaultCmdRunner{exec.Command(name, arg...)} +} + +// dockerClient interface defines the Docker client methods we use +type dockerClient interface { + ImageInspectWithRaw(ctx context.Context, imageID string) (types.ImageInspect, []byte, error) + ImageTag(ctx context.Context, source, target string) error +} + +// dockerProvider abstracts the creation of Docker clients +type dockerProvider interface { + newClient() (dockerClient, error) +} + +// defaultDockerProvider is the default implementation of dockerProvider +type defaultDockerProvider struct{} + +func (p *defaultDockerProvider) newClient() (dockerClient, error) { + opts := []client.Opt{client.FromEnv} + + // Check if default docker socket exists + hostURL, err := client.ParseHostURL(client.DefaultDockerHost) + if err != nil { + return nil, fmt.Errorf("failed to parse default docker host: %w", err) + } + + // For unix sockets, check if the socket file exists + if hostURL.Scheme == "unix" { + if _, err := os.Stat(hostURL.Path); os.IsNotExist(err) { + // Default socket doesn't exist, try alternate location. Docker Desktop uses ~/.docker/run/docker.sock + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("failed to get user home directory: %w", err) + } + homeSocketPath := fmt.Sprintf("%s/.docker/run/docker.sock", homeDir) + if _, err := os.Stat(homeSocketPath); os.IsNotExist(err) { + return nil, errors.New("failed to find docker socket") + } + socketURL := &url.URL{ + Scheme: "unix", + Path: homeSocketPath, + } + // prepend the host, so that it can still be overridden by the environment. + opts = append([]client.Opt{client.WithHost(socketURL.String())}, opts...) + } + } + + return client.NewClientWithOpts(opts...) +} + // DockerBuilder handles building docker images using just commands type DockerBuilder struct { // Base directory where the build commands should be executed @@ -16,6 +93,10 @@ type DockerBuilder struct { cmdTemplate *template.Template // Dry run mode dryRun bool + // Docker provider for creating clients + dockerProvider dockerProvider + // Command factory for testing + cmdFactory cmdFactory builtImages map[string]string } @@ -48,13 +129,29 @@ func WithDockerDryRun(dryRun bool) DockerBuilderOptions { } } +// withDockerProvider is a package-private option for testing +func withDockerProvider(provider dockerProvider) DockerBuilderOptions { + return func(b *DockerBuilder) { + b.dockerProvider = provider + } +} + +// withCmdFactory is a package-private option for testing +func withCmdFactory(factory cmdFactory) DockerBuilderOptions { + return func(b *DockerBuilder) { + b.cmdFactory = factory + } +} + // NewDockerBuilder creates a new DockerBuilder instance func NewDockerBuilder(opts ...DockerBuilderOptions) *DockerBuilder { b := &DockerBuilder{ - baseDir: ".", - cmdTemplate: defaultCmdTemplate, - dryRun: false, - builtImages: make(map[string]string), + baseDir: ".", + cmdTemplate: defaultCmdTemplate, + dryRun: false, + dockerProvider: &defaultDockerProvider{}, + cmdFactory: defaultCmdFactory, + builtImages: make(map[string]string), } for _, opt := range opts { @@ -71,12 +168,20 @@ type templateData struct { } // Build executes the docker build command for the given project and image tag +// Note: the returned image tag is the image ID, so we don't accidentally +// de-duplicate steps that should not be de-duplicated. func (b *DockerBuilder) Build(projectName, imageTag string) (string, error) { if builtImage, ok := b.builtImages[projectName]; ok { return builtImage, nil } log.Printf("Building docker image for project: %s with tag: %s", projectName, imageTag) + + if b.dryRun { + b.builtImages[projectName] = imageTag + return imageTag, nil + } + // Prepare template data data := templateData{ ImageTag: imageTag, @@ -90,17 +195,35 @@ func (b *DockerBuilder) Build(projectName, imageTag string) (string, error) { } // Create command - cmd := exec.Command("sh", "-c", cmdBuf.String()) - cmd.Dir = b.baseDir + cmd := b.cmdFactory("sh", "-c", cmdBuf.String()) + output, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("build command failed: %w\nOutput: %s", err, string(output)) + } - if !b.dryRun { - output, err := cmd.CombinedOutput() - if err != nil { - return "", fmt.Errorf("build command failed: %w\nOutput: %s", err, string(output)) - } + dockerClient, err := b.dockerProvider.newClient() + if err != nil { + return "", fmt.Errorf("failed to create docker client: %w", err) + } + + ctx := context.Background() + + // Inspect the image to get its ID + inspect, _, err := dockerClient.ImageInspectWithRaw(ctx, imageTag) + if err != nil { + return "", fmt.Errorf("failed to inspect image: %w", err) + } + + // Get the short ID (first 12 characters of the SHA256) + shortID := strings.TrimPrefix(inspect.ID, "sha256:")[:12] + + // Create a new tag with projectName:shortID + fullTag := fmt.Sprintf("%s:%s", projectName, shortID) + err = dockerClient.ImageTag(ctx, imageTag, fullTag) + if err != nil { + return "", fmt.Errorf("failed to tag image: %w", err) } - // Return the image tag as confirmation of successful build - b.builtImages[projectName] = imageTag - return imageTag, nil + b.builtImages[projectName] = fullTag + return fullTag, nil } diff --git a/kurtosis-devnet/pkg/build/docker_test.go b/kurtosis-devnet/pkg/build/docker_test.go new file mode 100644 index 0000000000..f0c2b4fd74 --- /dev/null +++ b/kurtosis-devnet/pkg/build/docker_test.go @@ -0,0 +1,115 @@ +package build + +import ( + "context" + "testing" + + "github.com/docker/docker/api/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// mockDockerClient implements the dockerClient interface for testing +type mockDockerClient struct { + inspectFunc func(ctx context.Context, imageID string) (types.ImageInspect, []byte, error) + tagFunc func(ctx context.Context, source, target string) error +} + +func (m *mockDockerClient) ImageInspectWithRaw(ctx context.Context, imageID string) (types.ImageInspect, []byte, error) { + return m.inspectFunc(ctx, imageID) +} + +func (m *mockDockerClient) ImageTag(ctx context.Context, source, target string) error { + return m.tagFunc(ctx, source, target) +} + +// mockDockerProvider implements the dockerProvider interface for testing +type mockDockerProvider struct { + client dockerClient +} + +func (p *mockDockerProvider) newClient() (dockerClient, error) { + return p.client, nil +} + +// mockCmd is a mock for command execution that always succeeds +type mockCmd struct { + output []byte +} + +func (m *mockCmd) CombinedOutput() ([]byte, error) { + return m.output, nil +} + +// mockCmdFactory creates mock commands for testing +func mockCmdFactory(output []byte) cmdFactory { + return func(name string, arg ...string) cmdRunner { + return &mockCmd{output: output} + } +} + +// TestDockerBuilderNaming tests the image naming logic in the DockerBuilder +func TestDockerBuilderNaming(t *testing.T) { + tests := []struct { + name string + projectName string + imageTag string + mockInspect types.ImageInspect + mockTagErr error + wantTag string + wantErr bool + }{ + { + name: "successful image build and tag", + projectName: "test-project", + imageTag: "test-image:latest", + mockInspect: types.ImageInspect{ + ID: "sha256:abcdef123456789abcdef123456789abcdef1234", + }, + wantTag: "test-project:abcdef123456", + wantErr: false, + }, + { + name: "tag error", + projectName: "test-project", + imageTag: "test-image:latest", + mockInspect: types.ImageInspect{ + ID: "sha256:abcdef123456789abcdef123456789abcdef1234", + }, + mockTagErr: assert.AnError, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockClient := &mockDockerClient{ + inspectFunc: func(ctx context.Context, imageID string) (types.ImageInspect, []byte, error) { + return tt.mockInspect, nil, nil + }, + tagFunc: func(ctx context.Context, source, target string) error { + return tt.mockTagErr + }, + } + + mockProvider := &mockDockerProvider{ + client: mockClient, + } + + // Create a builder with our mocks + builder := NewDockerBuilder( + withDockerProvider(mockProvider), + withCmdFactory(mockCmdFactory([]byte("mock build output"))), + ) + + tag, err := builder.Build(tt.projectName, tt.imageTag) + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.wantTag, tag) + }) + } +} diff --git a/kurtosis-devnet/pkg/deploy/deploy.go b/kurtosis-devnet/pkg/deploy/deploy.go index 6bbf328f9a..442edb697d 100644 --- a/kurtosis-devnet/pkg/deploy/deploy.go +++ b/kurtosis-devnet/pkg/deploy/deploy.go @@ -240,12 +240,14 @@ func (d *Deployer) Deploy(ctx context.Context, r io.Reader) (*kurtosis.KurtosisE deployer: d.ktDeployer, } + ch := srv.getState(ctx) + buf, err := d.renderTemplate(tmpDir, srv.URL) if err != nil { return nil, fmt.Errorf("error rendering template: %w", err) } - if err := srv.Deploy(ctx, tmpDir); err != nil { + if err := srv.Deploy(ctx, tmpDir, ch); err != nil { return nil, fmt.Errorf("error deploying fileserver: %w", err) } diff --git a/kurtosis-devnet/pkg/deploy/fileserver.go b/kurtosis-devnet/pkg/deploy/fileserver.go index 84725f2039..6040395b85 100644 --- a/kurtosis-devnet/pkg/deploy/fileserver.go +++ b/kurtosis-devnet/pkg/deploy/fileserver.go @@ -3,11 +3,17 @@ package deploy import ( "bytes" "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" "fmt" + "log" "os" "path/filepath" "strings" + "sync" + ktfs "github.com/ethereum-optimism/optimism/devnet-sdk/kt/fs" "github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis" "github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/util" ) @@ -25,17 +31,43 @@ func (f *FileServer) URL(path ...string) string { return fmt.Sprintf("http://%s/%s", FILESERVER_PACKAGE, strings.Join(path, "/")) } -func (f *FileServer) Deploy(ctx context.Context, sourceDir string) error { +func (f *FileServer) Deploy(ctx context.Context, sourceDir string, stateCh <-chan *fileserverState) error { + // Check if source directory is empty. If it is, then ie means we don't have + // anything to serve, so we might as well not deploy the fileserver. + entries, err := os.ReadDir(sourceDir) + if err != nil { + return fmt.Errorf("error reading source directory: %w", err) + } + if len(entries) == 0 { + return nil + } + + srcHash, err := calculateDirHash(sourceDir) + if err != nil { + return fmt.Errorf("error calculating source directory hash: %w", err) + } // Create a temp dir in the fileserver package baseDir := filepath.Join(f.baseDir, FILESERVER_PACKAGE) if err := os.MkdirAll(baseDir, 0755); err != nil { return fmt.Errorf("error creating base directory: %w", err) } + + configHash, err := calculateDirHash(filepath.Join(baseDir, "static_files", "nginx")) + if err != nil { + return fmt.Errorf("error calculating base directory hash: %w", err) + } + + refState := <-stateCh + if refState.contentHash == srcHash && refState.configHash == configHash { + log.Println("No changes to fileserver, skipping deployment") + return nil + } + // Can't use MkdirTemp here because the directory name needs to always be the same // in order for kurtosis file artifact upload to be idempotent. // (i.e. the file upload and all its downstream dependencies can be SKIPPED on re-runs) tempDir := filepath.Join(baseDir, "upload-content") - err := os.Mkdir(tempDir, 0755) + err = os.Mkdir(tempDir, 0755) if err != nil { return fmt.Errorf("error creating temporary directory: %w", err) } @@ -68,3 +100,145 @@ func (f *FileServer) Deploy(ctx context.Context, sourceDir string) error { return nil } + +type fileserverState struct { + contentHash string + configHash string +} + +// downloadAndHashArtifact downloads an artifact and calculates its hash +func downloadAndHashArtifact(ctx context.Context, enclave, artifactName string) (string, error) { + fs, err := ktfs.NewEnclaveFS(ctx, enclave) + if err != nil { + return "", fmt.Errorf("failed to create enclave fs: %w", err) + } + + // Create temp dir + tempDir, err := os.MkdirTemp("", artifactName+"-*") + if err != nil { + return "", fmt.Errorf("failed to create temp dir: %w", err) + } + defer os.RemoveAll(tempDir) + + // Download artifact + artifact, err := fs.GetArtifact(ctx, artifactName) + if err != nil { + return "", fmt.Errorf("failed to get artifact: %w", err) + } + + // Ensure parent directories exist before extracting + if err := os.MkdirAll(tempDir, 0755); err != nil { + return "", fmt.Errorf("failed to create temp dir structure: %w", err) + } + + // Extract to temp dir + if err := artifact.Download(tempDir); err != nil { + return "", fmt.Errorf("failed to download artifact: %w", err) + } + + // Calculate hash + hash, err := calculateDirHash(tempDir) + if err != nil { + return "", fmt.Errorf("failed to calculate hash: %w", err) + } + + return hash, nil +} + +func (f *FileServer) getState(ctx context.Context) <-chan *fileserverState { + stateCh := make(chan *fileserverState) + + go func(ctx context.Context) { + st := &fileserverState{} + var wg sync.WaitGroup + + type artifactInfo struct { + name string + dest *string + } + + artifacts := []artifactInfo{ + {"fileserver-content", &st.contentHash}, + {"fileserver-nginx-conf", &st.configHash}, + } + + for _, art := range artifacts { + wg.Add(1) + go func(art artifactInfo) { + defer wg.Done() + hash, err := downloadAndHashArtifact(ctx, f.enclave, art.name) + if err == nil { + *art.dest = hash + } + }(art) + } + + wg.Wait() + stateCh <- st + }(ctx) + + return stateCh +} + +type entry struct { + RelPath string `json:"rel_path"` + Size int64 `json:"size"` + Mode string `json:"mode"` + Content []byte `json:"content"` +} + +// calculateDirHash returns a SHA256 hash of the directory contents +// It walks through the directory, hashing file names and contents +func calculateDirHash(dir string) (string, error) { + hash := sha256.New() + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Get path relative to root dir + relPath, err := filepath.Rel(dir, path) + if err != nil { + return err + } + + // Skip the root directory + if relPath == "." { + return nil + } + + // Add the relative path and file info to hash + entry := entry{ + RelPath: relPath, + Size: info.Size(), + Mode: info.Mode().String(), + } + + // If it's a regular file, add its contents to hash + if !info.IsDir() { + content, err := os.ReadFile(path) + if err != nil { + return err + } + entry.Content = content + } + + jsonBytes, err := json.Marshal(entry) + if err != nil { + return err + } + _, err = hash.Write(jsonBytes) + if err != nil { + return err + } + + return nil + }) + + if err != nil { + return "", fmt.Errorf("error walking directory: %w", err) + } + + hashStr := hex.EncodeToString(hash.Sum(nil)) + return hashStr, nil +} diff --git a/kurtosis-devnet/pkg/deploy/fileserver_test.go b/kurtosis-devnet/pkg/deploy/fileserver_test.go index 2b3cd96fad..ff87eccc48 100644 --- a/kurtosis-devnet/pkg/deploy/fileserver_test.go +++ b/kurtosis-devnet/pkg/deploy/fileserver_test.go @@ -9,6 +9,7 @@ import ( "github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis" "github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/spec" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -20,36 +21,111 @@ func TestDeployFileserver(t *testing.T) { require.NoError(t, err) defer os.RemoveAll(tmpDir) + // Create test files + sourceDir := filepath.Join(tmpDir, "fileserver") + require.NoError(t, os.MkdirAll(sourceDir, 0755)) + + // Create required directory structure + nginxDir := filepath.Join(sourceDir, "static_files", "nginx") + require.NoError(t, os.MkdirAll(nginxDir, 0755)) + // Create a mock deployer function mockDeployerFunc := func(opts ...kurtosis.KurtosisDeployerOptions) (deployer, error) { return &mockDeployer{}, nil } testCases := []struct { - name string - fs *FileServer - shouldError bool + name string + setup func(t *testing.T, sourceDir, nginxDir string, state *fileserverState) + state *fileserverState + shouldError bool + shouldDeploy bool }{ { - name: "successful deployment", - fs: &FileServer{ - baseDir: tmpDir, - enclave: "test-enclave", - dryRun: true, - deployer: mockDeployerFunc, + name: "empty source directory - no deployment needed", + setup: func(t *testing.T, sourceDir, nginxDir string, state *fileserverState) { + // No files to create }, - shouldError: false, + state: &fileserverState{}, + shouldError: false, + shouldDeploy: false, + }, + { + name: "new files to deploy", + setup: func(t *testing.T, sourceDir, nginxDir string, state *fileserverState) { + require.NoError(t, os.WriteFile( + filepath.Join(sourceDir, "test.txt"), + []byte("test content"), + 0644, + )) + }, + state: &fileserverState{}, + shouldError: false, + shouldDeploy: true, + }, + { + name: "no changes - deployment skipped", + setup: func(t *testing.T, sourceDir, nginxDir string, state *fileserverState) { + require.NoError(t, os.WriteFile( + filepath.Join(sourceDir, "test.txt"), + []byte("test content"), + 0644, + )) + + // Calculate actual hash for the test file + hash, err := calculateDirHash(sourceDir) + require.NoError(t, err) + + // Calculate nginx config hash + configHash, err := calculateDirHash(nginxDir) + require.NoError(t, err) + + // Update state with actual hashes + state.contentHash = hash + state.configHash = configHash + }, + state: &fileserverState{}, + shouldError: false, + shouldDeploy: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - err := tc.fs.Deploy(ctx, filepath.Join(tmpDir, "fileserver")) + // Clean up and recreate source directory for each test + require.NoError(t, os.RemoveAll(sourceDir)) + require.NoError(t, os.MkdirAll(sourceDir, 0755)) + + // Recreate nginx directory + require.NoError(t, os.MkdirAll(nginxDir, 0755)) + + // Setup test files + tc.setup(t, sourceDir, nginxDir, tc.state) + + fs := &FileServer{ + baseDir: tmpDir, + enclave: "test-enclave", + dryRun: true, + deployer: mockDeployerFunc, + } + + // Create state channel and send test state + ch := make(chan *fileserverState, 1) + ch <- tc.state + close(ch) + + err := fs.Deploy(ctx, sourceDir, ch) if tc.shouldError { require.Error(t, err) } else { require.NoError(t, err) } + + // Verify deployment directory was created only if deployment was needed + deployDir := filepath.Join(tmpDir, FILESERVER_PACKAGE) + if tc.shouldDeploy { + assert.DirExists(t, deployDir) + } }) } } diff --git a/kurtosis-devnet/pkg/kurtosis/endpoints.go b/kurtosis-devnet/pkg/kurtosis/endpoints.go index 0072233c93..3207e5ccb1 100644 --- a/kurtosis-devnet/pkg/kurtosis/endpoints.go +++ b/kurtosis-devnet/pkg/kurtosis/endpoints.go @@ -13,6 +13,7 @@ type ServiceFinder struct { services inspect.ServiceMap nodeServices []string l2ServicePrefix string + l2Networks []string } // ServiceFinderOption configures a ServiceFinder @@ -32,6 +33,13 @@ func WithL2ServicePrefix(prefix string) ServiceFinderOption { } } +// WithL2Networks sets the L2 networks +func WithL2Networks(networks []string) ServiceFinderOption { + return func(f *ServiceFinder) { + f.l2Networks = networks + } +} + // NewServiceFinder creates a new ServiceFinder with the given options func NewServiceFinder(services inspect.ServiceMap, opts ...ServiceFinderOption) *ServiceFinder { f := &ServiceFinder{ @@ -69,6 +77,20 @@ func (f *ServiceFinder) FindL2Services(network string) ([]descriptors.Node, desc tag, idx := f.serviceTag(strings.TrimPrefix(name, f.l2ServicePrefix)) return tag, idx, true } + + // skip over the other L2 services + for _, l2Network := range f.l2Networks { + if strings.HasSuffix(serviceName, "-"+l2Network) { + return "", 0, false + } + } + + // Some services don't have a network suffix, as they span multiple chains + // TODO(14849): ideally we'd need to handle *partial* chain coverage. + if strings.HasPrefix(serviceName, f.l2ServicePrefix) { + tag, idx := f.serviceTag(strings.TrimPrefix(serviceName, f.l2ServicePrefix)) + return tag, idx, true + } return "", 0, false }) } diff --git a/kurtosis-devnet/pkg/kurtosis/endpoints_test.go b/kurtosis-devnet/pkg/kurtosis/endpoints_test.go index 5158140396..90ef905040 100644 --- a/kurtosis-devnet/pkg/kurtosis/endpoints_test.go +++ b/kurtosis-devnet/pkg/kurtosis/endpoints_test.go @@ -155,10 +155,153 @@ func TestFindRPCEndpoints(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - finder := NewServiceFinder(tt.services) + finder := NewServiceFinder(tt.services, WithL2Networks([]string{"op-kurtosis", "network1", "network2", "custom-host"})) gotNodes, gotServices := tt.findFn(finder) assert.Equal(t, tt.wantNodes, gotNodes) assert.Equal(t, tt.wantServices, gotServices) }) } } + +func TestFindL2ServicesSkipsOtherNetworks(t *testing.T) { + // Create a service map with services from multiple L2 networks + services := inspect.ServiceMap{ + // network1 services + "op-batcher-network1": inspect.PortMap{ + "http": {Port: 8080}, + }, + "op-proposer-network1": inspect.PortMap{ + "http": {Port: 8082}, + }, + "op-cl-1-op-node-op-geth-network1": inspect.PortMap{ + "http": {Port: 8084}, + }, + + // network2 services + "op-batcher-network2": inspect.PortMap{ + "http": {Port: 8081}, + }, + "op-proposer-network2": inspect.PortMap{ + "http": {Port: 8083}, + }, + "op-cl-1-op-node-op-geth-network2": inspect.PortMap{ + "http": {Port: 8085}, + }, + + // network3 services + "op-batcher-network3": inspect.PortMap{ + "http": {Port: 8086}, + }, + "op-proposer-network3": inspect.PortMap{ + "http": {Port: 8087}, + }, + "op-cl-1-op-node-op-geth-network3": inspect.PortMap{ + "http": {Port: 8088}, + }, + + // Common service without network suffix + "op-common-service": inspect.PortMap{ + "http": {Port: 8089}, + }, + } + + // Create a service finder with all networks configured + finder := NewServiceFinder( + services, + WithL2Networks([]string{"network1", "network2", "network3"}), + ) + + // Test finding services for network2 + t.Run("find network2 services", func(t *testing.T) { + nodes, serviceMap := finder.FindL2Services("network2") + + // Verify nodes + assert.Len(t, nodes, 1) + if len(nodes) > 0 { + assert.Contains(t, nodes[0].Services, "cl") + assert.Equal(t, "op-cl-1-op-node-op-geth-network2", nodes[0].Services["cl"].Name) + } + + // Verify services + assert.Len(t, serviceMap, 3) // batcher, proposer, common-service + assert.Contains(t, serviceMap, "batcher") + assert.Contains(t, serviceMap, "proposer") + assert.Contains(t, serviceMap, "common-service") + assert.Equal(t, "op-batcher-network2", serviceMap["batcher"].Name) + assert.Equal(t, "op-proposer-network2", serviceMap["proposer"].Name) + assert.Equal(t, "op-common-service", serviceMap["common-service"].Name) + + // Verify network1 and network3 services are not included + for _, service := range serviceMap { + assert.NotContains(t, service.Name, "network1") + assert.NotContains(t, service.Name, "network3") + } + }) + + // Test with a network that doesn't exist + t.Run("find non-existent network services", func(t *testing.T) { + nodes, serviceMap := finder.FindL2Services("non-existent") + + // Should only find common services + assert.Len(t, nodes, 0) + assert.Len(t, serviceMap, 1) + assert.Contains(t, serviceMap, "common-service") + assert.Equal(t, "op-common-service", serviceMap["common-service"].Name) + }) +} + +func TestServiceTag(t *testing.T) { + finder := NewServiceFinder(inspect.ServiceMap{}) + + tests := []struct { + name string + input string + wantTag string + wantIndex int + }{ + { + name: "simple service without index", + input: "batcher", + wantTag: "batcher", + wantIndex: 0, + }, + { + name: "service with index 1", + input: "node-1", + wantTag: "node", + wantIndex: 1, + }, + { + name: "service with index 2", + input: "node-2", + wantTag: "node", + wantIndex: 2, + }, + { + name: "service with double digit index", + input: "node-10", + wantTag: "node", + wantIndex: 10, + }, + { + name: "service with index in the middle", + input: "node-1-suffix", + wantTag: "node", + wantIndex: 1, + }, + { + name: "service with multiple hyphens", + input: "multi-part-name-1", + wantTag: "multi-part-name", + wantIndex: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotTag, gotIndex := finder.serviceTag(tt.input) + assert.Equal(t, tt.wantTag, gotTag) + assert.Equal(t, tt.wantIndex, gotIndex) + }) + } +} diff --git a/kurtosis-devnet/pkg/kurtosis/kurtosis.go b/kurtosis-devnet/pkg/kurtosis/kurtosis.go index b79d0d165c..a47a08e8f7 100644 --- a/kurtosis-devnet/pkg/kurtosis/kurtosis.go +++ b/kurtosis-devnet/pkg/kurtosis/kurtosis.go @@ -170,9 +170,14 @@ func (d *KurtosisDeployer) GetEnvironmentInfo(ctx context.Context, spec *spec.En } // Find L1 endpoint - finder := NewServiceFinder(inspectResult.UserServices) + networks := make([]string, len(spec.Chains)) + for idx, chainSpec := range spec.Chains { + networks[idx] = chainSpec.Name + } + finder := NewServiceFinder(inspectResult.UserServices, WithL2Networks(networks)) if nodes, services := finder.FindL1Services(); len(nodes) > 0 { chain := &descriptors.Chain{ + ID: deployerState.L1ChainID, Name: "Ethereum", Services: services, Nodes: nodes, diff --git a/kurtosis-devnet/pkg/kurtosis/sources/deployer/deployer.go b/kurtosis-devnet/pkg/kurtosis/sources/deployer/deployer.go index d4ad22d37f..0b8055abf9 100644 --- a/kurtosis-devnet/pkg/kurtosis/sources/deployer/deployer.go +++ b/kurtosis-devnet/pkg/kurtosis/sources/deployer/deployer.go @@ -25,7 +25,7 @@ const ( defaultWalletsName = "wallets.json" defaultStateName = "state.json" defaultGenesisArtifactName = "el_cl_genesis_data" - defaultMnemonicsName = "mnemonics.yaml" + defaultMnemonicName = "mnemonics.yaml" defaultGenesisNameTemplate = "genesis-{{.ChainID}}.json" defaultL1GenesisName = "genesis.json" ) @@ -68,17 +68,18 @@ type WalletList []*Wallet type DeployerData struct { L1ValidatorWallets WalletList `json:"wallets"` State *DeployerState `json:"state"` + L1ChainID string `json:"l1_chain_id"` } type Deployer struct { - enclave string - deployerArtifactName string - walletsName string - stateName string - genesisArtifactName string - mnemonicsName string - l2GenesisNameTemplate string - l1GenesisName string + enclave string + deployerArtifactName string + walletsName string + stateName string + genesisArtifactName string + l1ValidatorMnemonicName string + l2GenesisNameTemplate string + l1GenesisName string } type DeployerOption func(*Deployer) @@ -109,7 +110,7 @@ func WithGenesisArtifactName(name string) DeployerOption { func WithMnemonicsName(name string) DeployerOption { return func(d *Deployer) { - d.mnemonicsName = name + d.l1ValidatorMnemonicName = name } } @@ -121,14 +122,14 @@ func WithGenesisNameTemplate(name string) DeployerOption { func NewDeployer(enclave string, opts ...DeployerOption) *Deployer { d := &Deployer{ - enclave: enclave, - deployerArtifactName: defaultDeployerArtifactName, - walletsName: defaultWalletsName, - stateName: defaultStateName, - genesisArtifactName: defaultGenesisArtifactName, - mnemonicsName: defaultMnemonicsName, - l2GenesisNameTemplate: defaultGenesisNameTemplate, - l1GenesisName: defaultL1GenesisName, + enclave: enclave, + deployerArtifactName: defaultDeployerArtifactName, + walletsName: defaultWalletsName, + stateName: defaultStateName, + genesisArtifactName: defaultGenesisArtifactName, + l1ValidatorMnemonicName: defaultMnemonicName, + l2GenesisNameTemplate: defaultGenesisNameTemplate, + l1GenesisName: defaultL1GenesisName, } for _, opt := range opts { @@ -337,12 +338,23 @@ func (d *Deployer) ExtractData(ctx context.Context) (*DeployerData, error) { state.Deployments[id] = deployment } - l1ValidatorWallets, err := d.getL1ValidatorWallets(deployerArtifact) + l1GenesisArtifact, err := fs.GetArtifact(ctx, d.genesisArtifactName) + if err != nil { + return nil, err + } + + l1ValidatorWallets, err := d.getL1ValidatorWallets(l1GenesisArtifact) + if err != nil { + return nil, err + } + + l1ChainID, err := d.getL1ChainID(l1GenesisArtifact) if err != nil { return nil, err } return &DeployerData{ + L1ChainID: l1ChainID, State: state, L1ValidatorWallets: l1ValidatorWallets, }, nil diff --git a/kurtosis-devnet/pkg/kurtosis/sources/deployer/wallets.go b/kurtosis-devnet/pkg/kurtosis/sources/deployer/wallets.go index 7247fe3b91..76373eb4c1 100644 --- a/kurtosis-devnet/pkg/kurtosis/sources/deployer/wallets.go +++ b/kurtosis-devnet/pkg/kurtosis/sources/deployer/wallets.go @@ -2,12 +2,14 @@ package deployer import ( "bytes" + "encoding/json" "fmt" "io" ktfs "github.com/ethereum-optimism/optimism/devnet-sdk/kt/fs" "github.com/ethereum-optimism/optimism/op-chain-ops/devkeys" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/crypto" "gopkg.in/yaml.v3" ) @@ -36,17 +38,17 @@ func getMnemonics(r io.Reader) (string, error) { func (d *Deployer) getL1ValidatorWallets(deployerArtifact *ktfs.Artifact) ([]*Wallet, error) { mnemonicsBuffer := bytes.NewBuffer(nil) if err := deployerArtifact.ExtractFiles( - ktfs.NewArtifactFileWriter(d.mnemonicsName, mnemonicsBuffer), + ktfs.NewArtifactFileWriter(d.l1ValidatorMnemonicName, mnemonicsBuffer), ); err != nil { return nil, err } - mnemonics, err := getMnemonics(mnemonicsBuffer) + mnemonic, err := getMnemonics(mnemonicsBuffer) if err != nil { return nil, err } - m, _ := devkeys.NewMnemonicDevKeys(mnemonics) + m, _ := devkeys.NewMnemonicDevKeys(mnemonic) knownWallets := make([]*Wallet, 0) var keys []devkeys.Key @@ -67,3 +69,20 @@ func (d *Deployer) getL1ValidatorWallets(deployerArtifact *ktfs.Artifact) ([]*Wa return knownWallets, nil } + +func (d *Deployer) getL1ChainID(genesisArtifact *ktfs.Artifact) (string, error) { + genesisBuffer := bytes.NewBuffer(nil) + if err := genesisArtifact.ExtractFiles( + ktfs.NewArtifactFileWriter(d.l1GenesisName, genesisBuffer), + ); err != nil { + return "", err + } + + // Parse the genesis file JSON into a core.Genesis struct + var genesis core.Genesis + if err := json.NewDecoder(genesisBuffer).Decode(&genesis); err != nil { + return "", fmt.Errorf("failed to parse genesis file %s in artifact %s: %w", d.l1GenesisName, d.genesisArtifactName, err) + } + + return genesis.Config.ChainID.String(), nil +} diff --git a/kurtosis-devnet/templates/devnet.yaml b/kurtosis-devnet/templates/devnet.yaml index d38eb43793..233e90c4f9 100644 --- a/kurtosis-devnet/templates/devnet.yaml +++ b/kurtosis-devnet/templates/devnet.yaml @@ -15,7 +15,6 @@ optimism_package: image: {{ dig "overrides" "images" "op_supervisor" (localDockerImage "op-supervisor") $context }} extra_params: - {{ dig "overrides" "flags" "log_level" "!!str" $context }} - network: "kurtosis" {{ end }} chains: {{ range $l2_id, $l2 := $l2s }} diff --git a/mise.toml b/mise.toml index ca63c38d44..a3b89965f0 100644 --- a/mise.toml +++ b/mise.toml @@ -37,6 +37,7 @@ anvil = "v1.0.0" codecov-uploader = "0.8.0" goreleaser-pro = "2.3.2-pro" kurtosis = "1.4.4" +op-acceptor = "op-acceptor/v0.1.3" # Fake dependencies # Put things here if you need to track versions of tools or projects that can't @@ -54,6 +55,7 @@ just = "ubi:casey/just" codecov-uploader = "ubi:codecov/uploader" goreleaser-pro = "ubi:goreleaser/goreleaser-pro[exe=goreleaser]" kurtosis = "ubi:kurtosis-tech/kurtosis-cli-release-artifacts[exe=kurtosis]" +op-acceptor = "ubi:ethereum-optimism/infra[exe=op-acceptor,tag_prefix=op-acceptor/]" [settings] experimental = true diff --git a/op-acceptance-tests/.gitignore b/op-acceptance-tests/.gitignore new file mode 100644 index 0000000000..e0081f4804 --- /dev/null +++ b/op-acceptance-tests/.gitignore @@ -0,0 +1 @@ +.bin/ diff --git a/op-acceptance-tests/README.md b/op-acceptance-tests/README.md new file mode 100644 index 0000000000..31bc552495 --- /dev/null +++ b/op-acceptance-tests/README.md @@ -0,0 +1,64 @@ +# OP Stack Acceptance Tests + +## Overview + +This directory contains the acceptance tests and configuration for the OP Stack. These tests are executed by `op-acceptor`, which serves as an automated gatekeeper for OP Stack network promotions. + +Think of acceptance testing as Gandalf 🧙, standing at the gates and shouting, "You shall not pass!" to networks that don't meet our standards. It enforces the "Don't trust, verify" principle by: + +- Running automated acceptance tests +- Providing clear pass/fail results (and tracking these over time) +- Gating network promotions based on test results +- Providing insight into test feature/functional coverage + +The `op-acceptor` ensures network quality and readiness by running a comprehensive suite of acceptance tests before features can advance through the promotion pipeline: + +Localnet -> Alphanet → Betanet → Testnet + +This process helps maintain high-quality standards across all networks in the OP Stack ecosystem. + +## Dependencies + +* Docker +* Kurtosis +* Mise (install as instructed in CONTRIBUTING.md) + +Dependencies are managed using the repo-wide `mise` config. So ensure you've first run `mise install` at the repo root. If you need to manually modify the version of op-acceptor you wish to run you'll need to do it within the _mise.toml_ file at the repo root. + +## Usage + +The tests can be run using the `just` command runner: + +```bash +# Run the default acceptance tests against a simple devnet +just + +# Run the acceptance tests against a specific devnet and gate +just acceptance-test + +# Run the acceptance tests using a specific version of op-acceptor +ACCEPTOR_IMAGE=op-acceptor:latest just acceptance-test +``` + +### Configuration + +- `acceptance-tests.yaml`: Defines the validation gates and the suites and tests that should be run for each gate. +- `justfile`: Contains the commands for running the acceptance tests. + +## Adding New Tests + +To add new acceptance tests: + +1. Create your test in the appropriate Go package (as a regular Go test) +2. Register the test in `acceptance-tests.yaml` under the appropriate gate +3. Follow the existing pattern for test registration: + ```yaml + - name: YourTestName + package: github.com/ethereum-optimism/optimism/your/package/path + ``` + +## 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 diff --git a/op-acceptance-tests/acceptance-tests.yaml b/op-acceptance-tests/acceptance-tests.yaml new file mode 100644 index 0000000000..8df3940774 --- /dev/null +++ b/op-acceptance-tests/acceptance-tests.yaml @@ -0,0 +1,15 @@ +# Configuration file for acceptance tests (op-acceptor) +# +# All acceptance tests need to be registered here for op-acceptor to run them. +# +# Note: The current acceptance tests are placeholders; real ones to come soon. + +gates: + - id: localnet + description: "Localnet validation gate" + tests: + - name: TestFindRPCEndpoints + package: github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/api/run + - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/interop + - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/isthmus + - package: github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/fjord diff --git a/op-acceptance-tests/justfile b/op-acceptance-tests/justfile new file mode 100644 index 0000000000..9f83b920ee --- /dev/null +++ b/op-acceptance-tests/justfile @@ -0,0 +1,65 @@ +REPO_ROOT := `realpath ..` +KURTOSIS_DIR := REPO_ROOT + "/kurtosis-devnet" +ACCEPTOR_VERSION := env_var_or_default("ACCEPTOR_VERSION", "v0.1.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 + +# Run acceptance tests with mise-managed binary +acceptance-test devnet="simple" gate="localnet": + #!/usr/bin/env bash + set -euo pipefail + + # Check if mise is installed + if command -v mise >/dev/null; then + # Try to 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 + + # Run the appropriate devnet from the kurtosis-devnet directory if needed. + # Note: For now, due to a known bug, we ignore failures here + # because if the devnet is already running then this command will fail. + just {{KURTOSIS_DIR}}/{{ devnet }}-devnet || true + + # Print which binary is being used (for debugging) + BINARY_PATH=$(mise which op-acceptor) + echo "Using mise-managed binary: $BINARY_PATH" + + # Run the op-acceptor binary + "$BINARY_PATH" \ + --testdir "{{REPO_ROOT}}" \ + --gate {{gate}} \ + --validators ./acceptance-tests.yaml \ + --log.level info + else + echo "Mise not installed, falling back to Docker..." + just acceptance-test-docker {{devnet}} {{gate}} + fi + +# Run acceptance tests against a devnet using Docker (fallback if needed) +acceptance-test-docker devnet="simple" gate="localnet": + #!/usr/bin/env bash + set -euo pipefail + + # First run the appropriate devnet from the kurtosis-devnet directory if needed. + # We ignore failures here because if the devnet is already running then this command will fail. + just {{KURTOSIS_DIR}}/{{ devnet }}-devnet || true + + # Print which image is being used (for debugging) + echo "Using acceptor image: {{ACCEPTOR_IMAGE}}" + + # Run op-acceptor with the repository mounted at the correct Go module path + docker run \ + -v "$(pwd)/acceptance-tests.yaml:/acceptance-tests.yaml" \ + -v "{{REPO_ROOT}}:/go/src/github.com/ethereum-optimism/optimism" \ + {{ACCEPTOR_IMAGE}} \ + --testdir "/go/src/github.com/ethereum-optimism/optimism" \ + --gate {{gate}} \ + --validators /acceptance-tests.yaml \ + --log.level debug diff --git a/op-acceptance-tests/tests/ecotone/fees_test.go b/op-acceptance-tests/tests/ecotone/fees_test.go new file mode 100644 index 0000000000..a9acbec6b7 --- /dev/null +++ b/op-acceptance-tests/tests/ecotone/fees_test.go @@ -0,0 +1,306 @@ +package ecotone + +import ( + "context" + "errors" + "math/big" + "testing" + "time" + + "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" + "github.com/ethereum-optimism/optimism/devnet-sdk/types" + "github.com/ethereum-optimism/optimism/op-e2e/bindings" + "github.com/ethereum-optimism/optimism/op-node/rollup" + "github.com/ethereum-optimism/optimism/op-service/predeploys" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + gethTypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/params" + "github.com/stretchr/testify/require" +) + +// TestFees verifies that L1/L2 fees are handled properly in different fork configurations +func TestFees(t *testing.T) { + // Define which L2 chain we'll test + chainIdx := uint64(0) + + // Get validators and getters for accessing the system and wallets + lowLevelSystemGetter, lowLevelSystemValidator := validators.AcquireLowLevelSystem() + walletGetter, walletValidator := validators.AcquireL2WalletWithFunds(chainIdx, types.NewBalance(big.NewInt(params.Ether))) + + // Run ecotone test + _, forkValidator := validators.AcquireL2WithFork(chainIdx, rollup.Ecotone) + _, notForkValidator := validators.AcquireL2WithoutFork(chainIdx, rollup.Fjord) + systest.SystemTest(t, + feesTestScenario(lowLevelSystemGetter, walletGetter, chainIdx), + lowLevelSystemValidator, + walletValidator, + forkValidator, + notForkValidator, + ) + +} + +// stateGetterAdapter adapts the ethclient to implement the StateGetter interface +type stateGetterAdapter struct { + ctx context.Context + t systest.T + client *ethclient.Client +} + +// GetState implements the StateGetter interface +func (sga *stateGetterAdapter) GetState(addr common.Address, key common.Hash) common.Hash { + var result common.Hash + val, err := sga.client.StorageAt(sga.ctx, addr, key, nil) + require.NoError(sga.t, err) + copy(result[:], val) + return result +} + +// waitForTransaction polls for a transaction receipt until it is available or the context is canceled. +// It's a simpler version of the functionality in SimpleTxManager. +func waitForTransaction(ctx context.Context, client *ethclient.Client, hash common.Hash) (*gethTypes.Receipt, error) { + ticker := time.NewTicker(500 * time.Millisecond) // Poll every 500ms + defer ticker.Stop() + + for { + receipt, err := client.TransactionReceipt(ctx, hash) + if receipt != nil && err == nil { + return receipt, nil + } else if err != nil && !errors.Is(err, ethereum.NotFound) { + return nil, err + } + + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-ticker.C: + // Continue polling + } + } +} + +// feesTestScenario creates a test scenario for verifying fee calculations +func feesTestScenario( + lowLevelSystemGetter validators.LowLevelSystemGetter, + walletGetter validators.WalletGetter, + chainIdx uint64, +) systest.SystemTestFunc { + return func(t systest.T, sys system.System) { + ctx := t.Context() + + // Get the low-level system and wallet + llsys := lowLevelSystemGetter(ctx) + wallet := walletGetter(ctx) + + // Get the L2 client + l2Chain := llsys.L2s()[chainIdx] + l2Client, err := l2Chain.GethClient() + require.NoError(t, err) + + // TODO: Wait for first block after genesis + // The genesis block has zero L1Block values and will throw off the GPO checks + header, err := l2Client.HeaderByNumber(ctx, big.NewInt(1)) + require.NoError(t, err) + + startBlockNumber := header.Number + + // Get the genesis config + chainConfig, err := l2Chain.Config() + require.NoError(t, err) + + // Create state getter adapter for L1 cost function + sga := &stateGetterAdapter{ + ctx: ctx, + t: t, + client: l2Client, + } + + // Create L1 cost function + l1CostFn := gethTypes.NewL1CostFunc(chainConfig, sga) + + // Create operator fee function + operatorFeeFn := gethTypes.NewOperatorCostFunc(chainConfig, sga) + + // Get wallet private key and address + fromAddr := wallet.Address() + privateKey := wallet.PrivateKey() + + // Find gaspriceoracle contract + gpoContract, err := bindings.NewGasPriceOracle(predeploys.GasPriceOracleAddr, l2Client) + require.NoError(t, err) + + // Get wallet balance before test + startBalance, err := l2Client.BalanceAt(ctx, fromAddr, startBlockNumber) + require.NoError(t, err) + require.Greater(t, startBalance.Uint64(), big.NewInt(0).Uint64()) + + // Get initial balances of fee recipients + baseFeeRecipientStartBalance, err := l2Client.BalanceAt(ctx, predeploys.BaseFeeVaultAddr, startBlockNumber) + require.NoError(t, err) + + l1FeeRecipientStartBalance, err := l2Client.BalanceAt(ctx, predeploys.L1FeeVaultAddr, startBlockNumber) + require.NoError(t, err) + + sequencerFeeVaultStartBalance, err := l2Client.BalanceAt(ctx, predeploys.SequencerFeeVaultAddr, startBlockNumber) + require.NoError(t, err) + + operatorFeeVaultStartBalance, err := l2Client.BalanceAt(ctx, predeploys.OperatorFeeVaultAddr, startBlockNumber) + require.NoError(t, err) + + genesisBlock, err := l2Client.BlockByNumber(ctx, startBlockNumber) + require.NoError(t, err) + + coinbaseStartBalance, err := l2Client.BalanceAt(ctx, genesisBlock.Coinbase(), startBlockNumber) + require.NoError(t, err) + + // Send a simple transfer from wallet to a test address + transferAmount := big.NewInt(params.Ether / 10) // 0.1 ETH + targetAddr := common.Address{0xff, 0xff} + + // Get suggested gas tip from the client instead of using a hardcoded value + gasTip, err := l2Client.SuggestGasTipCap(ctx) + require.NoError(t, err, "Failed to get suggested gas tip") + + // Estimate gas for the transaction instead of using a hardcoded value + msg := ethereum.CallMsg{ + From: fromAddr, + To: &targetAddr, + Value: transferAmount, + } + gasLimit, err := l2Client.EstimateGas(ctx, msg) + require.NoError(t, err, "Failed to estimate gas") + + // Create and sign transaction with the suggested values + nonce, err := l2Client.PendingNonceAt(ctx, fromAddr) + require.NoError(t, err) + + // Get latest header to get the base fee + header, err = l2Client.HeaderByNumber(ctx, nil) + require.NoError(t, err) + + // Calculate a reasonable gas fee cap based on the base fee + // A common approach is to set fee cap to 2x the base fee + tip + gasFeeCap := new(big.Int).Add( + new(big.Int).Mul(header.BaseFee, big.NewInt(2)), + gasTip, + ) + + txData := &gethTypes.DynamicFeeTx{ + ChainID: l2Chain.ID(), + Nonce: nonce, + GasTipCap: gasTip, + GasFeeCap: gasFeeCap, + Gas: gasLimit, + To: &targetAddr, + Value: transferAmount, + Data: nil, + } + + // Sign transaction + tx := gethTypes.NewTx(txData) + signedTx, err := gethTypes.SignTx(tx, gethTypes.LatestSignerForChainID(l2Chain.ID()), privateKey) + require.NoError(t, err) + + // Send transaction + err = l2Client.SendTransaction(ctx, signedTx) + require.NoError(t, err) + + // Wait for transaction receipt with timeout + ctx, cancel := context.WithTimeout(ctx, time.Second*10) + defer cancel() + receipt, err := waitForTransaction(ctx, l2Client, signedTx.Hash()) + require.NoError(t, err, "Failed to wait for transaction receipt") + require.NotNil(t, receipt) + require.Equal(t, gethTypes.ReceiptStatusSuccessful, receipt.Status) + + // Get block header where transaction was included + header, err = l2Client.HeaderByNumber(ctx, receipt.BlockNumber) + require.NoError(t, err) + + // Get final balances after transaction + coinbaseEndBalance, err := l2Client.BalanceAt(ctx, header.Coinbase, header.Number) + require.NoError(t, err) + + endBalance, err := l2Client.BalanceAt(ctx, fromAddr, header.Number) + require.NoError(t, err) + + baseFeeRecipientEndBalance, err := l2Client.BalanceAt(ctx, predeploys.BaseFeeVaultAddr, header.Number) + require.NoError(t, err) + + operatorFeeVaultEndBalance, err := l2Client.BalanceAt(ctx, predeploys.OperatorFeeVaultAddr, header.Number) + require.NoError(t, err) + + l1FeeRecipientEndBalance, err := l2Client.BalanceAt(ctx, predeploys.L1FeeVaultAddr, header.Number) + require.NoError(t, err) + + sequencerFeeVaultEndBalance, err := l2Client.BalanceAt(ctx, predeploys.SequencerFeeVaultAddr, header.Number) + require.NoError(t, err) + + // Calculate differences in balances + baseFeeRecipientDiff := new(big.Int).Sub(baseFeeRecipientEndBalance, baseFeeRecipientStartBalance) + l1FeeRecipientDiff := new(big.Int).Sub(l1FeeRecipientEndBalance, l1FeeRecipientStartBalance) + sequencerFeeVaultDiff := new(big.Int).Sub(sequencerFeeVaultEndBalance, sequencerFeeVaultStartBalance) + coinbaseDiff := new(big.Int).Sub(coinbaseEndBalance, coinbaseStartBalance) + operatorFeeVaultDiff := new(big.Int).Sub(operatorFeeVaultEndBalance, operatorFeeVaultStartBalance) + + // Verify L2 fee + l2Fee := new(big.Int).Mul(gasTip, new(big.Int).SetUint64(receipt.GasUsed)) + require.Equal(t, sequencerFeeVaultDiff, coinbaseDiff, "coinbase is always sequencer fee vault") + require.Equal(t, l2Fee, coinbaseDiff, "l2 fee mismatch") + require.Equal(t, l2Fee, sequencerFeeVaultDiff) + + // Verify base fee + baseFee := new(big.Int).Mul(header.BaseFee, new(big.Int).SetUint64(receipt.GasUsed)) + require.Equal(t, baseFee, baseFeeRecipientDiff, "base fee mismatch") + + // Verify L1 fee + txBytes, err := tx.MarshalBinary() + require.NoError(t, err) + + // Calculate L1 fee based on transaction data and blocktime + l1Fee := l1CostFn(tx.RollupCostData(), header.Time) + require.Equal(t, l1Fee, l1FeeRecipientDiff, "L1 fee mismatch") + + // Calculate operator fee + expectedOperatorFee := operatorFeeFn(receipt.GasUsed, header.Time) + expectedOperatorFeeVaultEndBalance := new(big.Int).Sub(operatorFeeVaultStartBalance, expectedOperatorFee.ToBig()) + require.True(t, + operatorFeeVaultDiff.Cmp(expectedOperatorFee.ToBig()) == 0, + "operator fee mismatch: operator fee vault start balance %v, actual end balance %v, expected end balance %v", + operatorFeeVaultStartBalance, + operatorFeeVaultEndBalance, + expectedOperatorFeeVaultEndBalance, + ) + + // Verify GPO matches expected state + gpoEcotone, err := gpoContract.IsEcotone(&bind.CallOpts{BlockNumber: header.Number}) + require.NoError(t, err) + + require.NoError(t, err) + require.True(t, gpoEcotone, "GPO and chain must have same ecotone view") + + // Verify gas price oracle L1 fee calculation + gpoL1Fee, err := gpoContract.GetL1Fee(&bind.CallOpts{BlockNumber: header.Number}, txBytes) + require.NoError(t, err) + + adjustedGPOFee := gpoL1Fee + require.Equal(t, l1Fee, adjustedGPOFee, "GPO reports L1 fee mismatch") + + // Verify receipt L1 fee + require.Equal(t, receipt.L1Fee, l1Fee, "l1 fee in receipt is correct") + + // Calculate total fee and verify wallet balance difference + totalFeeRecipient := new(big.Int).Add(baseFeeRecipientDiff, sequencerFeeVaultDiff) + totalFee := new(big.Int).Add(totalFeeRecipient, l1FeeRecipientDiff) + totalFee = new(big.Int).Add(totalFee, operatorFeeVaultDiff) + + balanceDiff := new(big.Int).Sub(startBalance, endBalance) + balanceDiff.Sub(balanceDiff, transferAmount) + require.Equal(t, balanceDiff, totalFee, "balances should add up") + } +} diff --git a/op-acceptance-tests/tests/fjord/check_scripts_test.go b/op-acceptance-tests/tests/fjord/check_scripts_test.go new file mode 100644 index 0000000000..35806b15b3 --- /dev/null +++ b/op-acceptance-tests/tests/fjord/check_scripts_test.go @@ -0,0 +1,93 @@ +package fjord + +import ( + "math/big" + "testing" + + "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" + "github.com/ethereum-optimism/optimism/devnet-sdk/types" + fjordChecks "github.com/ethereum-optimism/optimism/op-chain-ops/cmd/check-fjord/checks" + "github.com/ethereum-optimism/optimism/op-node/rollup" + "github.com/ethereum-optimism/optimism/op-service/testlog" + "github.com/ethereum/go-ethereum/log" + "github.com/stretchr/testify/require" +) + +// TestCheckFjordScript ensures the op-chain-ops/cmd/check-fjord script runs successfully +// against a test chain with the fjord hardfork activated/unactivated +func TestCheckFjordScript(t *testing.T) { + + l2ChainIndex := uint64(0) + + lowLevelSystemGetter, lowLevelSystemValidator := validators.AcquireLowLevelSystem() + walletGetter, walletValidator := validators.AcquireL2WalletWithFunds(l2ChainIndex, types.NewBalance(big.NewInt(1_000_000))) + forkConfigGetter, forkValidatorA := validators.AcquireL2WithFork(l2ChainIndex, rollup.Fjord) + _, forkValidatorB := validators.AcquireL2WithoutFork(l2ChainIndex, rollup.Granite) + systest.SystemTest(t, + checkFjordScriptScenario(lowLevelSystemGetter, walletGetter, forkConfigGetter, l2ChainIndex), + lowLevelSystemValidator, + walletValidator, + forkValidatorA, + forkValidatorB, + ) + + forkConfigGetter, notForkValidator := validators.AcquireL2WithoutFork(l2ChainIndex, rollup.Fjord) + systest.SystemTest(t, + checkFjordScriptScenario(lowLevelSystemGetter, walletGetter, forkConfigGetter, l2ChainIndex), + lowLevelSystemValidator, + walletValidator, + notForkValidator, + ) + +} + +func checkFjordScriptScenario(lowLevelSystemGetter validators.LowLevelSystemGetter, walletGetter validators.WalletGetter, chainConfigGetter validators.ChainConfigGetter, chainIndex uint64) systest.SystemTestFunc { + return func(t systest.T, sys system.System) { + llsys := lowLevelSystemGetter(t.Context()) + wallet := walletGetter(t.Context()) + chainConfig := chainConfigGetter(t.Context()) + + l2 := sys.L2s()[chainIndex] + l2LowLevelClient, err := llsys.L2s()[chainIndex].GethClient() + require.NoError(t, err) + + // Get the wallet's private key and address + privateKey := wallet.PrivateKey() + walletAddr := wallet.Address() + + logger := testlog.Logger(t, log.LevelDebug) + checkFjordConfig := &fjordChecks.CheckFjordConfig{ + Log: logger, + L2: l2LowLevelClient, + Key: privateKey, + Addr: walletAddr, + } + + block, err := l2.Node().BlockByNumber(t.Context(), nil) + require.NoError(t, err) + time := block.Time() + + isFjordActivated, err := validators.IsForkActivated(chainConfig, rollup.Fjord, time) + require.NoError(t, err) + + if !isFjordActivated { + err = fjordChecks.CheckRIP7212(t.Context(), checkFjordConfig) + require.Error(t, err, "expected error for CheckRIP7212") + err = fjordChecks.CheckGasPriceOracle(t.Context(), checkFjordConfig) + require.Error(t, err, "expected error for CheckGasPriceOracle") + err = fjordChecks.CheckTxEmpty(t.Context(), checkFjordConfig) + require.Error(t, err, "expected error for CheckTxEmpty") + err = fjordChecks.CheckTxAllZero(t.Context(), checkFjordConfig) + require.Error(t, err, "expected error for CheckTxAllZero") + err = fjordChecks.CheckTxAll42(t.Context(), checkFjordConfig) + require.Error(t, err, "expected error for CheckTxAll42") + err = fjordChecks.CheckTxRandom(t.Context(), checkFjordConfig) + require.Error(t, err, "expected error for CheckTxRandom") + } else { + err = fjordChecks.CheckAll(t.Context(), checkFjordConfig) + require.NoError(t, err, "should not error on CheckAll") + } + } +} diff --git a/op-acceptance-tests/tests/fjord/fees_test.go b/op-acceptance-tests/tests/fjord/fees_test.go new file mode 100644 index 0000000000..5c398b3c19 --- /dev/null +++ b/op-acceptance-tests/tests/fjord/fees_test.go @@ -0,0 +1,300 @@ +package fjord + +import ( + "context" + "errors" + "math/big" + "testing" + "time" + + "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" + "github.com/ethereum-optimism/optimism/devnet-sdk/types" + "github.com/ethereum-optimism/optimism/op-e2e/bindings" + "github.com/ethereum-optimism/optimism/op-node/rollup" + "github.com/ethereum-optimism/optimism/op-service/predeploys" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + gethTypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/params" + "github.com/stretchr/testify/require" +) + +// TestFees verifies that L1/L2 fees are handled properly in different fork configurations +func TestFees(t *testing.T) { + // Define which L2 chain we'll test + chainIdx := uint64(0) + + // Get validators and getters for accessing the system and wallets + lowLevelSystemGetter, lowLevelSystemValidator := validators.AcquireLowLevelSystem() + walletGetter, walletValidator := validators.AcquireL2WalletWithFunds(chainIdx, types.NewBalance(big.NewInt(params.Ether))) + + // Run fjord test + _, forkValidator := validators.AcquireL2WithFork(chainIdx, rollup.Fjord) + _, notForkValidator := validators.AcquireL2WithoutFork(chainIdx, rollup.Isthmus) + systest.SystemTest(t, + feesTestScenario(lowLevelSystemGetter, walletGetter, chainIdx), + lowLevelSystemValidator, + walletValidator, + forkValidator, + notForkValidator, + ) +} + +// stateGetterAdapter adapts the ethclient to implement the StateGetter interface +type stateGetterAdapter struct { + ctx context.Context + t systest.T + client *ethclient.Client +} + +// GetState implements the StateGetter interface +func (sga *stateGetterAdapter) GetState(addr common.Address, key common.Hash) common.Hash { + var result common.Hash + val, err := sga.client.StorageAt(sga.ctx, addr, key, nil) + require.NoError(sga.t, err) + copy(result[:], val) + return result +} + +// waitForTransaction polls for a transaction receipt until it is available or the context is canceled. +// It's a simpler version of the functionality in SimpleTxManager. +func waitForTransaction(ctx context.Context, client *ethclient.Client, hash common.Hash) (*gethTypes.Receipt, error) { + ticker := time.NewTicker(500 * time.Millisecond) // Poll every 500ms + defer ticker.Stop() + + for { + receipt, err := client.TransactionReceipt(ctx, hash) + if receipt != nil && err == nil { + return receipt, nil + } else if err != nil && !errors.Is(err, ethereum.NotFound) { + return nil, err + } + + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-ticker.C: + // Continue polling + } + } +} + +// feesTestScenario creates a test scenario for verifying fee calculations +func feesTestScenario( + lowLevelSystemGetter validators.LowLevelSystemGetter, + walletGetter validators.WalletGetter, + chainIdx uint64, +) systest.SystemTestFunc { + return func(t systest.T, sys system.System) { + ctx := t.Context() + + // Get the low-level system and wallet + llsys := lowLevelSystemGetter(ctx) + wallet := walletGetter(ctx) + + // Get the L2 client + l2Chain := llsys.L2s()[chainIdx] + l2Client, err := l2Chain.GethClient() + require.NoError(t, err) + + // TODO: Wait for first block after genesis + // The genesis block has zero L1Block values and will throw off the GPO checks + header, err := l2Client.HeaderByNumber(ctx, big.NewInt(1)) + require.NoError(t, err) + + startBlockNumber := header.Number + + // Get the genesis config + chainConfig, err := l2Chain.Config() + require.NoError(t, err) + + // Create state getter adapter for L1 cost function + sga := &stateGetterAdapter{ + ctx: ctx, + t: t, + client: l2Client, + } + + // Create L1 cost function + l1CostFn := gethTypes.NewL1CostFunc(chainConfig, sga) + + // Create operator fee function + operatorFeeFn := gethTypes.NewOperatorCostFunc(chainConfig, sga) + + // Get wallet private key and address + fromAddr := wallet.Address() + privateKey := wallet.PrivateKey() + + // Find gaspriceoracle contract + gpoContract, err := bindings.NewGasPriceOracle(predeploys.GasPriceOracleAddr, l2Client) + require.NoError(t, err) + + // Get wallet balance before test + startBalance, err := l2Client.BalanceAt(ctx, fromAddr, startBlockNumber) + require.NoError(t, err) + require.Greater(t, startBalance.Uint64(), big.NewInt(0).Uint64()) + + // Get initial balances of fee recipients + baseFeeRecipientStartBalance, err := l2Client.BalanceAt(ctx, predeploys.BaseFeeVaultAddr, startBlockNumber) + require.NoError(t, err) + + l1FeeRecipientStartBalance, err := l2Client.BalanceAt(ctx, predeploys.L1FeeVaultAddr, startBlockNumber) + require.NoError(t, err) + + sequencerFeeVaultStartBalance, err := l2Client.BalanceAt(ctx, predeploys.SequencerFeeVaultAddr, startBlockNumber) + require.NoError(t, err) + + operatorFeeVaultStartBalance, err := l2Client.BalanceAt(ctx, predeploys.OperatorFeeVaultAddr, startBlockNumber) + require.NoError(t, err) + + genesisBlock, err := l2Client.BlockByNumber(ctx, startBlockNumber) + require.NoError(t, err) + + coinbaseStartBalance, err := l2Client.BalanceAt(ctx, genesisBlock.Coinbase(), startBlockNumber) + require.NoError(t, err) + + // Send a simple transfer from wallet to a test address + transferAmount := big.NewInt(params.Ether / 10) // 0.1 ETH + targetAddr := common.Address{0xff, 0xff} + + // Get suggested gas tip from the client instead of using a hardcoded value + gasTip, err := l2Client.SuggestGasTipCap(ctx) + require.NoError(t, err, "Failed to get suggested gas tip") + + // Estimate gas for the transaction instead of using a hardcoded value + msg := ethereum.CallMsg{ + From: fromAddr, + To: &targetAddr, + Value: transferAmount, + } + gasLimit, err := l2Client.EstimateGas(ctx, msg) + require.NoError(t, err, "Failed to estimate gas") + + // Create and sign transaction with the suggested values + nonce, err := l2Client.PendingNonceAt(ctx, fromAddr) + require.NoError(t, err) + + // Get latest header to get the base fee + header, err = l2Client.HeaderByNumber(ctx, nil) + require.NoError(t, err) + + // Calculate a reasonable gas fee cap based on the base fee + // A common approach is to set fee cap to 2x the base fee + tip + gasFeeCap := new(big.Int).Add( + new(big.Int).Mul(header.BaseFee, big.NewInt(2)), + gasTip, + ) + + txData := &gethTypes.DynamicFeeTx{ + ChainID: l2Chain.ID(), + Nonce: nonce, + GasTipCap: gasTip, + GasFeeCap: gasFeeCap, + Gas: gasLimit, + To: &targetAddr, + Value: transferAmount, + Data: nil, + } + + // Sign transaction + tx := gethTypes.NewTx(txData) + signedTx, err := gethTypes.SignTx(tx, gethTypes.LatestSignerForChainID(l2Chain.ID()), privateKey) + require.NoError(t, err) + + // Send transaction + err = l2Client.SendTransaction(ctx, signedTx) + require.NoError(t, err) + + // Wait for transaction receipt with timeout + ctx, cancel := context.WithTimeout(ctx, time.Second*10) + defer cancel() + receipt, err := waitForTransaction(ctx, l2Client, signedTx.Hash()) + require.NoError(t, err, "Failed to wait for transaction receipt") + require.NotNil(t, receipt) + require.Equal(t, gethTypes.ReceiptStatusSuccessful, receipt.Status) + + // Get block header where transaction was included + header, err = l2Client.HeaderByNumber(ctx, receipt.BlockNumber) + require.NoError(t, err) + + // Get final balances after transaction + coinbaseEndBalance, err := l2Client.BalanceAt(ctx, header.Coinbase, header.Number) + require.NoError(t, err) + + endBalance, err := l2Client.BalanceAt(ctx, fromAddr, header.Number) + require.NoError(t, err) + + baseFeeRecipientEndBalance, err := l2Client.BalanceAt(ctx, predeploys.BaseFeeVaultAddr, header.Number) + require.NoError(t, err) + + operatorFeeVaultEndBalance, err := l2Client.BalanceAt(ctx, predeploys.OperatorFeeVaultAddr, header.Number) + require.NoError(t, err) + + l1FeeRecipientEndBalance, err := l2Client.BalanceAt(ctx, predeploys.L1FeeVaultAddr, header.Number) + require.NoError(t, err) + + sequencerFeeVaultEndBalance, err := l2Client.BalanceAt(ctx, predeploys.SequencerFeeVaultAddr, header.Number) + require.NoError(t, err) + + // Calculate differences in balances + baseFeeRecipientDiff := new(big.Int).Sub(baseFeeRecipientEndBalance, baseFeeRecipientStartBalance) + l1FeeRecipientDiff := new(big.Int).Sub(l1FeeRecipientEndBalance, l1FeeRecipientStartBalance) + sequencerFeeVaultDiff := new(big.Int).Sub(sequencerFeeVaultEndBalance, sequencerFeeVaultStartBalance) + coinbaseDiff := new(big.Int).Sub(coinbaseEndBalance, coinbaseStartBalance) + operatorFeeVaultDiff := new(big.Int).Sub(operatorFeeVaultEndBalance, operatorFeeVaultStartBalance) + + // Verify L2 fee + l2Fee := new(big.Int).Mul(gasTip, new(big.Int).SetUint64(receipt.GasUsed)) + require.Equal(t, sequencerFeeVaultDiff, coinbaseDiff, "coinbase is always sequencer fee vault") + require.Equal(t, l2Fee, coinbaseDiff, "l2 fee mismatch") + require.Equal(t, l2Fee, sequencerFeeVaultDiff) + + // Verify base fee + baseFee := new(big.Int).Mul(header.BaseFee, new(big.Int).SetUint64(receipt.GasUsed)) + require.Equal(t, baseFee, baseFeeRecipientDiff, "base fee mismatch") + + // Verify L1 fee + txBytes, err := tx.MarshalBinary() + require.NoError(t, err) + + // Calculate L1 fee based on transaction data and blocktime + l1Fee := l1CostFn(tx.RollupCostData(), header.Time) + require.Equal(t, l1Fee, l1FeeRecipientDiff, "L1 fee mismatch") + + // Calculate operator fee + expectedOperatorFee := operatorFeeFn(receipt.GasUsed, header.Time) + expectedOperatorFeeVaultEndBalance := new(big.Int).Sub(operatorFeeVaultStartBalance, expectedOperatorFee.ToBig()) + require.True(t, + operatorFeeVaultDiff.Cmp(expectedOperatorFee.ToBig()) == 0, + "operator fee mismatch: operator fee vault start balance %v, actual end balance %v, expected end balance %v", + operatorFeeVaultStartBalance, + operatorFeeVaultEndBalance, + expectedOperatorFeeVaultEndBalance, + ) + + gpoFjord, err := gpoContract.IsFjord(&bind.CallOpts{BlockNumber: header.Number}) + require.NoError(t, err) + require.True(t, gpoFjord, "GPO must report Fjord") + + // Verify gas price oracle L1 fee calculation + gpoL1Fee, err := gpoContract.GetL1Fee(&bind.CallOpts{BlockNumber: header.Number}, txBytes) + require.NoError(t, err) + require.Equal(t, l1Fee, gpoL1Fee, "GPO reports L1 fee mismatch") + + // Verify receipt L1 fee + require.Equal(t, receipt.L1Fee, l1Fee, "l1 fee in receipt is correct") + + // Calculate total fee and verify wallet balance difference + totalFeeRecipient := new(big.Int).Add(baseFeeRecipientDiff, sequencerFeeVaultDiff) + totalFee := new(big.Int).Add(totalFeeRecipient, l1FeeRecipientDiff) + totalFee = new(big.Int).Add(totalFee, operatorFeeVaultDiff) + + balanceDiff := new(big.Int).Sub(startBalance, endBalance) + balanceDiff.Sub(balanceDiff, transferAmount) + require.Equal(t, balanceDiff, totalFee, "balances should add up") + } +} diff --git a/kurtosis-devnet/tests/interop/boilerplate_test.go b/op-acceptance-tests/tests/interop/boilerplate_test.go similarity index 100% rename from kurtosis-devnet/tests/interop/boilerplate_test.go rename to op-acceptance-tests/tests/interop/boilerplate_test.go diff --git a/kurtosis-devnet/tests/interop/interop_smoke_test.go b/op-acceptance-tests/tests/interop/interop_smoke_test.go similarity index 90% rename from kurtosis-devnet/tests/interop/interop_smoke_test.go rename to op-acceptance-tests/tests/interop/interop_smoke_test.go index 96874b4443..97fecc0880 100644 --- a/kurtosis-devnet/tests/interop/interop_smoke_test.go +++ b/op-acceptance-tests/tests/interop/interop_smoke_test.go @@ -71,6 +71,18 @@ func TestInteropSystemNoop(t *testing.T) { }) } +func TestInteropSystemSupervisor(t *testing.T) { + systest.InteropSystemTest(t, func(t systest.T, sys system.InteropSystem) { + ctx := t.Context() + supervisor, err := sys.Supervisor(ctx) + require.NoError(t, err) + block, err := supervisor.FinalizedL1(ctx) + require.NoError(t, err) + require.NotNil(t, block) + testlog.Logger(t, log.LevelInfo).Info("finalized l1 block", "block", block) + }) +} + func TestSmokeTestFailure(t *testing.T) { // Create mock failing system mockAddr := common.HexToAddress("0x1234567890123456789012345678901234567890") diff --git a/op-acceptance-tests/tests/interop/interop_tx_test.go b/op-acceptance-tests/tests/interop/interop_tx_test.go new file mode 100644 index 0000000000..1c7bab0149 --- /dev/null +++ b/op-acceptance-tests/tests/interop/interop_tx_test.go @@ -0,0 +1,133 @@ +package interop + +import ( + "encoding/hex" + "math/big" + "testing" + + "github.com/ethereum-optimism/optimism/devnet-sdk/contracts/bindings" + "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-e2e/e2eutils/wait" + "github.com/ethereum-optimism/optimism/op-service/testlog" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/log" + "github.com/stretchr/testify/require" +) + +func messagePassingScenario(lowLevelSystemGetter validators.LowLevelSystemGetter, sourceChainIdx, destChainIdx uint64, sourceWalletGetter, destWalletGetter validators.WalletGetter) systest.InteropSystemTestFunc { + return func(t systest.T, sys system.InteropSystem) { + ctx := t.Context() + llsys := lowLevelSystemGetter(ctx) + + logger := testlog.Logger(t, log.LevelInfo) + logger = logger.With("test", "TestMessagePassing", "devnet", sys.Identifier()) + + chainA := sys.L2s()[sourceChainIdx] + chainB := llsys.L2s()[destChainIdx] + + logger.Info("chain info", "sourceChain", chainA.ID(), "destChain", chainB.ID()) + + // userA is funded at chainA and want to initialize message at chain A + userA := sourceWalletGetter(ctx) + // userB is funded at chainB and want to execute message to chainB + userB := destWalletGetter(ctx) + + sha256PrecompileAddr := common.BytesToAddress([]byte{0x2}) + dummyMessage := []byte("l33t message") + + // Initiate message + logger.Info("Initiate message", "address", sha256PrecompileAddr, "message", dummyMessage) + initResult := userA.InitiateMessage(chainB.ID(), sha256PrecompileAddr, dummyMessage).Send(ctx) + require.NoError(t, initResult.Wait()) + + initReceipt, ok := initResult.Info().(system.Receipt) + require.True(t, ok) + logger.Info("Initiate message", "txHash", initReceipt.TxHash().Hex()) + logs := initReceipt.Logs() + // We are directly calling sendMessage, so we expect single log for SentMessage event + require.Equal(t, 1, len(logs), "expected single log") + log := logs[0] + + // Build sentMessage for message execution + blockNumber := initReceipt.BlockNumber() + blockA, err := chainA.Node().BlockByNumber(ctx, blockNumber) + require.NoError(t, err) + blockTimeA := big.NewInt(int64(blockA.Time())) + logger.Info("Initiate message was included at", "timestamp", blockTimeA.String()) + + sentMessage := []byte{} + for _, topic := range log.Topics { + sentMessage = append(sentMessage, topic.Bytes()...) + } + sentMessage = append(sentMessage, log.Data...) + + // Build identifier for message execution + logIndex := big.NewInt(int64(log.Index)) + identifier := bindings.Identifier{ + Origin: constants.L2ToL2CrossDomainMessenger, + BlockNumber: blockNumber, + LogIndex: logIndex, + Timestamp: blockTimeA, + ChainId: chainA.ID(), + } + + // Execute message + logger.Info("Execute message", "address", sha256PrecompileAddr, "message", dummyMessage) + execResult := userB.ExecuteMessage(identifier, sentMessage).Send(ctx) + require.NoError(t, execResult.Wait()) + + execReceipt, ok := execResult.Info().(system.Receipt) + require.True(t, ok) + + execTxHash := execReceipt.TxHash() + logger.Info("Execute message", "txHash", execTxHash.Hex()) + + blockNumberB := execReceipt.BlockNumber() + blockB, err := chainB.Node().BlockByNumber(ctx, blockNumberB) + require.NoError(t, err) + blockTimeB := big.NewInt(int64(blockB.Time())) + logger.Info("Execute message was included at", "timestamp", blockTimeB.String()) + + // Validation that message has passed and got executed successfully + gethClient, err := chainB.GethClient() + require.NoError(t, err) + + trace, err := wait.DebugTraceTx(ctx, gethClient, execTxHash) + require.NoError(t, err) + + precompile := vm.PrecompiledContractsHomestead[sha256PrecompileAddr] + expected, err := precompile.Run(dummyMessage) + require.NoError(t, err) + logger.Info("sha256 computed offchain", "value", hex.EncodeToString(expected)) + + // length of sha256 image is 32 + output := trace.CallTrace.Output + require.GreaterOrEqual(t, len(output), 32) + actual := []byte(output[len(output)-32:]) + logger.Info("sha256 computed onchain", "value", hex.EncodeToString(actual)) + + require.Equal(t, expected, actual) + } +} + +// TestMessagePassing checks the basic functionality of message passing two interoperable chains. +// Scenario: Source chain initiates message to make destination chain execute sha256 precompile. +func TestMessagePassing(t *testing.T) { + sourceChainIdx := uint64(0) + destChainIdx := uint64(1) + sourceWalletGetter, sourcefundsValidator := validators.AcquireL2WalletWithFunds(sourceChainIdx, sdktypes.NewBalance(big.NewInt(1.0*constants.ETH))) + destWalletGetter, destfundsValiator := validators.AcquireL2WalletWithFunds(destChainIdx, sdktypes.NewBalance(big.NewInt(1.0*constants.ETH))) + lowLevelSystemGetter, lowLevelSystemValidator := validators.AcquireLowLevelSystem() + + systest.InteropSystemTest(t, + messagePassingScenario(lowLevelSystemGetter, sourceChainIdx, destChainIdx, sourceWalletGetter, destWalletGetter), + sourcefundsValidator, + destfundsValiator, + lowLevelSystemValidator, + ) +} diff --git a/kurtosis-devnet/tests/interop/mocks_test.go b/op-acceptance-tests/tests/interop/mocks_test.go similarity index 94% rename from kurtosis-devnet/tests/interop/mocks_test.go rename to op-acceptance-tests/tests/interop/mocks_test.go index 2bc2305d59..c5d6cdb9d0 100644 --- a/kurtosis-devnet/tests/interop/mocks_test.go +++ b/op-acceptance-tests/tests/interop/mocks_test.go @@ -9,6 +9,7 @@ import ( "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/descriptors" "github.com/ethereum-optimism/optimism/devnet-sdk/interfaces" @@ -51,6 +52,10 @@ 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 @@ -78,6 +83,14 @@ func (m *mockFailingWallet) SendETH(to types.Address, amount types.Balance) type 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 } diff --git a/op-acceptance-tests/tests/isthmus/erc20_bridge_test.go b/op-acceptance-tests/tests/isthmus/erc20_bridge_test.go new file mode 100644 index 0000000000..508f4352ee --- /dev/null +++ b/op-acceptance-tests/tests/isthmus/erc20_bridge_test.go @@ -0,0 +1,213 @@ +package isthmus + +import ( + "fmt" + "math/big" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" + + "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-e2e/bindings" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait" + "github.com/ethereum-optimism/optimism/op-service/predeploys" + "github.com/ethereum-optimism/optimism/op-service/testlog" +) + +func TestERC20Bridge(t *testing.T) { + chainIdx := uint64(0) // We'll use the first L2 chain for this test + + l2WalletGetter, l2WalletFundsValidator := validators.AcquireL2WalletWithFunds( + chainIdx, + sdktypes.NewBalance(big.NewInt(1.0*constants.ETH)), + ) + l1WalletGetter, l1WalletFundsValidator := validators.AcquireL1WalletWithFunds(sdktypes.NewBalance(big.NewInt(1.0 * constants.ETH))) + lowLevelSystemGetter, lowLevelSystemValidator := validators.AcquireLowLevelSystem() + + systest.SystemTest(t, + erc20BridgeTestScenario(lowLevelSystemGetter, chainIdx, l1WalletGetter, l2WalletGetter), + l2WalletFundsValidator, + l1WalletFundsValidator, + lowLevelSystemValidator, + ) +} + +// erc20BridgeTestScenario tests depositing an ERC20 token from L1 to L2 through the bridge +func erc20BridgeTestScenario(lowLevelSystemGetter validators.LowLevelSystemGetter, chainIdx uint64, l1WalletGetter validators.WalletGetter, l2WalletGetter validators.WalletGetter) systest.SystemTestFunc { + return func(t systest.T, sys system.System) { + ctx := t.Context() + + // Get the l2User wallet + llsys := lowLevelSystemGetter(ctx) + l1User := l1WalletGetter(ctx) + l2User := l2WalletGetter(ctx) + + // Get the L1 chain + l1Chain := llsys.L1() + + // Get the L2 chain + l2Chain := llsys.L2s()[chainIdx] + + logger := testlog.Logger(t, log.LevelInfo) + logger.Info("Started ERC20 bridge test") + + // Connect to L1 and L2 + l1Client, err := l1Chain.GethClient() + require.NoError(t, err) + t.Cleanup(func() { l1Client.Close() }) + + l2Client, err := l2Chain.GethClient() + require.NoError(t, err) + t.Cleanup(func() { l2Client.Close() }) + + // Print the L1 chain ID as a string + logger.Info("L1 Chain ID", "id", l1Chain.ID()) + + // Create transaction options for L1 + l1Opts, err := bind.NewKeyedTransactorWithChainID(l1User.PrivateKey(), l1Chain.ID()) + require.NoError(t, err) + + // Create transaction options for L2 + l2Opts, err := bind.NewKeyedTransactorWithChainID(l2User.PrivateKey(), l2Chain.ID()) + require.NoError(t, err) + + // Deploy a test ERC20 token on L1 (WETH) + logger.Info("Deploying WETH token on L1") + l1TokenAddress, tx, l1Token, err := bindings.DeployWETH(l1Opts, l1Client) + require.NoError(t, err) + + // Wait for the token deployment transaction to be confirmed + _, err = wait.ForReceiptOK(ctx, l1Client, tx.Hash()) + require.NoError(t, err, "Failed to deploy L1 token") + logger.Info("Deployed L1 token", "address", l1TokenAddress) + + // Mint some tokens to the user (deposit ETH to get WETH) + mintAmount := big.NewInt(params.Ether) // 1 ETH + l1Opts.Value = mintAmount + tx, err = l1Token.Deposit(l1Opts) + require.NoError(t, err) + _, err = wait.ForReceiptOK(ctx, l1Client, tx.Hash()) + require.NoError(t, err, "Failed to mint L1 tokens") + l1Opts.Value = nil + + l1Balance, err := l1Token.BalanceOf(&bind.CallOpts{}, l1User.Address()) + require.NoError(t, err) + require.Equal(t, mintAmount, l1Balance, "User should have the minted tokens on L1") + logger.Info("User has tokens on L1", "balance", l1Balance) + + // Create the corresponding L2 token using the OptimismMintableERC20Factory + logger.Info("Creating L2 token via OptimismMintableERC20Factory") + optimismMintableTokenFactory, err := bindings.NewOptimismMintableERC20Factory(predeploys.OptimismMintableERC20FactoryAddr, l2Client) + require.NoError(t, err) + + // Create the L2 token + l2TokenName := "L2 Test Token" + l2TokenSymbol := "L2TEST" + tx, err = optimismMintableTokenFactory.CreateOptimismMintableERC20(l2Opts, l1TokenAddress, l2TokenName, l2TokenSymbol) + require.NoError(t, err) + l2TokenReceipt, err := wait.ForReceiptOK(ctx, l2Client, tx.Hash()) + require.NoError(t, err, "Failed to create L2 token") + + // Extract the L2 token address from the event logs + var l2TokenAddress common.Address + for _, log := range l2TokenReceipt.Logs { + createdEvent, err := optimismMintableTokenFactory.ParseOptimismMintableERC20Created(*log) + if err == nil && createdEvent != nil { + l2TokenAddress = createdEvent.LocalToken + break + } + } + require.NotEqual(t, common.Address{}, l2TokenAddress, "Failed to find L2 token address from events") + logger.Info("Created L2 token", "address", l2TokenAddress) + + // Get the L2 token contract + l2Token, err := bindings.NewOptimismMintableERC20(l2TokenAddress, l2Client) + require.NoError(t, err) + + // Check initial L2 token balance (should be 0) + initialL2Balance, err := l2Token.BalanceOf(&bind.CallOpts{}, l2User.Address()) + require.NoError(t, err) + require.True(t, big.NewInt(0).Cmp(initialL2Balance) == 0, "Initial L2 token balance should be 0, actual was %s", initialL2Balance.String()) + + l1StandardBridgeAddress, ok := l2Chain.Addresses()["l1StandardBridgeProxy"] + require.True(t, ok, fmt.Errorf("no L1 proxy address configured for this test")) + + l1StandardBridge, err := bindings.NewL1StandardBridge(l1StandardBridgeAddress, l1Client) + require.NoError(t, err) + + // Approve the L1 bridge to spend tokens + logger.Info("Approving L1 bridge to spend tokens") + tx, err = l1Token.Approve(l1Opts, l1StandardBridgeAddress, mintAmount) + require.NoError(t, err) + _, err = wait.ForReceiptOK(ctx, l1Client, tx.Hash()) + require.NoError(t, err, "Failed to approve L1 bridge") + + // Amount to bridge + bridgeAmount := big.NewInt(params.Ether / 10) // 0.1 token + minGasLimit := uint32(200000) // Minimum gas limit for the L2 transaction + + // Bridge the tokens from L1 to L2 + logger.Info("Bridging tokens from L1 to L2", "amount", bridgeAmount) + tx, err = l1StandardBridge.DepositERC20To( + l1Opts, + l1TokenAddress, + l2TokenAddress, + l2User.Address(), + bridgeAmount, + minGasLimit, + []byte{}, // No extra data + ) + require.NoError(t, err) + depositReceipt, err := wait.ForReceiptOK(ctx, l1Client, tx.Hash()) + require.NoError(t, err, "Failed to deposit tokens to L2") + logger.Info("Deposit transaction confirmed on L1", "tx", tx.Hash().Hex()) + + // Get the OptimismPortal contract to find the deposit event + optimismPortal, err := bindings.NewOptimismPortal(l2Chain.Addresses()["optimismPortalProxy"], l1Client) + require.NoError(t, err) + + // Find the TransactionDeposited event from the logs + var depositFound bool + for _, log := range depositReceipt.Logs { + depositEvent, err := optimismPortal.ParseTransactionDeposited(*log) + if err == nil && depositEvent != nil { + logger.Info("Found deposit event", "from", depositEvent.From) + depositFound = true + break + } + } + require.True(t, depositFound, "No deposit event found in transaction logs") + + // Wait for the deposit to be processed on L2 + // This may take some time as it depends on the L2 block time and the deposit processing + logger.Info("Waiting for deposit to be processed on L2...") + + // Poll for the L2 balance to change + err = wait.For(ctx, 200*time.Millisecond, func() (bool, error) { + l2Balance, err := l2Token.BalanceOf(&bind.CallOpts{}, l2User.Address()) + if err != nil { + return false, err + } + return l2Balance.Cmp(initialL2Balance) > 0, nil + }) + require.NoError(t, err, "Timed out waiting for L2 balance to change") + + // Verify the final L2 balance + finalL2Balance, err := l2Token.BalanceOf(&bind.CallOpts{}, l2User.Address()) + require.NoError(t, err) + require.True(t, bridgeAmount.Cmp(finalL2Balance) == 0, "L2 balance should match the bridged amount, L2 balance=%s, bridged amount=%s", finalL2Balance, bridgeAmount) + logger.Info("Successfully verified tokens on L2", "balance", finalL2Balance) + + logger.Info("ERC20 bridge test completed successfully!") + } +} diff --git a/op-acceptance-tests/tests/isthmus/fees_test.go b/op-acceptance-tests/tests/isthmus/fees_test.go new file mode 100644 index 0000000000..33aa620de3 --- /dev/null +++ b/op-acceptance-tests/tests/isthmus/fees_test.go @@ -0,0 +1,302 @@ +package isthmus + +import ( + "context" + "errors" + "math/big" + "testing" + "time" + + "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" + "github.com/ethereum-optimism/optimism/devnet-sdk/types" + "github.com/ethereum-optimism/optimism/op-e2e/bindings" + "github.com/ethereum-optimism/optimism/op-node/rollup" + "github.com/ethereum-optimism/optimism/op-service/predeploys" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + gethTypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/params" + "github.com/stretchr/testify/require" +) + +// TestFees verifies that L1/L2 fees are handled properly in different fork configurations +func TestFees(t *testing.T) { + // Define which L2 chain we'll test + chainIdx := uint64(0) + + // Get validators and getters for accessing the system and wallets + lowLevelSystemGetter, lowLevelSystemValidator := validators.AcquireLowLevelSystem() + walletGetter, walletValidator := validators.AcquireL2WalletWithFunds(chainIdx, types.NewBalance(big.NewInt(params.Ether))) + + // Run isthmus test + _, forkValidator := validators.AcquireL2WithFork(chainIdx, rollup.Isthmus) + systest.SystemTest(t, + feesTestScenario(lowLevelSystemGetter, walletGetter, chainIdx), + lowLevelSystemValidator, + walletValidator, + forkValidator, + ) +} + +// stateGetterAdapter adapts the ethclient to implement the StateGetter interface +type stateGetterAdapter struct { + ctx context.Context + t systest.T + client *ethclient.Client +} + +// GetState implements the StateGetter interface +func (sga *stateGetterAdapter) GetState(addr common.Address, key common.Hash) common.Hash { + var result common.Hash + val, err := sga.client.StorageAt(sga.ctx, addr, key, nil) + require.NoError(sga.t, err) + copy(result[:], val) + return result +} + +// waitForTransaction polls for a transaction receipt until it is available or the context is canceled. +// It's a simpler version of the functionality in SimpleTxManager. +func waitForTransaction(ctx context.Context, client *ethclient.Client, hash common.Hash) (*gethTypes.Receipt, error) { + ticker := time.NewTicker(500 * time.Millisecond) // Poll every 500ms + defer ticker.Stop() + + for { + receipt, err := client.TransactionReceipt(ctx, hash) + if receipt != nil && err == nil { + return receipt, nil + } else if err != nil && !errors.Is(err, ethereum.NotFound) { + return nil, err + } + + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-ticker.C: + // Continue polling + } + } +} + +// feesTestScenario creates a test scenario for verifying fee calculations +func feesTestScenario( + lowLevelSystemGetter validators.LowLevelSystemGetter, + walletGetter validators.WalletGetter, + chainIdx uint64, +) systest.SystemTestFunc { + return func(t systest.T, sys system.System) { + ctx := t.Context() + + // Get the low-level system and wallet + llsys := lowLevelSystemGetter(ctx) + wallet := walletGetter(ctx) + + // Get the L2 client + l2Chain := llsys.L2s()[chainIdx] + l2Client, err := l2Chain.GethClient() + require.NoError(t, err) + + // TODO: Wait for first block after genesis + // The genesis block has zero L1Block values and will throw off the GPO checks + _, err = l2Client.HeaderByNumber(ctx, big.NewInt(1)) + require.NoError(t, err) + + // Get the genesis config + chainConfig, err := l2Chain.Config() + require.NoError(t, err) + + // Create state getter adapter for L1 cost function + sga := &stateGetterAdapter{ + ctx: ctx, + t: t, + client: l2Client, + } + + // Create L1 cost function + l1CostFn := gethTypes.NewL1CostFunc(chainConfig, sga) + + // Create operator fee function + operatorFeeFn := gethTypes.NewOperatorCostFunc(chainConfig, sga) + + // Get wallet private key and address + fromAddr := wallet.Address() + privateKey := wallet.PrivateKey() + + // Find gaspriceoracle contract + gpoContract, err := bindings.NewGasPriceOracle(predeploys.GasPriceOracleAddr, l2Client) + require.NoError(t, err) + + block, err := l2Client.BlockByNumber(t.Context(), nil) + require.NoError(t, err) + startBlockNumber := block.Number() + + // Get wallet balance before test + startBalance, err := l2Client.BalanceAt(ctx, fromAddr, startBlockNumber) + require.NoError(t, err) + require.Greater(t, startBalance.Uint64(), big.NewInt(0).Uint64()) + + // Get initial balances of fee recipients + baseFeeRecipientStartBalance, err := l2Client.BalanceAt(ctx, predeploys.BaseFeeVaultAddr, startBlockNumber) + require.NoError(t, err) + + l1FeeRecipientStartBalance, err := l2Client.BalanceAt(ctx, predeploys.L1FeeVaultAddr, startBlockNumber) + require.NoError(t, err) + + sequencerFeeVaultStartBalance, err := l2Client.BalanceAt(ctx, predeploys.SequencerFeeVaultAddr, startBlockNumber) + require.NoError(t, err) + + operatorFeeVaultStartBalance, err := l2Client.BalanceAt(ctx, predeploys.OperatorFeeVaultAddr, startBlockNumber) + require.NoError(t, err) + + genesisBlock, err := l2Client.BlockByNumber(ctx, startBlockNumber) + require.NoError(t, err) + + coinbaseStartBalance, err := l2Client.BalanceAt(ctx, genesisBlock.Coinbase(), startBlockNumber) + require.NoError(t, err) + + // Send a simple transfer from wallet to a test address + transferAmount := big.NewInt(params.Ether / 10) // 0.1 ETH + targetAddr := common.Address{0xff, 0xff} + + // Get suggested gas tip from the client instead of using a hardcoded value + gasTip, err := l2Client.SuggestGasTipCap(ctx) + require.NoError(t, err, "Failed to get suggested gas tip") + + // Estimate gas for the transaction instead of using a hardcoded value + msg := ethereum.CallMsg{ + From: fromAddr, + To: &targetAddr, + Value: transferAmount, + } + gasLimit, err := l2Client.EstimateGas(ctx, msg) + require.NoError(t, err, "Failed to estimate gas") + + // Create and sign transaction with the suggested values + nonce, err := l2Client.PendingNonceAt(ctx, fromAddr) + require.NoError(t, err) + + // Get latest header to get the base fee + header, err := l2Client.HeaderByNumber(ctx, nil) + require.NoError(t, err) + + // Calculate a reasonable gas fee cap based on the base fee + // A common approach is to set fee cap to 2x the base fee + tip + gasFeeCap := new(big.Int).Add( + new(big.Int).Mul(header.BaseFee, big.NewInt(2)), + gasTip, + ) + + txData := &gethTypes.DynamicFeeTx{ + ChainID: l2Chain.ID(), + Nonce: nonce, + GasTipCap: gasTip, + GasFeeCap: gasFeeCap, + Gas: gasLimit, + To: &targetAddr, + Value: transferAmount, + Data: nil, + } + + // Sign transaction + tx := gethTypes.NewTx(txData) + signedTx, err := gethTypes.SignTx(tx, gethTypes.LatestSignerForChainID(l2Chain.ID()), privateKey) + require.NoError(t, err) + + // Send transaction + err = l2Client.SendTransaction(ctx, signedTx) + require.NoError(t, err) + + // Wait for transaction receipt with timeout + ctx, cancel := context.WithTimeout(ctx, time.Second*10) + defer cancel() + receipt, err := waitForTransaction(ctx, l2Client, signedTx.Hash()) + require.NoError(t, err, "Failed to wait for transaction receipt") + require.NotNil(t, receipt) + require.Equal(t, gethTypes.ReceiptStatusSuccessful, receipt.Status) + + // Get block header where transaction was included + header, err = l2Client.HeaderByNumber(ctx, receipt.BlockNumber) + require.NoError(t, err) + require.Equal(t, header.Coinbase, predeploys.SequencerFeeVaultAddr, "coinbase address should always be the same as the sequencer fee vault address") + + // Get final balances after transaction + coinbaseEndBalance, err := l2Client.BalanceAt(ctx, header.Coinbase, header.Number) + require.NoError(t, err) + + endBalance, err := l2Client.BalanceAt(ctx, fromAddr, header.Number) + require.NoError(t, err) + + baseFeeRecipientEndBalance, err := l2Client.BalanceAt(ctx, predeploys.BaseFeeVaultAddr, header.Number) + require.NoError(t, err) + + operatorFeeVaultEndBalance, err := l2Client.BalanceAt(ctx, predeploys.OperatorFeeVaultAddr, header.Number) + require.NoError(t, err) + + l1FeeRecipientEndBalance, err := l2Client.BalanceAt(ctx, predeploys.L1FeeVaultAddr, header.Number) + require.NoError(t, err) + + sequencerFeeVaultEndBalance, err := l2Client.BalanceAt(ctx, predeploys.SequencerFeeVaultAddr, header.Number) + require.NoError(t, err) + + // Calculate differences in balances + baseFeeRecipientDiff := new(big.Int).Sub(baseFeeRecipientEndBalance, baseFeeRecipientStartBalance) + l1FeeRecipientDiff := new(big.Int).Sub(l1FeeRecipientEndBalance, l1FeeRecipientStartBalance) + sequencerFeeVaultDiff := new(big.Int).Sub(sequencerFeeVaultEndBalance, sequencerFeeVaultStartBalance) + coinbaseDiff := new(big.Int).Sub(coinbaseEndBalance, coinbaseStartBalance) + operatorFeeVaultDiff := new(big.Int).Sub(operatorFeeVaultEndBalance, operatorFeeVaultStartBalance) + + // Verify L2 fee + l2Fee := new(big.Int).Mul(gasTip, new(big.Int).SetUint64(receipt.GasUsed)) + require.Equal(t, sequencerFeeVaultDiff, coinbaseDiff, "coinbase is always sequencer fee vault") + require.Equal(t, l2Fee, coinbaseDiff, "l2 fee mismatch") + require.Equal(t, l2Fee, sequencerFeeVaultDiff) + + // Verify base fee + baseFee := new(big.Int).Mul(header.BaseFee, new(big.Int).SetUint64(receipt.GasUsed)) + require.Equal(t, baseFee, baseFeeRecipientDiff, "base fee mismatch") + + // Verify L1 fee + txBytes, err := tx.MarshalBinary() + require.NoError(t, err) + + // Calculate L1 fee based on transaction data and blocktime + l1Fee := l1CostFn(tx.RollupCostData(), header.Time) + require.Equal(t, l1Fee, l1FeeRecipientDiff, "L1 fee mismatch") + + // Calculate operator fee + expectedOperatorFee := operatorFeeFn(receipt.GasUsed, header.Time) + expectedOperatorFeeVaultEndBalance := new(big.Int).Sub(operatorFeeVaultStartBalance, expectedOperatorFee.ToBig()) + require.True(t, + operatorFeeVaultDiff.Cmp(expectedOperatorFee.ToBig()) == 0, + "operator fee mismatch: operator fee vault start balance %v, actual end balance %v, expected end balance %v", + operatorFeeVaultStartBalance, + operatorFeeVaultEndBalance, + expectedOperatorFeeVaultEndBalance, + ) + + gpoIsthmus, err := gpoContract.IsIsthmus(&bind.CallOpts{BlockNumber: header.Number}) + require.NoError(t, err) + require.True(t, gpoIsthmus, "GPO and chain must have same isthmus view") + + // Verify gas price oracle L1 fee calculation + adjustedGPOFee, err := gpoContract.GetL1Fee(&bind.CallOpts{BlockNumber: header.Number}, txBytes) + require.NoError(t, err) + + require.Equal(t, l1Fee, adjustedGPOFee, "GPO reports L1 fee mismatch") + + // Verify receipt L1 fee + require.Equal(t, receipt.L1Fee, l1Fee, "l1 fee in receipt is correct") + + // Calculate total fee and verify wallet balance difference + totalFeeRecipient := new(big.Int).Add(baseFeeRecipientDiff, sequencerFeeVaultDiff) + totalFee := new(big.Int).Add(totalFeeRecipient, l1FeeRecipientDiff) + totalFee = new(big.Int).Add(totalFee, operatorFeeVaultDiff) + + balanceDiff := new(big.Int).Sub(startBalance, endBalance) + balanceDiff.Sub(balanceDiff, transferAmount) + require.Equal(t, balanceDiff, totalFee, "balances should add up") + } +} diff --git a/op-acceptance-tests/tests/isthmus/withdrawal_root_test.go b/op-acceptance-tests/tests/isthmus/withdrawal_root_test.go new file mode 100644 index 0000000000..24b0105481 --- /dev/null +++ b/op-acceptance-tests/tests/isthmus/withdrawal_root_test.go @@ -0,0 +1,154 @@ +package isthmus + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + gtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" + + "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" + "github.com/ethereum-optimism/optimism/devnet-sdk/types" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait" + "github.com/ethereum-optimism/optimism/op-node/rollup" + "github.com/ethereum-optimism/optimism/op-service/client" + "github.com/ethereum-optimism/optimism/op-service/predeploys" + "github.com/ethereum-optimism/optimism/op-service/sources" + "github.com/ethereum-optimism/optimism/op-service/testlog" + "github.com/lmittmann/w3" +) + +func TestWithdrawalsRoot(t *testing.T) { + chainIdx := uint64(0) // We'll use the first L2 chain for this test + + walletGetter, fundsValidator := validators.AcquireL2WalletWithFunds( + chainIdx, + types.NewBalance(big.NewInt(1.0*constants.ETH)), + ) + llsysGetter, llsysValidator := validators.AcquireLowLevelSystem() + _, forkValidator := validators.AcquireL2WithFork(chainIdx, rollup.Isthmus) + + systest.SystemTest(t, + withdrawalRootTestScenario(chainIdx, walletGetter, llsysGetter), + fundsValidator, + llsysValidator, + forkValidator, + ) +} + +func withdrawalRootTestScenario(chainIdx uint64, walletGetter validators.WalletGetter, llsysGetter validators.LowLevelSystemGetter) systest.SystemTestFunc { + return func(t systest.T, sys system.System) { + ctx := t.Context() + + llsys := llsysGetter(ctx) + chain := llsys.L2s()[chainIdx] + gethCl, err := chain.GethClient() + require.NoError(t, err) + + logger := testlog.Logger(t, log.LevelInfo) + logger.Info("Started test") + + user := walletGetter(ctx) + + // Sad eth clients + rpcCl, err := client.NewRPC(ctx, logger, chain.RPCURL()) + require.NoError(t, err) + t.Cleanup(rpcCl.Close) + ethCl, err := sources.NewEthClient(rpcCl, logger, nil, sources.DefaultEthClientConfig(10)) + require.NoError(t, err) + + // Determine pre-state + preBlock, err := gethCl.BlockByNumber(ctx, nil) + require.NoError(t, err) + logger.Info("Got pre-state block", "hash", preBlock.Hash(), "number", preBlock.Number()) + + preBlockHash := preBlock.Hash() + preProof, err := ethCl.GetProof(ctx, predeploys.L2ToL1MessagePasserAddr, nil, preBlockHash.String()) + require.NoError(t, err) + preWithdrawalsRoot := preProof.StorageHash + + logger.Info("Got pre proof", "storage hash", preWithdrawalsRoot) + + // check isthmus withdrawals-root in the block matches the state + gotPre := preBlock.WithdrawalsRoot() + require.NotNil(t, gotPre) + require.Equal(t, preWithdrawalsRoot, *gotPre, "withdrawals root in block is what we expect") + + chainID := (*big.Int)(chain.ID()) + signer := gtypes.LatestSignerForChainID(chainID) + priv := user.PrivateKey() + require.NoError(t, err) + + // construct call input, ugly but no bindings... + funcInitiateWithdrawal := w3.MustNewFunc(`initiateWithdrawal(address, uint256, bytes memory)`, "") + args, err := funcInitiateWithdrawal.EncodeArgs( + common.Address{}, + big.NewInt(1_000_000), + []byte{}, + ) + require.NoError(t, err) + + // Try to simulate the transaction first to check for errors + gasLimit, err := gethCl.EstimateGas(ctx, ethereum.CallMsg{ + From: user.Address(), + To: &predeploys.L2ToL1MessagePasserAddr, + Value: big.NewInt(0), + Data: args, + }) + require.NoError(t, err, "Gas estimation failed") + + nonce, err := gethCl.PendingNonceAt(ctx, user.Address()) + require.NoError(t, err) + + gasPrice, err := gethCl.SuggestGasPrice(ctx) + require.NoError(t, err, "failed to suggest gas price") + + tip, err := gethCl.SuggestGasTipCap(ctx) + require.NoError(t, err, "error getting gas tip cap") + + tx, err := gtypes.SignNewTx(priv, signer, >ypes.DynamicFeeTx{ + ChainID: chainID, + Nonce: nonce, + GasTipCap: tip, + GasFeeCap: new(big.Int).Add(tip, new(big.Int).Mul(gasPrice, big.NewInt(2))), + Gas: gasLimit, + To: &predeploys.L2ToL1MessagePasserAddr, + Value: big.NewInt(0), + Data: args, + }) + require.NoError(t, err, "sign tx") + + err = gethCl.SendTransaction(ctx, tx) + require.NoError(t, err, "send tx") + + // Find when the withdrawal waskincluded + rec, err := wait.ForReceipt(ctx, gethCl, tx.Hash(), gtypes.ReceiptStatusSuccessful) + require.NoError(t, err) + + // Load the storage at this particular block + postBlockHash := rec.BlockHash + postProof, err := ethCl.GetProof(ctx, predeploys.L2ToL1MessagePasserAddr, nil, postBlockHash.String()) + require.NoError(t, err, "Error getting L2ToL1MessagePasser contract proof") + postWithdrawalsRoot := postProof.StorageHash + + // Check that the withdrawals-root changed + require.NotEqual(t, preWithdrawalsRoot, postWithdrawalsRoot, "withdrawals storage root changes") + + postBlock, err := gethCl.BlockByHash(ctx, postBlockHash) + require.NoError(t, err) + logger.Info("Got post-state block", "hash", postBlock.Hash(), "number", postBlock.Number()) + + gotPost := postBlock.WithdrawalsRoot() + require.NotNil(t, gotPost) + require.Equal(t, postWithdrawalsRoot, *gotPost, "block contains new withdrawals root") + + logger.Info("Withdrawals root test passed") + } +} diff --git a/op-batcher/batcher/channel_manager.go b/op-batcher/batcher/channel_manager.go index 8083cc4c82..acb63b74c5 100644 --- a/op-batcher/batcher/channel_manager.go +++ b/op-batcher/batcher/channel_manager.go @@ -83,7 +83,10 @@ func (s *channelManager) Clear(l1OriginLastSubmittedChannel eth.BlockID) { s.tip = common.Hash{} s.currentChannel = nil s.channelQueue = nil - s.metr.RecordChannelQueueLength(0) + + // This is particularly important because pendingDABytes metric controls throttling: + s.metr.ClearAllStateMetrics() + s.txChannels = make(map[string]*channel) } diff --git a/op-batcher/batcher/channel_manager_test.go b/op-batcher/batcher/channel_manager_test.go index 8dbab2f924..2685c93595 100644 --- a/op-batcher/batcher/channel_manager_test.go +++ b/op-batcher/batcher/channel_manager_test.go @@ -122,7 +122,7 @@ func ChannelManager_Clear(t *testing.T, batchType uint) { // clearing confirmed transactions, and resetting the pendingChannels map cfg.ChannelTimeout = 10 cfg.InitRatioCompressor(1, derive.Zlib) - m := NewChannelManager(log, metrics.NoopMetrics, cfg, defaultTestRollupConfig) + m := NewChannelManager(log, metrics.NewMetrics("test"), cfg, defaultTestRollupConfig) // Channel Manager state should be empty by default require.Empty(m.blocks) @@ -150,7 +150,6 @@ func ChannelManager_Clear(t *testing.T, batchType uint) { // Process the blocks // We should have a pending channel with 1 frame - require.NoError(m.processBlocks()) require.NoError(m.currentChannel.channelBuilder.co.Flush()) require.NoError(m.outputFrames()) @@ -174,6 +173,11 @@ func ChannelManager_Clear(t *testing.T, batchType uint) { safeL1Origin := eth.BlockID{ Number: 123, } + + // Artificially pump up some metrics which need to be cleared + m.metr.RecordL2BlockInPendingQueue(a) + require.NotZero(m.metr.PendingDABytes()) + // Clear the channel manager m.Clear(safeL1Origin) @@ -184,6 +188,7 @@ func ChannelManager_Clear(t *testing.T, batchType uint) { require.Nil(m.currentChannel) require.Empty(m.channelQueue) require.Empty(m.txChannels) + require.Zero(m.metr.PendingDABytes()) } func ChannelManager_TxResend(t *testing.T, batchType uint) { diff --git a/op-batcher/batcher/sync_actions.go b/op-batcher/batcher/sync_actions.go index a6645e6ae7..fe4abedddc 100644 --- a/op-batcher/batcher/sync_actions.go +++ b/op-batcher/batcher/sync_actions.go @@ -57,11 +57,11 @@ func computeSyncActions[T channelStatuser]( ) (syncActions, bool) { m := l.With( - "syncStatus.headL1", newSyncStatus.HeadL1, - "syncStatus.currentL1", newSyncStatus.CurrentL1, - "syncStatus.localSafeL2", newSyncStatus.LocalSafeL2, - "syncStatus.safeL2", newSyncStatus.SafeL2, - "syncStatus.unsafeL2", newSyncStatus.UnsafeL2, + "syncStatus.headL1", newSyncStatus.HeadL1.TerminalString(), + "syncStatus.currentL1", newSyncStatus.CurrentL1.TerminalString(), + "syncStatus.localSafeL2", newSyncStatus.LocalSafeL2.TerminalString(), + "syncStatus.safeL2", newSyncStatus.SafeL2.TerminalString(), + "syncStatus.unsafeL2", newSyncStatus.UnsafeL2.TerminalString(), ) safeL2 := newSyncStatus.SafeL2 @@ -78,7 +78,7 @@ func computeSyncActions[T channelStatuser]( if newSyncStatus.CurrentL1.Number < prevCurrentL1.Number { // This can happen when the sequencer restarts - m.Warn("sequencer currentL1 reversed", "prevCurrentL1", prevCurrentL1) + m.Warn("sequencer currentL1 reversed", "prevCurrentL1", prevCurrentL1.TerminalString()) return syncActions{}, true } @@ -94,7 +94,7 @@ func computeSyncActions[T channelStatuser]( s := syncActions{ blocksToLoad: allUnsafeBlocks, } - m.Info("no blocks in state", "syncActions", s) + m.Info("no blocks in state", "syncActions", s.TerminalString()) return s, false } @@ -112,7 +112,7 @@ func computeSyncActions[T channelStatuser]( if nextSafeBlockNum < oldestBlockInStateNum { m.Warn("next safe block is below oldest block in state", - "syncActions", startAfresh, + "syncActions", startAfresh.TerminalString(), "oldestBlockInStateNum", oldestBlockInStateNum) return startAfresh, false } @@ -128,16 +128,16 @@ func computeSyncActions[T channelStatuser]( // The sequencer may have derived the safe chain // from channels sent by a previous batcher instance. m.Warn("safe head above newest block in state, clearing channel manager state", - "syncActions", startAfresh, - "newestBlockInState", eth.ToBlockID(newestBlockInState), + "syncActions", startAfresh.TerminalString(), + "newestBlockInState", eth.ToBlockID(newestBlockInState).TerminalString(), ) return startAfresh, false } if numBlocksToDequeue > 0 && blocks[numBlocksToDequeue-1].Hash() != safeL2.Hash { m.Warn("safe chain reorg, clearing channel manager state", - "syncActions", startAfresh, - "existingBlock", eth.ToBlockID(blocks[numBlocksToDequeue-1])) + "syncActions", startAfresh.TerminalString(), + "existingBlock", eth.ToBlockID(blocks[numBlocksToDequeue-1]).TerminalString()) return startAfresh, false } @@ -152,8 +152,8 @@ func computeSyncActions[T channelStatuser]( // that the derivation pipeline may have stalled // e.g. because of Holocene strict ordering rules. m.Warn("sequencer did not make expected progress", - "syncActions", startAfresh, - "existingBlock", ch.LatestL2()) + "syncActions", startAfresh.TerminalString(), + "existingBlock", ch.LatestL2().TerminalString()) return startAfresh, false } } @@ -179,6 +179,6 @@ func computeSyncActions[T channelStatuser]( channelsToPrune: numChannelsToPrune, blocksToLoad: allUnsafeBlocksAboveState, } - m.Debug("computed sync actions", "syncActions", a) + m.Debug("computed sync actions", "syncActions", a.TerminalString()) return a, false } diff --git a/op-batcher/metrics/metrics.go b/op-batcher/metrics/metrics.go index 68033f376b..4eee89e518 100644 --- a/op-batcher/metrics/metrics.go +++ b/op-batcher/metrics/metrics.go @@ -44,6 +44,10 @@ type Metricer interface { RecordChannelTimedOut(id derive.ChannelID) RecordChannelQueueLength(len int) + // ClearAllStateMetrics resets any metrics that track current ChannelManager state + // It should be called when clearing the ChannelManager state. + ClearAllStateMetrics() + RecordBatchTxSubmitted() RecordBatchTxSuccess() RecordBatchTxFailed() @@ -349,6 +353,17 @@ func (m *Metrics) RecordChannelQueueLength(len int) { m.channelQueueLength.Set(float64(len)) } +// ClearAllStateMetrics clears all state metrics. +// +// This should cover any metric which is a Gauge and is incremented / decremented rather than "set". +// Counter Metrics only ever go up, so they can't be reset and shouldn't be. +// Gauge Metrics which are "set" will get the right value the next time they are updated and don't need to be reset. +func (m *Metrics) ClearAllStateMetrics() { + m.RecordChannelQueueLength(0) + atomic.StoreInt64(&m.pendingDABytes, 0) + m.pendingBlocksBytesCurrent.Set(0) +} + // estimateBatchSize returns the estimated size of the block in a batch both with compression ('daSize') and without // ('rawSize'). func estimateBatchSize(block *types.Block) (daSize, rawSize uint64) { diff --git a/op-batcher/metrics/noop.go b/op-batcher/metrics/noop.go index 9fb7b0342a..fc6795058f 100644 --- a/op-batcher/metrics/noop.go +++ b/op-batcher/metrics/noop.go @@ -61,3 +61,5 @@ type ThrottlingMetrics struct { func (nm *ThrottlingMetrics) PendingDABytes() float64 { return math.MaxFloat64 } + +func (*noopMetrics) ClearAllStateMetrics() {} diff --git a/op-batcher/metrics/test.go b/op-batcher/metrics/test.go index 7e2ce29597..dc4c759ab5 100644 --- a/op-batcher/metrics/test.go +++ b/op-batcher/metrics/test.go @@ -8,19 +8,29 @@ type TestMetrics struct { noopMetrics PendingBlocksBytesCurrent float64 ChannelQueueLength int + pendingDABytes float64 } var _ Metricer = new(TestMetrics) func (m *TestMetrics) RecordL2BlockInPendingQueue(block *types.Block) { - _, rawSize := estimateBatchSize(block) + daSize, rawSize := estimateBatchSize(block) m.PendingBlocksBytesCurrent += float64(rawSize) - + m.pendingDABytes += float64(daSize) } func (m *TestMetrics) RecordL2BlockInChannel(block *types.Block) { - _, rawSize := estimateBatchSize(block) + daSize, rawSize := estimateBatchSize(block) m.PendingBlocksBytesCurrent -= float64(rawSize) + m.pendingDABytes -= float64(daSize) } func (m *TestMetrics) RecordChannelQueueLength(l int) { m.ChannelQueueLength = l } +func (m *TestMetrics) PendingDABytes() float64 { + return m.pendingDABytes +} +func (m *TestMetrics) ClearAllStateMetrics() { + m.PendingBlocksBytesCurrent = 0 + m.ChannelQueueLength = 0 + m.pendingDABytes = 0 +} diff --git a/op-chain-ops/cmd/check-derivation/main.go b/op-chain-ops/cmd/check-derivation/main.go index 71c0fec1b9..026e821c01 100644 --- a/op-chain-ops/cmd/check-derivation/main.go +++ b/op-chain-ops/cmd/check-derivation/main.go @@ -18,6 +18,7 @@ import ( "github.com/ethereum-optimism/optimism/op-service/retry" "github.com/ethereum-optimism/optimism/op-service/sources" "github.com/ethereum-optimism/optimism/op-service/testutils" + "github.com/holiman/uint256" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" @@ -275,13 +276,33 @@ func getRandomSignedTransaction(ctx context.Context, ethClient *ethclient.Client Value: amount, Data: data, } + + case types.SetCodeTxType: + gasLimit, err := core.FloorDataGas(data) + if err != nil { + return nil, fmt.Errorf("failed to get intrinsicGas: %w", err) + } + gasTipCap, err := ethClient.SuggestGasTipCap(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get gas tip cap: %w", err) + } + txData = &types.SetCodeTx{ + ChainID: uint256.MustFromBig(chainId), + Nonce: nonce, + GasTipCap: uint256.MustFromBig(gasTipCap), + GasFeeCap: uint256.MustFromBig(gasPrice), + Gas: gasLimit, + To: randomAddress, + Value: uint256.MustFromBig(amount), + Data: data, + } default: return nil, fmt.Errorf("unsupported tx type: %d", txType) } tx := types.NewTx(txData) - signer := types.NewLondonSigner(chainId) + signer := types.NewIsthmusSigner(chainId) if !protected { if txType == types.LegacyTxType { signer = types.HomesteadSigner{} @@ -359,7 +380,7 @@ func checkConsolidation(cliCtx *cli.Context) error { txType := types.LegacyTxType protected := true // Generate all tx types alternately - switch i % 4 { + switch i % 5 { case 0: protected = false // legacy unprotected TX (Homestead) case 1: @@ -368,6 +389,8 @@ func checkConsolidation(cliCtx *cli.Context) error { txType = types.AccessListTxType case 3: txType = types.DynamicFeeTxType + case 4: + txType = types.SetCodeTxType } tx, err := getRandomSignedTransaction(ctx, cl, rng, from, privateKey, l2ChainID, txType, protected) if err != nil { diff --git a/op-chain-ops/cmd/check-ecotone/main.go b/op-chain-ops/cmd/check-ecotone/main.go index b49bb0de33..37e590c47f 100644 --- a/op-chain-ops/cmd/check-ecotone/main.go +++ b/op-chain-ops/cmd/check-ecotone/main.go @@ -786,7 +786,7 @@ func checkL1Fees(ctx context.Context, env *actionEnv) error { Data: []byte("hello"), AccessList: nil, } - tx, err := types.SignNewTx(env.key, types.NewLondonSigner(txData.ChainID), txData) + tx, err := types.SignNewTx(env.key, types.NewIsthmusSigner(txData.ChainID), txData) if err != nil { return fmt.Errorf("failed to sign test tx: %w", err) } diff --git a/op-chain-ops/foundry/artifact.go b/op-chain-ops/foundry/artifact.go index 70e3085b09..a3e0a6e076 100644 --- a/op-chain-ops/foundry/artifact.go +++ b/op-chain-ops/foundry/artifact.go @@ -59,6 +59,7 @@ func (a Artifact) MarshalJSON() ([]byte, error) { // foundry artifacts. type artifactMarshaling struct { ABI json.RawMessage `json:"abi"` + Source string `json:"source"` StorageLayout solc.StorageLayout `json:"storageLayout"` DeployedBytecode DeployedBytecode `json:"deployedBytecode"` Bytecode Bytecode `json:"bytecode"` @@ -77,7 +78,7 @@ type Metadata struct { Settings struct { // Remappings of the contract imports - Remappings json.RawMessage `json:"remappings"` + Remappings []string `json:"remappings"` // Optimizer settings affect the compiler output, but can be arbitrary. // We load them opaquely, to include it in the hash of what we run. Optimizer json.RawMessage `json:"optimizer"` @@ -102,6 +103,7 @@ type Metadata struct { type ContractSource struct { Keccak256 common.Hash `json:"keccak256"` URLs []string `json:"urls"` + Content string `json:"content"` License string `json:"license"` } @@ -153,3 +155,27 @@ func ReadArtifact(path string) (*Artifact, error) { } return &artifact, nil } + +// SearchRemappings applies the configured remappings to a given source path, +// or returns the source path unchanged if no remapping is found. It assumes that +// each remapping is of the form "alias/=actualPath". +func (a Artifact) SearchRemappings(sourcePath string) string { + for _, mapping := range a.Metadata.Settings.Remappings { + parts := strings.Split(mapping, "/=") + if len(parts) != 2 { + continue + } + alias := parts[0] + if !strings.HasSuffix(alias, "/") { + alias += "/" + } + actualPath := parts[1] + if !strings.HasSuffix(actualPath, "/") { + actualPath += "/" + } + if strings.HasPrefix(sourcePath, actualPath) { + return alias + sourcePath[len(actualPath):] + } + } + return sourcePath +} diff --git a/op-chain-ops/foundry/testdata/forge-artifacts/Owned.sol/Owned.json b/op-chain-ops/foundry/testdata/forge-artifacts/Owned.sol/Owned.json index 2646394b63..f8f68822ae 100644 --- a/op-chain-ops/foundry/testdata/forge-artifacts/Owned.sol/Owned.json +++ b/op-chain-ops/foundry/testdata/forge-artifacts/Owned.sol/Owned.json @@ -1 +1 @@ -{"abi":[{"type":"function","name":"owner","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"setOwner","inputs":[{"name":"newOwner","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"OwnerUpdated","inputs":[{"name":"user","type":"address","indexed":true,"internalType":"address"},{"name":"newOwner","type":"address","indexed":true,"internalType":"address"}],"anonymous":false}],"bytecode":{"object":"0x","sourceMap":"","linkReferences":{}},"deployedBytecode":{"object":"0x","sourceMap":"","linkReferences":{}},"methodIdentifiers":{"owner()":"8da5cb5b","setOwner(address)":"13af4035"},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.15+commit.e14f2714\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"user\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"OwnerUpdated\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"setOwner\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}],\"devdoc\":{\"author\":\"Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/auth/Owned.sol)\",\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"notice\":\"Simple single owner authorization mixin.\",\"version\":1}},\"settings\":{\"compilationTarget\":{\"lib/solmate/src/auth/Owned.sol\":\"Owned\"},\"evmVersion\":\"london\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"none\"},\"optimizer\":{\"enabled\":true,\"runs\":999999},\"remappings\":[\":@lib-keccak/=lib/lib-keccak/contracts/lib/\",\":@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/\",\":@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/\",\":@rari-capital/solmate/=lib/solmate/\",\":@solady-test/=lib/lib-keccak/lib/solady/test/\",\":@solady/=lib/solady/src/\",\":automate/=lib/automate/contracts/\",\":ds-test/=lib/forge-std/lib/ds-test/src/\",\":erc4626-tests/=lib/automate/lib/openzeppelin-contracts/lib/erc4626-tests/\",\":forge-std/=lib/forge-std/src/\",\":gelato/=lib/automate/contracts/\",\":hardhat/=lib/automate/node_modules/hardhat/\",\":kontrol-cheatcodes/=lib/kontrol-cheatcodes/src/\",\":lib-keccak/=lib/lib-keccak/contracts/\",\":openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/\",\":openzeppelin-contracts/=lib/openzeppelin-contracts/\",\":prb-test/=lib/automate/lib/prb-test/src/\",\":prb/-est/=lib/automate/lib/prb-test/src/\",\":safe-contracts/=lib/safe-contracts/contracts/\",\":solady/=lib/solady/\",\":solmate/=lib/solmate/src/\"]},\"sources\":{\"lib/solmate/src/auth/Owned.sol\":{\"keccak256\":\"0x7e91c80b0dd1a14a19cb9e661b99924043adab6d9d893bbfcf3a6a3dc23a6743\",\"license\":\"AGPL-3.0-only\",\"urls\":[\"bzz-raw://515890d9fc87d6762dae2354a3a0714a26c652f0ea5bb631122be1968ef8c0e9\",\"dweb:/ipfs/QmTRpQ7uoAR1vCACKJm14Ba3oKVLqcA9reTwbHAPxawVpM\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.15+commit.e14f2714"},"language":"Solidity","output":{"abi":[{"inputs":[{"internalType":"address","name":"user","type":"address","indexed":true},{"internalType":"address","name":"newOwner","type":"address","indexed":true}],"type":"event","name":"OwnerUpdated","anonymous":false},{"inputs":[],"stateMutability":"view","type":"function","name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}]},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"stateMutability":"nonpayable","type":"function","name":"setOwner"}],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":["@lib-keccak/=lib/lib-keccak/contracts/lib/","@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/","@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/","@rari-capital/solmate/=lib/solmate/","@solady-test/=lib/lib-keccak/lib/solady/test/","@solady/=lib/solady/src/","automate/=lib/automate/contracts/","ds-test/=lib/forge-std/lib/ds-test/src/","erc4626-tests/=lib/automate/lib/openzeppelin-contracts/lib/erc4626-tests/","forge-std/=lib/forge-std/src/","gelato/=lib/automate/contracts/","hardhat/=lib/automate/node_modules/hardhat/","kontrol-cheatcodes/=lib/kontrol-cheatcodes/src/","lib-keccak/=lib/lib-keccak/contracts/","openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/","openzeppelin-contracts/=lib/openzeppelin-contracts/","prb-test/=lib/automate/lib/prb-test/src/","prb/-est/=lib/automate/lib/prb-test/src/","safe-contracts/=lib/safe-contracts/contracts/","solady/=lib/solady/","solmate/=lib/solmate/src/"],"optimizer":{"enabled":true,"runs":999999},"metadata":{"bytecodeHash":"none"},"compilationTarget":{"lib/solmate/src/auth/Owned.sol":"Owned"},"evmVersion":"london","libraries":{}},"sources":{"lib/solmate/src/auth/Owned.sol":{"keccak256":"0x7e91c80b0dd1a14a19cb9e661b99924043adab6d9d893bbfcf3a6a3dc23a6743","urls":["bzz-raw://515890d9fc87d6762dae2354a3a0714a26c652f0ea5bb631122be1968ef8c0e9","dweb:/ipfs/QmTRpQ7uoAR1vCACKJm14Ba3oKVLqcA9reTwbHAPxawVpM"],"license":"AGPL-3.0-only"}},"version":1},"storageLayout":{"storage":[{"astId":53517,"contract":"lib/solmate/src/auth/Owned.sol:Owned","label":"owner","offset":0,"slot":"0","type":"t_address"}],"types":{"t_address":{"encoding":"inplace","label":"address","numberOfBytes":"20"}}},"userdoc":{"version":1,"kind":"user","notice":"Simple single owner authorization mixin."},"devdoc":{"version":1,"kind":"dev","author":"Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/auth/Owned.sol)"},"ast":{"absolutePath":"lib/solmate/src/auth/Owned.sol","id":53567,"exportedSymbols":{"Owned":[53566]},"nodeType":"SourceUnit","src":"42:1398:68","nodes":[{"id":53508,"nodeType":"PragmaDirective","src":"42:24:68","nodes":[],"literals":["solidity",">=","0.8",".0"]},{"id":53566,"nodeType":"ContractDefinition","src":"212:1227:68","nodes":[{"id":53515,"nodeType":"EventDefinition","src":"421:67:68","nodes":[],"anonymous":false,"eventSelector":"8292fce18fa69edf4db7b94ea2e58241df0ae57f97e0a6c9b29067028bf92d76","name":"OwnerUpdated","nameLocation":"427:12:68","parameters":{"id":53514,"nodeType":"ParameterList","parameters":[{"constant":false,"id":53511,"indexed":true,"mutability":"mutable","name":"user","nameLocation":"456:4:68","nodeType":"VariableDeclaration","scope":53515,"src":"440:20:68","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"},"typeName":{"id":53510,"name":"address","nodeType":"ElementaryTypeName","src":"440:7:68","stateMutability":"nonpayable","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"visibility":"internal"},{"constant":false,"id":53513,"indexed":true,"mutability":"mutable","name":"newOwner","nameLocation":"478:8:68","nodeType":"VariableDeclaration","scope":53515,"src":"462:24:68","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"},"typeName":{"id":53512,"name":"address","nodeType":"ElementaryTypeName","src":"462:7:68","stateMutability":"nonpayable","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"visibility":"internal"}],"src":"439:48:68"}},{"id":53517,"nodeType":"VariableDeclaration","src":"679:20:68","nodes":[],"constant":false,"functionSelector":"8da5cb5b","mutability":"mutable","name":"owner","nameLocation":"694:5:68","scope":53566,"stateVariable":true,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"},"typeName":{"id":53516,"name":"address","nodeType":"ElementaryTypeName","src":"679:7:68","stateMutability":"nonpayable","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"visibility":"public"},{"id":53529,"nodeType":"ModifierDefinition","src":"706:102:68","nodes":[],"body":{"id":53528,"nodeType":"Block","src":"735:73:68","nodes":[],"statements":[{"expression":{"arguments":[{"commonType":{"typeIdentifier":"t_address","typeString":"address"},"id":53523,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"leftExpression":{"expression":{"id":53520,"name":"msg","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":-15,"src":"753:3:68","typeDescriptions":{"typeIdentifier":"t_magic_message","typeString":"msg"}},"id":53521,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"memberName":"sender","nodeType":"MemberAccess","src":"753:10:68","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"nodeType":"BinaryOperation","operator":"==","rightExpression":{"id":53522,"name":"owner","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":53517,"src":"767:5:68","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"src":"753:19:68","typeDescriptions":{"typeIdentifier":"t_bool","typeString":"bool"}},{"hexValue":"554e415554484f52495a4544","id":53524,"isConstant":false,"isLValue":false,"isPure":true,"kind":"string","lValueRequested":false,"nodeType":"Literal","src":"774:14:68","typeDescriptions":{"typeIdentifier":"t_stringliteral_269df367cd41cace5897a935d0e0858fe4543b5619d45e09af6b124c1bb3d528","typeString":"literal_string \"UNAUTHORIZED\""},"value":"UNAUTHORIZED"}],"expression":{"argumentTypes":[{"typeIdentifier":"t_bool","typeString":"bool"},{"typeIdentifier":"t_stringliteral_269df367cd41cace5897a935d0e0858fe4543b5619d45e09af6b124c1bb3d528","typeString":"literal_string \"UNAUTHORIZED\""}],"id":53519,"name":"require","nodeType":"Identifier","overloadedDeclarations":[-18,-18],"referencedDeclaration":-18,"src":"745:7:68","typeDescriptions":{"typeIdentifier":"t_function_require_pure$_t_bool_$_t_string_memory_ptr_$returns$__$","typeString":"function (bool,string memory) pure"}},"id":53525,"isConstant":false,"isLValue":false,"isPure":false,"kind":"functionCall","lValueRequested":false,"names":[],"nodeType":"FunctionCall","src":"745:44:68","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_tuple$__$","typeString":"tuple()"}},"id":53526,"nodeType":"ExpressionStatement","src":"745:44:68"},{"id":53527,"nodeType":"PlaceholderStatement","src":"800:1:68"}]},"name":"onlyOwner","nameLocation":"715:9:68","parameters":{"id":53518,"nodeType":"ParameterList","parameters":[],"src":"724:2:68"},"virtual":true,"visibility":"internal"},{"id":53547,"nodeType":"FunctionDefinition","src":"996:107:68","nodes":[],"body":{"id":53546,"nodeType":"Block","src":"1024:79:68","nodes":[],"statements":[{"expression":{"id":53536,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"leftHandSide":{"id":53534,"name":"owner","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":53517,"src":"1034:5:68","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"nodeType":"Assignment","operator":"=","rightHandSide":{"id":53535,"name":"_owner","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":53531,"src":"1042:6:68","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"src":"1034:14:68","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"id":53537,"nodeType":"ExpressionStatement","src":"1034:14:68"},{"eventCall":{"arguments":[{"arguments":[{"hexValue":"30","id":53541,"isConstant":false,"isLValue":false,"isPure":true,"kind":"number","lValueRequested":false,"nodeType":"Literal","src":"1085:1:68","typeDescriptions":{"typeIdentifier":"t_rational_0_by_1","typeString":"int_const 0"},"value":"0"}],"expression":{"argumentTypes":[{"typeIdentifier":"t_rational_0_by_1","typeString":"int_const 0"}],"id":53540,"isConstant":false,"isLValue":false,"isPure":true,"lValueRequested":false,"nodeType":"ElementaryTypeNameExpression","src":"1077:7:68","typeDescriptions":{"typeIdentifier":"t_type$_t_address_$","typeString":"type(address)"},"typeName":{"id":53539,"name":"address","nodeType":"ElementaryTypeName","src":"1077:7:68","typeDescriptions":{}}},"id":53542,"isConstant":false,"isLValue":false,"isPure":true,"kind":"typeConversion","lValueRequested":false,"names":[],"nodeType":"FunctionCall","src":"1077:10:68","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},{"id":53543,"name":"_owner","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":53531,"src":"1089:6:68","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}}],"expression":{"argumentTypes":[{"typeIdentifier":"t_address","typeString":"address"},{"typeIdentifier":"t_address","typeString":"address"}],"id":53538,"name":"OwnerUpdated","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":53515,"src":"1064:12:68","typeDescriptions":{"typeIdentifier":"t_function_event_nonpayable$_t_address_$_t_address_$returns$__$","typeString":"function (address,address)"}},"id":53544,"isConstant":false,"isLValue":false,"isPure":false,"kind":"functionCall","lValueRequested":false,"names":[],"nodeType":"FunctionCall","src":"1064:32:68","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_tuple$__$","typeString":"tuple()"}},"id":53545,"nodeType":"EmitStatement","src":"1059:37:68"}]},"implemented":true,"kind":"constructor","modifiers":[],"name":"","nameLocation":"-1:-1:-1","parameters":{"id":53532,"nodeType":"ParameterList","parameters":[{"constant":false,"id":53531,"mutability":"mutable","name":"_owner","nameLocation":"1016:6:68","nodeType":"VariableDeclaration","scope":53547,"src":"1008:14:68","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"},"typeName":{"id":53530,"name":"address","nodeType":"ElementaryTypeName","src":"1008:7:68","stateMutability":"nonpayable","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"visibility":"internal"}],"src":"1007:16:68"},"returnParameters":{"id":53533,"nodeType":"ParameterList","parameters":[],"src":"1024:0:68"},"scope":53566,"stateMutability":"nonpayable","virtual":false,"visibility":"internal"},{"id":53565,"nodeType":"FunctionDefinition","src":"1293:144:68","nodes":[],"body":{"id":53564,"nodeType":"Block","src":"1354:83:68","nodes":[],"statements":[{"expression":{"id":53556,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"leftHandSide":{"id":53554,"name":"owner","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":53517,"src":"1364:5:68","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"nodeType":"Assignment","operator":"=","rightHandSide":{"id":53555,"name":"newOwner","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":53549,"src":"1372:8:68","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"src":"1364:16:68","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"id":53557,"nodeType":"ExpressionStatement","src":"1364:16:68"},{"eventCall":{"arguments":[{"expression":{"id":53559,"name":"msg","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":-15,"src":"1409:3:68","typeDescriptions":{"typeIdentifier":"t_magic_message","typeString":"msg"}},"id":53560,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"memberName":"sender","nodeType":"MemberAccess","src":"1409:10:68","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},{"id":53561,"name":"newOwner","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":53549,"src":"1421:8:68","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}}],"expression":{"argumentTypes":[{"typeIdentifier":"t_address","typeString":"address"},{"typeIdentifier":"t_address","typeString":"address"}],"id":53558,"name":"OwnerUpdated","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":53515,"src":"1396:12:68","typeDescriptions":{"typeIdentifier":"t_function_event_nonpayable$_t_address_$_t_address_$returns$__$","typeString":"function (address,address)"}},"id":53562,"isConstant":false,"isLValue":false,"isPure":false,"kind":"functionCall","lValueRequested":false,"names":[],"nodeType":"FunctionCall","src":"1396:34:68","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_tuple$__$","typeString":"tuple()"}},"id":53563,"nodeType":"EmitStatement","src":"1391:39:68"}]},"functionSelector":"13af4035","implemented":true,"kind":"function","modifiers":[{"id":53552,"kind":"modifierInvocation","modifierName":{"id":53551,"name":"onlyOwner","nodeType":"IdentifierPath","referencedDeclaration":53529,"src":"1344:9:68"},"nodeType":"ModifierInvocation","src":"1344:9:68"}],"name":"setOwner","nameLocation":"1302:8:68","parameters":{"id":53550,"nodeType":"ParameterList","parameters":[{"constant":false,"id":53549,"mutability":"mutable","name":"newOwner","nameLocation":"1319:8:68","nodeType":"VariableDeclaration","scope":53565,"src":"1311:16:68","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"},"typeName":{"id":53548,"name":"address","nodeType":"ElementaryTypeName","src":"1311:7:68","stateMutability":"nonpayable","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"visibility":"internal"}],"src":"1310:18:68"},"returnParameters":{"id":53553,"nodeType":"ParameterList","parameters":[],"src":"1354:0:68"},"scope":53566,"stateMutability":"nonpayable","virtual":true,"visibility":"public"}],"abstract":true,"baseContracts":[],"canonicalName":"Owned","contractDependencies":[],"contractKind":"contract","documentation":{"id":53509,"nodeType":"StructuredDocumentation","src":"68:144:68","text":"@notice Simple single owner authorization mixin.\n @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/auth/Owned.sol)"},"fullyImplemented":true,"linearizedBaseContracts":[53566],"name":"Owned","nameLocation":"230:5:68","scope":53567,"usedErrors":[]}],"license":"AGPL-3.0-only"},"id":68} +{"abi":[{"type":"function","name":"owner","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"setOwner","inputs":[{"name":"newOwner","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"OwnerUpdated","inputs":[{"name":"user","type":"address","indexed":true,"internalType":"address"},{"name":"newOwner","type":"address","indexed":true,"internalType":"address"}],"anonymous":false}],"bytecode":{"object":"0x","sourceMap":"","linkReferences":{}},"deployedBytecode":{"object":"0x","sourceMap":"","linkReferences":{}},"methodIdentifiers":{"owner()":"8da5cb5b","setOwner(address)":"13af4035"},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.15+commit.e14f2714\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"user\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"OwnerUpdated\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"setOwner\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}],\"devdoc\":{\"author\":\"Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/auth/Owned.sol)\",\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"notice\":\"Simple single owner authorization mixin.\",\"version\":1}},\"settings\":{\"compilationTarget\":{\"lib/solmate/src/auth/Owned.sol\":\"Owned\"},\"evmVersion\":\"london\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"none\"},\"optimizer\":{\"enabled\":true,\"runs\":999999},\"remappings\":[\":@lib-keccak/=lib/lib-keccak/contracts/lib/\",\":@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/\",\":@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/\",\":@rari-capital/solmate/=lib/solmate/\",\":@solady-test/=lib/lib-keccak/lib/solady/test/\",\":@solady/=lib/solady/src/\",\":automate/=lib/automate/contracts/\",\":ds-test/=lib/forge-std/lib/ds-test/src/\",\":erc4626-tests/=lib/automate/lib/openzeppelin-contracts/lib/erc4626-tests/\",\":forge-std/=lib/forge-std/src/\",\":gelato/=lib/automate/contracts/\",\":hardhat/=lib/automate/node_modules/hardhat/\",\":kontrol-cheatcodes/=lib/kontrol-cheatcodes/src/\",\":lib-keccak/=lib/lib-keccak/contracts/\",\":openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/\",\":openzeppelin-contracts/=lib/openzeppelin-contracts/\",\":prb-test/=lib/automate/lib/prb-test/src/\",\":prb/-est/=lib/automate/lib/prb-test/src/\",\":safe-contracts/=lib/safe-contracts/contracts/\",\":solady/=lib/solady/\",\":solmate/=lib/solmate/src/\"]},\"sources\":{\"lib/solmate/src/auth/Owned.sol\":{\"content\":\"\",\"keccak256\":\"0x7e91c80b0dd1a14a19cb9e661b99924043adab6d9d893bbfcf3a6a3dc23a6743\",\"license\":\"AGPL-3.0-only\",\"urls\":[\"bzz-raw://515890d9fc87d6762dae2354a3a0714a26c652f0ea5bb631122be1968ef8c0e9\",\"dweb:/ipfs/QmTRpQ7uoAR1vCACKJm14Ba3oKVLqcA9reTwbHAPxawVpM\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.15+commit.e14f2714"},"language":"Solidity","output":{"abi":[{"inputs":[{"internalType":"address","name":"user","type":"address","indexed":true},{"internalType":"address","name":"newOwner","type":"address","indexed":true}],"type":"event","name":"OwnerUpdated","anonymous":false},{"inputs":[],"stateMutability":"view","type":"function","name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}]},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"stateMutability":"nonpayable","type":"function","name":"setOwner"}],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":["@lib-keccak/=lib/lib-keccak/contracts/lib/","@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/","@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/","@rari-capital/solmate/=lib/solmate/","@solady-test/=lib/lib-keccak/lib/solady/test/","@solady/=lib/solady/src/","automate/=lib/automate/contracts/","ds-test/=lib/forge-std/lib/ds-test/src/","erc4626-tests/=lib/automate/lib/openzeppelin-contracts/lib/erc4626-tests/","forge-std/=lib/forge-std/src/","gelato/=lib/automate/contracts/","hardhat/=lib/automate/node_modules/hardhat/","kontrol-cheatcodes/=lib/kontrol-cheatcodes/src/","lib-keccak/=lib/lib-keccak/contracts/","openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/","openzeppelin-contracts/=lib/openzeppelin-contracts/","prb-test/=lib/automate/lib/prb-test/src/","prb/-est/=lib/automate/lib/prb-test/src/","safe-contracts/=lib/safe-contracts/contracts/","solady/=lib/solady/","solmate/=lib/solmate/src/"],"optimizer":{"enabled":true,"runs":999999},"metadata":{"bytecodeHash":"none"},"compilationTarget":{"lib/solmate/src/auth/Owned.sol":"Owned"},"evmVersion":"london","libraries":{}},"sources":{"lib/solmate/src/auth/Owned.sol":{"content":"","keccak256":"0x7e91c80b0dd1a14a19cb9e661b99924043adab6d9d893bbfcf3a6a3dc23a6743","urls":["bzz-raw://515890d9fc87d6762dae2354a3a0714a26c652f0ea5bb631122be1968ef8c0e9","dweb:/ipfs/QmTRpQ7uoAR1vCACKJm14Ba3oKVLqcA9reTwbHAPxawVpM"],"license":"AGPL-3.0-only"}},"version":1},"storageLayout":{"storage":[{"astId":53517,"contract":"lib/solmate/src/auth/Owned.sol:Owned","label":"owner","offset":0,"slot":"0","type":"t_address"}],"types":{"t_address":{"encoding":"inplace","label":"address","numberOfBytes":"20"}}},"userdoc":{"version":1,"kind":"user","notice":"Simple single owner authorization mixin."},"devdoc":{"version":1,"kind":"dev","author":"Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/auth/Owned.sol)"},"ast":{"absolutePath":"lib/solmate/src/auth/Owned.sol","id":53567,"exportedSymbols":{"Owned":[53566]},"nodeType":"SourceUnit","src":"42:1398:68","nodes":[{"id":53508,"nodeType":"PragmaDirective","src":"42:24:68","nodes":[],"literals":["solidity",">=","0.8",".0"]},{"id":53566,"nodeType":"ContractDefinition","src":"212:1227:68","nodes":[{"id":53515,"nodeType":"EventDefinition","src":"421:67:68","nodes":[],"anonymous":false,"eventSelector":"8292fce18fa69edf4db7b94ea2e58241df0ae57f97e0a6c9b29067028bf92d76","name":"OwnerUpdated","nameLocation":"427:12:68","parameters":{"id":53514,"nodeType":"ParameterList","parameters":[{"constant":false,"id":53511,"indexed":true,"mutability":"mutable","name":"user","nameLocation":"456:4:68","nodeType":"VariableDeclaration","scope":53515,"src":"440:20:68","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"},"typeName":{"id":53510,"name":"address","nodeType":"ElementaryTypeName","src":"440:7:68","stateMutability":"nonpayable","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"visibility":"internal"},{"constant":false,"id":53513,"indexed":true,"mutability":"mutable","name":"newOwner","nameLocation":"478:8:68","nodeType":"VariableDeclaration","scope":53515,"src":"462:24:68","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"},"typeName":{"id":53512,"name":"address","nodeType":"ElementaryTypeName","src":"462:7:68","stateMutability":"nonpayable","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"visibility":"internal"}],"src":"439:48:68"}},{"id":53517,"nodeType":"VariableDeclaration","src":"679:20:68","nodes":[],"constant":false,"functionSelector":"8da5cb5b","mutability":"mutable","name":"owner","nameLocation":"694:5:68","scope":53566,"stateVariable":true,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"},"typeName":{"id":53516,"name":"address","nodeType":"ElementaryTypeName","src":"679:7:68","stateMutability":"nonpayable","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"visibility":"public"},{"id":53529,"nodeType":"ModifierDefinition","src":"706:102:68","nodes":[],"body":{"id":53528,"nodeType":"Block","src":"735:73:68","nodes":[],"statements":[{"expression":{"arguments":[{"commonType":{"typeIdentifier":"t_address","typeString":"address"},"id":53523,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"leftExpression":{"expression":{"id":53520,"name":"msg","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":-15,"src":"753:3:68","typeDescriptions":{"typeIdentifier":"t_magic_message","typeString":"msg"}},"id":53521,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"memberName":"sender","nodeType":"MemberAccess","src":"753:10:68","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"nodeType":"BinaryOperation","operator":"==","rightExpression":{"id":53522,"name":"owner","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":53517,"src":"767:5:68","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"src":"753:19:68","typeDescriptions":{"typeIdentifier":"t_bool","typeString":"bool"}},{"hexValue":"554e415554484f52495a4544","id":53524,"isConstant":false,"isLValue":false,"isPure":true,"kind":"string","lValueRequested":false,"nodeType":"Literal","src":"774:14:68","typeDescriptions":{"typeIdentifier":"t_stringliteral_269df367cd41cace5897a935d0e0858fe4543b5619d45e09af6b124c1bb3d528","typeString":"literal_string \"UNAUTHORIZED\""},"value":"UNAUTHORIZED"}],"expression":{"argumentTypes":[{"typeIdentifier":"t_bool","typeString":"bool"},{"typeIdentifier":"t_stringliteral_269df367cd41cace5897a935d0e0858fe4543b5619d45e09af6b124c1bb3d528","typeString":"literal_string \"UNAUTHORIZED\""}],"id":53519,"name":"require","nodeType":"Identifier","overloadedDeclarations":[-18,-18],"referencedDeclaration":-18,"src":"745:7:68","typeDescriptions":{"typeIdentifier":"t_function_require_pure$_t_bool_$_t_string_memory_ptr_$returns$__$","typeString":"function (bool,string memory) pure"}},"id":53525,"isConstant":false,"isLValue":false,"isPure":false,"kind":"functionCall","lValueRequested":false,"names":[],"nodeType":"FunctionCall","src":"745:44:68","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_tuple$__$","typeString":"tuple()"}},"id":53526,"nodeType":"ExpressionStatement","src":"745:44:68"},{"id":53527,"nodeType":"PlaceholderStatement","src":"800:1:68"}]},"name":"onlyOwner","nameLocation":"715:9:68","parameters":{"id":53518,"nodeType":"ParameterList","parameters":[],"src":"724:2:68"},"virtual":true,"visibility":"internal"},{"id":53547,"nodeType":"FunctionDefinition","src":"996:107:68","nodes":[],"body":{"id":53546,"nodeType":"Block","src":"1024:79:68","nodes":[],"statements":[{"expression":{"id":53536,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"leftHandSide":{"id":53534,"name":"owner","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":53517,"src":"1034:5:68","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"nodeType":"Assignment","operator":"=","rightHandSide":{"id":53535,"name":"_owner","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":53531,"src":"1042:6:68","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"src":"1034:14:68","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"id":53537,"nodeType":"ExpressionStatement","src":"1034:14:68"},{"eventCall":{"arguments":[{"arguments":[{"hexValue":"30","id":53541,"isConstant":false,"isLValue":false,"isPure":true,"kind":"number","lValueRequested":false,"nodeType":"Literal","src":"1085:1:68","typeDescriptions":{"typeIdentifier":"t_rational_0_by_1","typeString":"int_const 0"},"value":"0"}],"expression":{"argumentTypes":[{"typeIdentifier":"t_rational_0_by_1","typeString":"int_const 0"}],"id":53540,"isConstant":false,"isLValue":false,"isPure":true,"lValueRequested":false,"nodeType":"ElementaryTypeNameExpression","src":"1077:7:68","typeDescriptions":{"typeIdentifier":"t_type$_t_address_$","typeString":"type(address)"},"typeName":{"id":53539,"name":"address","nodeType":"ElementaryTypeName","src":"1077:7:68","typeDescriptions":{}}},"id":53542,"isConstant":false,"isLValue":false,"isPure":true,"kind":"typeConversion","lValueRequested":false,"names":[],"nodeType":"FunctionCall","src":"1077:10:68","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},{"id":53543,"name":"_owner","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":53531,"src":"1089:6:68","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}}],"expression":{"argumentTypes":[{"typeIdentifier":"t_address","typeString":"address"},{"typeIdentifier":"t_address","typeString":"address"}],"id":53538,"name":"OwnerUpdated","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":53515,"src":"1064:12:68","typeDescriptions":{"typeIdentifier":"t_function_event_nonpayable$_t_address_$_t_address_$returns$__$","typeString":"function (address,address)"}},"id":53544,"isConstant":false,"isLValue":false,"isPure":false,"kind":"functionCall","lValueRequested":false,"names":[],"nodeType":"FunctionCall","src":"1064:32:68","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_tuple$__$","typeString":"tuple()"}},"id":53545,"nodeType":"EmitStatement","src":"1059:37:68"}]},"implemented":true,"kind":"constructor","modifiers":[],"name":"","nameLocation":"-1:-1:-1","parameters":{"id":53532,"nodeType":"ParameterList","parameters":[{"constant":false,"id":53531,"mutability":"mutable","name":"_owner","nameLocation":"1016:6:68","nodeType":"VariableDeclaration","scope":53547,"src":"1008:14:68","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"},"typeName":{"id":53530,"name":"address","nodeType":"ElementaryTypeName","src":"1008:7:68","stateMutability":"nonpayable","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"visibility":"internal"}],"src":"1007:16:68"},"returnParameters":{"id":53533,"nodeType":"ParameterList","parameters":[],"src":"1024:0:68"},"scope":53566,"stateMutability":"nonpayable","virtual":false,"visibility":"internal"},{"id":53565,"nodeType":"FunctionDefinition","src":"1293:144:68","nodes":[],"body":{"id":53564,"nodeType":"Block","src":"1354:83:68","nodes":[],"statements":[{"expression":{"id":53556,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"leftHandSide":{"id":53554,"name":"owner","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":53517,"src":"1364:5:68","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"nodeType":"Assignment","operator":"=","rightHandSide":{"id":53555,"name":"newOwner","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":53549,"src":"1372:8:68","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"src":"1364:16:68","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"id":53557,"nodeType":"ExpressionStatement","src":"1364:16:68"},{"eventCall":{"arguments":[{"expression":{"id":53559,"name":"msg","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":-15,"src":"1409:3:68","typeDescriptions":{"typeIdentifier":"t_magic_message","typeString":"msg"}},"id":53560,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"memberName":"sender","nodeType":"MemberAccess","src":"1409:10:68","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},{"id":53561,"name":"newOwner","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":53549,"src":"1421:8:68","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}}],"expression":{"argumentTypes":[{"typeIdentifier":"t_address","typeString":"address"},{"typeIdentifier":"t_address","typeString":"address"}],"id":53558,"name":"OwnerUpdated","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":53515,"src":"1396:12:68","typeDescriptions":{"typeIdentifier":"t_function_event_nonpayable$_t_address_$_t_address_$returns$__$","typeString":"function (address,address)"}},"id":53562,"isConstant":false,"isLValue":false,"isPure":false,"kind":"functionCall","lValueRequested":false,"names":[],"nodeType":"FunctionCall","src":"1396:34:68","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_tuple$__$","typeString":"tuple()"}},"id":53563,"nodeType":"EmitStatement","src":"1391:39:68"}]},"functionSelector":"13af4035","implemented":true,"kind":"function","modifiers":[{"id":53552,"kind":"modifierInvocation","modifierName":{"id":53551,"name":"onlyOwner","nodeType":"IdentifierPath","referencedDeclaration":53529,"src":"1344:9:68"},"nodeType":"ModifierInvocation","src":"1344:9:68"}],"name":"setOwner","nameLocation":"1302:8:68","parameters":{"id":53550,"nodeType":"ParameterList","parameters":[{"constant":false,"id":53549,"mutability":"mutable","name":"newOwner","nameLocation":"1319:8:68","nodeType":"VariableDeclaration","scope":53565,"src":"1311:16:68","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"},"typeName":{"id":53548,"name":"address","nodeType":"ElementaryTypeName","src":"1311:7:68","stateMutability":"nonpayable","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"visibility":"internal"}],"src":"1310:18:68"},"returnParameters":{"id":53553,"nodeType":"ParameterList","parameters":[],"src":"1354:0:68"},"scope":53566,"stateMutability":"nonpayable","virtual":true,"visibility":"public"}],"abstract":true,"baseContracts":[],"canonicalName":"Owned","contractDependencies":[],"contractKind":"contract","documentation":{"id":53509,"nodeType":"StructuredDocumentation","src":"68:144:68","text":"@notice Simple single owner authorization mixin.\n @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/auth/Owned.sol)"},"fullyImplemented":true,"linearizedBaseContracts":[53566],"name":"Owned","nameLocation":"230:5:68","scope":53567,"usedErrors":[]}],"license":"AGPL-3.0-only"},"id":68} diff --git a/op-chain-ops/genesis/config.go b/op-chain-ops/genesis/config.go index 3cd7f9d677..4cae9aef4e 100644 --- a/op-chain-ops/genesis/config.go +++ b/op-chain-ops/genesis/config.go @@ -12,7 +12,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" @@ -364,6 +363,12 @@ type UpgradeScheduleDeployConfig struct { // Set it to 0 to activate at genesis. Nil to disable Interop. L2GenesisInteropTimeOffset *hexutil.Uint64 `json:"l2GenesisInteropTimeOffset,omitempty"` + // Optional Forks + + // L2GenesisPectraBlobScheduleTimeOffset is the number of seconds after genesis block that the PectraBlobSchedule fix activates. + // Set it to 0 to activate at genesis. Nil to disable the PectraBlobSchedule fix. + L2GenesisPectraBlobScheduleTimeOffset *hexutil.Uint64 `json:"l2GenesisPectraBlobScheduleTimeOffset,omitempty"` + // When Cancun activates. Relative to L1 genesis. L1CancunTimeOffset *hexutil.Uint64 `json:"l1CancunTimeOffset,omitempty"` // When Prague activates. Relative to L1 genesis. @@ -493,6 +498,10 @@ func (d *UpgradeScheduleDeployConfig) HoloceneTime(genesisTime uint64) *uint64 { return offsetToUpgradeTime(d.L2GenesisHoloceneTimeOffset, genesisTime) } +func (d *UpgradeScheduleDeployConfig) PectraBlobScheduleTime(genesisTime uint64) *uint64 { + return offsetToUpgradeTime(d.L2GenesisPectraBlobScheduleTimeOffset, genesisTime) +} + func (d *UpgradeScheduleDeployConfig) IsthmusTime(genesisTime uint64) *uint64 { return offsetToUpgradeTime(d.L2GenesisIsthmusTimeOffset, genesisTime) } @@ -981,7 +990,7 @@ func (d *DeployConfig) SetDeployments(deployments *L1Deployments) { // RollupConfig converts a DeployConfig to a rollup.Config. If Ecotone is active at genesis, the // Overhead value is considered a noop. -func (d *DeployConfig) RollupConfig(l1StartBlock *types.Header, l2GenesisBlockHash common.Hash, l2GenesisBlockNumber uint64) (*rollup.Config, error) { +func (d *DeployConfig) RollupConfig(l1StartBlock *eth.BlockRef, l2GenesisBlockHash common.Hash, l2GenesisBlockNumber uint64) (*rollup.Config, error) { if d.OptimismPortalProxy == (common.Address{}) { return nil, errors.New("OptimismPortalProxy cannot be address(0)") } @@ -1010,8 +1019,8 @@ func (d *DeployConfig) RollupConfig(l1StartBlock *types.Header, l2GenesisBlockHa return &rollup.Config{ Genesis: rollup.Genesis{ L1: eth.BlockID{ - Hash: l1StartBlock.Hash(), - Number: l1StartBlock.Number.Uint64(), + Hash: l1StartBlock.Hash, + Number: l1StartBlock.Number, }, L2: eth.BlockID{ Hash: l2GenesisBlockHash, @@ -1036,6 +1045,7 @@ func (d *DeployConfig) RollupConfig(l1StartBlock *types.Header, l2GenesisBlockHa FjordTime: d.FjordTime(l1StartTime), GraniteTime: d.GraniteTime(l1StartTime), HoloceneTime: d.HoloceneTime(l1StartTime), + PectraBlobScheduleTime: d.PectraBlobScheduleTime(l1StartTime), IsthmusTime: d.IsthmusTime(l1StartTime), InteropTime: d.InteropTime(l1StartTime), ProtocolVersionsAddress: d.ProtocolVersionsProxy, @@ -1100,6 +1110,8 @@ type L1Deployments struct { OptimismMintableERC20FactoryProxy common.Address `json:"OptimismMintableERC20FactoryProxy"` OptimismPortal common.Address `json:"OptimismPortal"` OptimismPortalProxy common.Address `json:"OptimismPortalProxy"` + ETHLockbox common.Address `json:"ETHLockbox"` + ETHLockboxProxy common.Address `json:"ETHLockboxProxy"` ProxyAdmin common.Address `json:"ProxyAdmin"` SystemConfig common.Address `json:"SystemConfig"` SystemConfigProxy common.Address `json:"SystemConfigProxy"` @@ -1145,6 +1157,7 @@ func (d *L1Deployments) Check(deployConfig *DeployConfig) error { name == "DataAvailabilityChallengeProxy") { continue } + if val.Field(i).Interface().(common.Address) == (common.Address{}) { return fmt.Errorf("%s is not set", name) } diff --git a/op-chain-ops/genesis/genesis.go b/op-chain-ops/genesis/genesis.go index 091adf15c5..8337c598ce 100644 --- a/op-chain-ops/genesis/genesis.go +++ b/op-chain-ops/genesis/genesis.go @@ -5,6 +5,7 @@ import ( "math/big" "time" + "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/predeploys" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" @@ -21,7 +22,7 @@ const defaultGasLimit = 30_000_000 var HoloceneExtraData = eip1559.EncodeHoloceneExtraData(250, 6) // NewL2Genesis will create a new L2 genesis -func NewL2Genesis(config *DeployConfig, l1StartHeader *types.Header) (*core.Genesis, error) { +func NewL2Genesis(config *DeployConfig, l1StartHeader *eth.BlockRef) (*core.Genesis, error) { if config.L2ChainID == 0 { return nil, errors.New("must define L2 ChainID") } diff --git a/op-chain-ops/genesis/layer_two.go b/op-chain-ops/genesis/layer_two.go index de30a38a1c..f698f22b7e 100644 --- a/op-chain-ops/genesis/layer_two.go +++ b/op-chain-ops/genesis/layer_two.go @@ -14,6 +14,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum-optimism/optimism/op-chain-ops/foundry" + "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/predeploys" ) @@ -40,7 +41,7 @@ var ( type AllocsLoader func(mode L2AllocsMode) *foundry.ForgeAllocs // BuildL2Genesis will build the L2 genesis block. -func BuildL2Genesis(config *DeployConfig, dump *foundry.ForgeAllocs, l1StartBlock *types.Header) (*core.Genesis, error) { +func BuildL2Genesis(config *DeployConfig, dump *foundry.ForgeAllocs, l1StartBlock *eth.BlockRef) (*core.Genesis, error) { genspec, err := NewL2Genesis(config, l1StartBlock) if err != nil { return nil, err diff --git a/op-chain-ops/interopgen/deploy.go b/op-chain-ops/interopgen/deploy.go index 4ef1d32b65..fd11b9e0c1 100644 --- a/op-chain-ops/interopgen/deploy.go +++ b/op-chain-ops/interopgen/deploy.go @@ -6,6 +6,7 @@ import ( "math/big" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm" + "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -298,7 +299,7 @@ func CompleteL2(l2Host *script.Host, cfg *L2Config, l1Block *types.Block, deploy }, } // l1Block is used to determine genesis time. - l2Genesis, err := genesis.NewL2Genesis(deployCfg, l1Block.Header()) + l2Genesis, err := genesis.NewL2Genesis(deployCfg, eth.BlockRefFromHeader(l1Block.Header())) if err != nil { return nil, fmt.Errorf("failed to build L2 genesis config: %w", err) } @@ -325,7 +326,7 @@ func CompleteL2(l2Host *script.Host, cfg *L2Config, l1Block *types.Block, deploy } l2GenesisBlock := l2Genesis.ToBlock() - rollupCfg, err := deployCfg.RollupConfig(l1Block.Header(), l2GenesisBlock.Hash(), l2GenesisBlock.NumberU64()) + rollupCfg, err := deployCfg.RollupConfig(eth.BlockRefFromHeader(l1Block.Header()), l2GenesisBlock.Hash(), l2GenesisBlock.NumberU64()) if err != nil { return nil, fmt.Errorf("failed to build L2 rollup config: %w", err) } diff --git a/op-chain-ops/interopgen/deployments.go b/op-chain-ops/interopgen/deployments.go index 6b9e34b7a8..48a2b93b22 100644 --- a/op-chain-ops/interopgen/deployments.go +++ b/op-chain-ops/interopgen/deployments.go @@ -16,6 +16,7 @@ type Implementations struct { OpcmUpgrader common.Address `json:"OPCMUpgrader"` DelayedWETHImpl common.Address `json:"DelayedWETHImpl"` OptimismPortalImpl common.Address `json:"OptimismPortalImpl"` + ETHLockboxImpl common.Address `json:"ETHLockboxImpl"` PreimageOracleSingleton common.Address `json:"PreimageOracleSingleton"` MipsSingleton common.Address `json:"MipsSingleton"` SystemConfigImpl common.Address `json:"SystemConfigImpl"` @@ -51,6 +52,7 @@ type L2OpchainDeployment struct { L1CrossDomainMessengerProxy common.Address `json:"L1CrossDomainMessengerProxy"` // Fault proof contracts below. OptimismPortalProxy common.Address `json:"OptimismPortalProxy"` + ETHLockboxProxy common.Address `json:"ETHLockboxProxy"` DisputeGameFactoryProxy common.Address `json:"DisputeGameFactoryProxy"` AnchorStateRegistryProxy common.Address `json:"AnchorStateRegistryProxy"` FaultDisputeGame common.Address `json:"FaultDisputeGame"` diff --git a/op-chain-ops/script/cheatcodes_external.go b/op-chain-ops/script/cheatcodes_external.go index afb42de58f..5b1b125d4e 100644 --- a/op-chain-ops/script/cheatcodes_external.go +++ b/op-chain-ops/script/cheatcodes_external.go @@ -137,51 +137,51 @@ func envOrList[E any](key string, } func (c *CheatCodesPrecompile) EnvOr_4777f3cf(key string, defaultValue bool) (bool, error) { - return envOrSingular[bool](key, c.h.GetEnvVar, c.ParseBool, defaultValue) + return envOrSingular(key, c.h.GetEnvVar, c.ParseBool, defaultValue) } func (c *CheatCodesPrecompile) EnvOr_5e97348f(key string, defaultValue *big.Int) (*big.Int, error) { - return envOrSingular[*big.Int](key, c.h.GetEnvVar, c.ParseUint, defaultValue) + return envOrSingular(key, c.h.GetEnvVar, c.ParseUint, defaultValue) } func (c *CheatCodesPrecompile) EnvOr_bbcb713e(key string, defaultValue *ABIInt256) (*ABIInt256, error) { - return envOrSingular[*ABIInt256](key, c.h.GetEnvVar, c.ParseInt, defaultValue) + return envOrSingular(key, c.h.GetEnvVar, c.ParseInt, defaultValue) } func (c *CheatCodesPrecompile) EnvOr_561fe540(key string, defaultValue common.Address) (common.Address, error) { - return envOrSingular[common.Address](key, c.h.GetEnvVar, c.ParseAddress, defaultValue) + return envOrSingular(key, c.h.GetEnvVar, c.ParseAddress, defaultValue) } func (c *CheatCodesPrecompile) EnvOr_b4a85892(key string, defaultValue [32]byte) ([32]byte, error) { - return envOrSingular[[32]byte](key, c.h.GetEnvVar, c.ParseBytes32, defaultValue) + return envOrSingular(key, c.h.GetEnvVar, c.ParseBytes32, defaultValue) } func (c *CheatCodesPrecompile) EnvOr_d145736c(key string, defaultValue string) (string, error) { - return envOrSingular[string](key, c.h.GetEnvVar, func(v string) (string, error) { + return envOrSingular(key, c.h.GetEnvVar, func(v string) (string, error) { return v, nil }, defaultValue) } func (c *CheatCodesPrecompile) EnvOr_b3e47705(key string, defaultValue []byte) ([]byte, error) { - return envOrSingular[[]byte](key, c.h.GetEnvVar, c.ParseBytes, defaultValue) + return envOrSingular(key, c.h.GetEnvVar, c.ParseBytes, defaultValue) } func (c *CheatCodesPrecompile) EnvOr_eb85e83b(key string, delimiter string, defaultValue []bool) ([]bool, error) { - return envOrList[bool](key, c.h.GetEnvVar, delimiter, c.ParseBool, defaultValue) + return envOrList(key, c.h.GetEnvVar, delimiter, c.ParseBool, defaultValue) } func (c *CheatCodesPrecompile) EnvOr_74318528(key string, delimiter string, defaultValue []*big.Int) ([]*big.Int, error) { - return envOrList[*big.Int](key, c.h.GetEnvVar, delimiter, c.ParseUint, defaultValue) + return envOrList(key, c.h.GetEnvVar, delimiter, c.ParseUint, defaultValue) } func (c *CheatCodesPrecompile) EnvOr_4700d74b(key string, delimiter string, defaultValue []*ABIInt256) ([]*ABIInt256, error) { - return envOrList[*ABIInt256](key, c.h.GetEnvVar, delimiter, c.ParseInt, defaultValue) + return envOrList(key, c.h.GetEnvVar, delimiter, c.ParseInt, defaultValue) } func (c *CheatCodesPrecompile) EnvOr_c74e9deb(key string, delimiter string, defaultValue []common.Address) ([]common.Address, error) { - return envOrList[common.Address](key, c.h.GetEnvVar, delimiter, c.ParseAddress, defaultValue) + return envOrList(key, c.h.GetEnvVar, delimiter, c.ParseAddress, defaultValue) } func (c *CheatCodesPrecompile) EnvOr_2281f367(key string, delimiter string, defaultValue [][32]byte) ([][32]byte, error) { - return envOrList[[32]byte](key, c.h.GetEnvVar, delimiter, c.ParseBytes32, defaultValue) + return envOrList(key, c.h.GetEnvVar, delimiter, c.ParseBytes32, defaultValue) } func (c *CheatCodesPrecompile) EnvOr_859216bc(key string, delimiter string, defaultValue []string) ([]string, error) { - return envOrList[string](key, c.h.GetEnvVar, delimiter, func(v string) (string, error) { + return envOrList(key, c.h.GetEnvVar, delimiter, func(v string) (string, error) { return v, nil }, defaultValue) } func (c *CheatCodesPrecompile) EnvOr_64bc3e64(key string, delimiter string, defaultValue [][]byte) ([][]byte, error) { - return envOrList[[]byte](key, c.h.GetEnvVar, delimiter, c.ParseBytes, defaultValue) + return envOrList(key, c.h.GetEnvVar, delimiter, c.ParseBytes, defaultValue) } func envSingular[E any](key string, @@ -221,69 +221,69 @@ func envList[E any](key string, // EnvBool implements https://book.getfoundry.sh/cheatcodes/env-bool func (c *CheatCodesPrecompile) EnvBool_7ed1ec7d(key string) (bool, error) { - return envSingular[bool](key, c.h.GetEnvVar, c.ParseBool) + return envSingular(key, c.h.GetEnvVar, c.ParseBool) } func (c *CheatCodesPrecompile) EnvBool_aaaddeaf(key string, delimiter string) ([]bool, error) { - return envList[bool](key, c.h.GetEnvVar, delimiter, c.ParseBool) + return envList(key, c.h.GetEnvVar, delimiter, c.ParseBool) } // EnvUint implements https://book.getfoundry.sh/cheatcodes/env-uint func (c *CheatCodesPrecompile) EnvUint_c1978d1f(key string) (*big.Int, error) { - return envSingular[*big.Int](key, c.h.GetEnvVar, c.ParseUint) + return envSingular(key, c.h.GetEnvVar, c.ParseUint) } func (c *CheatCodesPrecompile) EnvUint_f3dec099(key string, delimiter string) ([]*big.Int, error) { - return envList[*big.Int](key, c.h.GetEnvVar, delimiter, c.ParseUint) + return envList(key, c.h.GetEnvVar, delimiter, c.ParseUint) } // EnvInt implements https://book.getfoundry.sh/cheatcodes/env-int func (c *CheatCodesPrecompile) EnvInt_892a0c61(key string) (*ABIInt256, error) { - return envSingular[*ABIInt256](key, c.h.GetEnvVar, c.ParseInt) + return envSingular(key, c.h.GetEnvVar, c.ParseInt) } func (c *CheatCodesPrecompile) EnvInt_42181150(key string, delimiter string) ([]*ABIInt256, error) { - return envList[*ABIInt256](key, c.h.GetEnvVar, delimiter, c.ParseInt) + return envList(key, c.h.GetEnvVar, delimiter, c.ParseInt) } // EnvAddress implements https://book.getfoundry.sh/cheatcodes/env-address func (c *CheatCodesPrecompile) EnvAddress_350d56bf(key string) (common.Address, error) { - return envSingular[common.Address](key, c.h.GetEnvVar, c.ParseAddress) + return envSingular(key, c.h.GetEnvVar, c.ParseAddress) } func (c *CheatCodesPrecompile) EnvAddress_ad31b9fa(key string, delimiter string) ([]common.Address, error) { - return envList[common.Address](key, c.h.GetEnvVar, delimiter, c.ParseAddress) + return envList(key, c.h.GetEnvVar, delimiter, c.ParseAddress) } // EnvBytes32 implements https://book.getfoundry.sh/cheatcodes/env-bytes32 func (c *CheatCodesPrecompile) EnvBytes32_97949042(key string) ([32]byte, error) { - return envSingular[[32]byte](key, c.h.GetEnvVar, c.ParseBytes32) + return envSingular(key, c.h.GetEnvVar, c.ParseBytes32) } func (c *CheatCodesPrecompile) EnvBytes32_5af231c1(key string, delimiter string) ([][32]byte, error) { - return envList[[32]byte](key, c.h.GetEnvVar, delimiter, c.ParseBytes32) + return envList(key, c.h.GetEnvVar, delimiter, c.ParseBytes32) } // EnvString implements https://book.getfoundry.sh/cheatcodes/env-string func (c *CheatCodesPrecompile) EnvString_f877cb19(key string) (string, error) { - return envSingular[string](key, c.h.GetEnvVar, func(v string) (string, error) { + return envSingular(key, c.h.GetEnvVar, func(v string) (string, error) { return v, nil }) } func (c *CheatCodesPrecompile) EnvString_14b02bc9(key string, delimiter string) ([]string, error) { - return envList[string](key, c.h.GetEnvVar, delimiter, func(v string) (string, error) { + return envList(key, c.h.GetEnvVar, delimiter, func(v string) (string, error) { return v, nil }) } // EnvBytes implements https://book.getfoundry.sh/cheatcodes/env-bytes func (c *CheatCodesPrecompile) EnvBytes_4d7baf06(key string) ([]byte, error) { - return envSingular[[]byte](key, c.h.GetEnvVar, c.ParseBytes) + return envSingular(key, c.h.GetEnvVar, c.ParseBytes) } func (c *CheatCodesPrecompile) EnvBytes_ddc2651b(key string, delimiter string) ([][]byte, error) { - return envList[[]byte](key, c.h.GetEnvVar, delimiter, c.ParseBytes) + return envList(key, c.h.GetEnvVar, delimiter, c.ParseBytes) } // KeyExists implements https://book.getfoundry.sh/cheatcodes/key-exists diff --git a/op-chain-ops/solc/types.go b/op-chain-ops/solc/types.go index d109231eff..a7d24d208d 100644 --- a/op-chain-ops/solc/types.go +++ b/op-chain-ops/solc/types.go @@ -26,11 +26,12 @@ type CompilerInput struct { } type CompilerSettings struct { - Optimizer OptimizerSettings `json:"optimizer"` - Metadata CompilerInputMetadata `json:"metadata"` - OutputSelection map[string]map[string][]string `json:"outputSelection"` - EvmVersion string `json:"evmVersion,omitempty"` - Libraries map[string]map[string]string `json:"libraries,omitempty"` + Optimizer OptimizerSettings `json:"optimizer"` + Metadata CompilerInputMetadata `json:"metadata"` + CompilationTarget map[string]string `json:"compilationTarget"` + OutputSelection map[string]map[string][]string `json:"outputSelection"` + EvmVersion string `json:"evmVersion,omitempty"` + Libraries map[string]map[string]string `json:"libraries,omitempty"` } type OptimizerSettings struct { @@ -262,6 +263,8 @@ type Expression struct { ReferencedDeclaration int `json:"referencedDeclaration,omitempty"` ArgumentTypes []AstTypeDescriptions `json:"argumentTypes,omitempty"` Value interface{} `json:"value,omitempty"` + Kind string `json:"kind,omitempty"` + Expression *Expression `json:"expression,omitempty"` } type ForgeArtifact struct { diff --git a/op-challenger/cmd/list_claims.go b/op-challenger/cmd/list_claims.go index 6d07454aa3..5fee347ce3 100644 --- a/op-challenger/cmd/list_claims.go +++ b/op-challenger/cmd/list_claims.go @@ -82,7 +82,7 @@ func listClaims(ctx context.Context, game contracts.FaultDisputeGameContract, ve return fmt.Errorf("failed to retrieve split depth: %w", err) } status := metadata.Status - l2StartBlockNum, l2BlockNum, err := game.GetBlockRange(ctx) + l2StartBlockNum, l2BlockNum, err := game.GetGameRange(ctx) if err != nil { return fmt.Errorf("failed to retrieve status: %w", err) } diff --git a/op-challenger/cmd/list_games.go b/op-challenger/cmd/list_games.go index 0e0ce1880a..beb85bf1ec 100644 --- a/op-challenger/cmd/list_games.go +++ b/op-challenger/cmd/list_games.go @@ -116,7 +116,7 @@ func listGames(ctx context.Context, caller *batching.MultiCaller, factory *contr return } infos[currIndex].status = metadata.Status - infos[currIndex].l2BlockNum = metadata.L2BlockNum + infos[currIndex].l2BlockNum = metadata.L2SequenceNum infos[currIndex].rootClaim = metadata.RootClaim claimCount, err := gameContract.GetClaimCount(ctx) if err != nil { diff --git a/op-challenger/game/fault/contracts/faultdisputegame.go b/op-challenger/game/fault/contracts/faultdisputegame.go index f0385cd501..547c8c32ff 100644 --- a/op-challenger/game/fault/contracts/faultdisputegame.go +++ b/op-challenger/game/fault/contracts/faultdisputegame.go @@ -178,10 +178,10 @@ func (f *FaultDisputeGameContractLatest) GetBalanceAndDelay(ctx context.Context, return balance, delay, weth.Addr(), nil } -// GetBlockRange returns the block numbers of the absolute pre-state block (typically genesis or the bedrock activation block) +// GetGameRange returns the block numbers of the absolute pre-state block (typically genesis or the bedrock activation block) // and the post-state block (that the proposed output root is for). -func (f *FaultDisputeGameContractLatest) GetBlockRange(ctx context.Context) (prestateBlock uint64, poststateBlock uint64, retErr error) { - defer f.metrics.StartContractRequest("GetBlockRange")() +func (f *FaultDisputeGameContractLatest) GetGameRange(ctx context.Context) (prestateBlock uint64, poststateBlock uint64, retErr error) { + defer f.metrics.StartContractRequest("GetGameRange")() results, err := f.multiCaller.Call(ctx, rpcblock.Latest, f.contract.Call(methodStartingBlockNumber), f.contract.Call(methodL2BlockNumber)) @@ -200,7 +200,7 @@ func (f *FaultDisputeGameContractLatest) GetBlockRange(ctx context.Context) (pre type GameMetadata struct { L1Head common.Hash - L2BlockNum uint64 + L2SequenceNum uint64 RootClaim common.Hash Status gameTypes.GameStatus MaxClockDuration uint64 @@ -238,7 +238,7 @@ func (f *FaultDisputeGameContractLatest) GetGameMetadata(ctx context.Context, bl blockChallenger := results[6].GetAddress(0) return GameMetadata{ L1Head: l1Head, - L2BlockNum: l2BlockNumber, + L2SequenceNum: l2BlockNumber, RootClaim: rootClaim, Status: status, MaxClockDuration: duration, @@ -637,7 +637,7 @@ func (f *FaultDisputeGameContractLatest) decodeClaim(result *batching.CallResult type FaultDisputeGameContract interface { GetBalanceAndDelay(ctx context.Context, block rpcblock.Block) (*big.Int, time.Duration, common.Address, error) - GetBlockRange(ctx context.Context) (prestateBlock uint64, poststateBlock uint64, retErr error) + GetGameRange(ctx context.Context) (prestateBlock uint64, poststateBlock uint64, retErr error) GetGameMetadata(ctx context.Context, block rpcblock.Block) (GameMetadata, error) GetResolvedAt(ctx context.Context, block rpcblock.Block) (time.Time, error) GetStartingRootHash(ctx context.Context) (common.Hash, error) diff --git a/op-challenger/game/fault/contracts/faultdisputegame0180.go b/op-challenger/game/fault/contracts/faultdisputegame0180.go index e97a1211e3..6c0bc61cae 100644 --- a/op-challenger/game/fault/contracts/faultdisputegame0180.go +++ b/op-challenger/game/fault/contracts/faultdisputegame0180.go @@ -46,7 +46,7 @@ func (f *FaultDisputeGameContract0180) GetGameMetadata(ctx context.Context, bloc duration := results[4].GetUint64(0) return GameMetadata{ L1Head: l1Head, - L2BlockNum: l2BlockNumber, + L2SequenceNum: l2BlockNumber, RootClaim: rootClaim, Status: status, MaxClockDuration: duration, diff --git a/op-challenger/game/fault/contracts/faultdisputegame080.go b/op-challenger/game/fault/contracts/faultdisputegame080.go index 0ac1b3f5ee..492e160c16 100644 --- a/op-challenger/game/fault/contracts/faultdisputegame080.go +++ b/op-challenger/game/fault/contracts/faultdisputegame080.go @@ -53,7 +53,7 @@ func (f *FaultDisputeGameContract080) GetGameMetadata(ctx context.Context, block duration := results[4].GetUint64(0) return GameMetadata{ L1Head: l1Head, - L2BlockNum: l2BlockNumber, + L2SequenceNum: l2BlockNumber, RootClaim: rootClaim, Status: status, MaxClockDuration: duration / 2, diff --git a/op-challenger/game/fault/contracts/faultdisputegame_test.go b/op-challenger/game/fault/contracts/faultdisputegame_test.go index 683dd06551..151aec8632 100644 --- a/op-challenger/game/fault/contracts/faultdisputegame_test.go +++ b/op-challenger/game/fault/contracts/faultdisputegame_test.go @@ -48,7 +48,7 @@ func (c contractVersion) Is(versions ...string) bool { } func (c contractVersion) String() string { - return fmt.Sprintf("%s (%s)", c.version, c.gameType) + return fmt.Sprintf("%s_%s", c.version, c.gameType) } func (c contractVersion) IsSuperCannon() bool { @@ -109,7 +109,7 @@ var versions = []contractVersion{ { version: verSuperCannon, gameType: faultTypes.SuperCannonGameType, - loadAbi: snapshots.LoadFaultDisputeGameABI, + loadAbi: snapshots.LoadSuperFaultDisputeGameABI, }, } @@ -543,9 +543,14 @@ func TestGetBlockRange(t *testing.T) { stubRpc, contract := setupFaultDisputeGameTest(t, version) expectedStart := uint64(65) expectedEnd := uint64(102) - stubRpc.SetResponse(fdgAddr, methodStartingBlockNumber, rpcblock.Latest, nil, []interface{}{new(big.Int).SetUint64(expectedStart)}) - stubRpc.SetResponse(fdgAddr, methodL2BlockNumber, rpcblock.Latest, nil, []interface{}{new(big.Int).SetUint64(expectedEnd)}) - start, end, err := contract.GetBlockRange(context.Background()) + if version.IsSuperCannon() { + stubRpc.SetResponse(fdgAddr, methodStartingSequenceNumber, rpcblock.Latest, nil, []interface{}{new(big.Int).SetUint64(expectedStart)}) + stubRpc.SetResponse(fdgAddr, methodL2SequenceNumber, rpcblock.Latest, nil, []interface{}{new(big.Int).SetUint64(expectedEnd)}) + } else { + stubRpc.SetResponse(fdgAddr, methodStartingBlockNumber, rpcblock.Latest, nil, []interface{}{new(big.Int).SetUint64(expectedStart)}) + stubRpc.SetResponse(fdgAddr, methodL2BlockNumber, rpcblock.Latest, nil, []interface{}{new(big.Int).SetUint64(expectedEnd)}) + } + start, end, err := contract.GetGameRange(context.Background()) require.NoError(t, err) require.Equal(t, expectedStart, start) require.Equal(t, expectedEnd, end) @@ -581,7 +586,11 @@ func TestGetGameMetadata(t *testing.T) { expectedL2BlockNumberChallenger := common.Address{0xee} block := rpcblock.ByNumber(889) stubRpc.SetResponse(fdgAddr, methodL1Head, block, nil, []interface{}{expectedL1Head}) - stubRpc.SetResponse(fdgAddr, methodL2BlockNumber, block, nil, []interface{}{new(big.Int).SetUint64(expectedL2BlockNumber)}) + if version.IsSuperCannon() { + stubRpc.SetResponse(fdgAddr, methodL2SequenceNumber, block, nil, []interface{}{new(big.Int).SetUint64(expectedL2BlockNumber)}) + } else { + stubRpc.SetResponse(fdgAddr, methodL2BlockNumber, block, nil, []interface{}{new(big.Int).SetUint64(expectedL2BlockNumber)}) + } stubRpc.SetResponse(fdgAddr, methodRootClaim, block, nil, []interface{}{expectedRootClaim}) stubRpc.SetResponse(fdgAddr, methodStatus, block, nil, []interface{}{expectedStatus}) supportsL2BlockNumChallenge := (version.version != vers080 && version.version != vers0180) && !version.IsSuperCannon() @@ -589,13 +598,13 @@ func TestGetGameMetadata(t *testing.T) { stubRpc.SetResponse(fdgAddr, methodMaxClockDuration, block, nil, []interface{}{expectedMaxClockDuration}) stubRpc.SetResponse(fdgAddr, methodL2BlockNumberChallenged, block, nil, []interface{}{expectedL2BlockNumberChallenged}) stubRpc.SetResponse(fdgAddr, methodL2BlockNumberChallenger, block, nil, []interface{}{expectedL2BlockNumberChallenger}) - } else if expectedL2BlockNumberChallenged { + } else { t.Skip("Can't have challenged L2 block number on this contract version") } actual, err := contract.GetGameMetadata(context.Background(), block) expected := GameMetadata{ L1Head: expectedL1Head, - L2BlockNum: expectedL2BlockNumber, + L2SequenceNum: expectedL2BlockNumber, RootClaim: expectedRootClaim, Status: expectedStatus, MaxClockDuration: expectedMaxClockDuration, diff --git a/op-challenger/game/fault/contracts/superfaultdisputegame.go b/op-challenger/game/fault/contracts/superfaultdisputegame.go index 5add719a45..ed0561c7a7 100644 --- a/op-challenger/game/fault/contracts/superfaultdisputegame.go +++ b/op-challenger/game/fault/contracts/superfaultdisputegame.go @@ -14,12 +14,17 @@ import ( "github.com/ethereum/go-ethereum/common" ) +var ( + methodL2SequenceNumber = "l2SequenceNumber" + methodStartingSequenceNumber = "startingSequenceNumber" +) + type SuperFaultDisputeGameContractLatest struct { FaultDisputeGameContractLatest } func NewSuperFaultDisputeGameContract(ctx context.Context, metrics metrics.ContractMetricer, addr common.Address, caller *batching.MultiCaller) (FaultDisputeGameContract, error) { - contractAbi := snapshots.LoadFaultDisputeGameABI() + contractAbi := snapshots.LoadSuperFaultDisputeGameABI() return &SuperFaultDisputeGameContractLatest{ FaultDisputeGameContractLatest: FaultDisputeGameContractLatest{ metrics: metrics, @@ -34,7 +39,7 @@ func (f *SuperFaultDisputeGameContractLatest) GetGameMetadata(ctx context.Contex defer f.metrics.StartContractRequest("GetGameMetadata")() results, err := f.multiCaller.Call(ctx, block, f.contract.Call(methodL1Head), - f.contract.Call(methodL2BlockNumber), + f.contract.Call(methodL2SequenceNumber), f.contract.Call(methodRootClaim), f.contract.Call(methodStatus), f.contract.Call(methodMaxClockDuration), @@ -46,7 +51,7 @@ func (f *SuperFaultDisputeGameContractLatest) GetGameMetadata(ctx context.Contex return GameMetadata{}, fmt.Errorf("expected 5 results but got %v", len(results)) } l1Head := results[0].GetHash(0) - l2BlockNumber := results[1].GetBigInt(0).Uint64() + l2Timestamp := results[1].GetBigInt(0).Uint64() rootClaim := results[2].GetHash(0) status, err := gameTypes.GameStatusFromUint8(results[3].GetUint8(0)) if err != nil { @@ -55,7 +60,7 @@ func (f *SuperFaultDisputeGameContractLatest) GetGameMetadata(ctx context.Contex duration := results[4].GetUint64(0) return GameMetadata{ L1Head: l1Head, - L2BlockNum: l2BlockNumber, + L2SequenceNum: l2Timestamp, RootClaim: rootClaim, Status: status, MaxClockDuration: duration, @@ -69,3 +74,22 @@ func (f *SuperFaultDisputeGameContractLatest) IsL2BlockNumberChallenged(ctx cont func (f *SuperFaultDisputeGameContractLatest) ChallengeL2BlockNumberTx(challenge *types.InvalidL2BlockNumberChallenge) (txmgr.TxCandidate, error) { return txmgr.TxCandidate{}, ErrChallengeL2BlockNotSupported } + +// GetGameRange returns the timestamps of the absolute pre-state and the proposed super root +func (f *SuperFaultDisputeGameContractLatest) GetGameRange(ctx context.Context) (prestateBlock uint64, poststateBlock uint64, retErr error) { + defer f.metrics.StartContractRequest("GetGameRange")() + results, err := f.multiCaller.Call(ctx, rpcblock.Latest, + f.contract.Call(methodStartingSequenceNumber), + f.contract.Call(methodL2SequenceNumber)) + if err != nil { + retErr = fmt.Errorf("failed to retrieve game range: %w", err) + return + } + if len(results) != 2 { + retErr = fmt.Errorf("expected 2 results but got %v", len(results)) + return + } + prestateBlock = results[0].GetBigInt(0).Uint64() + poststateBlock = results[1].GetBigInt(0).Uint64() + return +} diff --git a/op-challenger/game/fault/register_task.go b/op-challenger/game/fault/register_task.go index 0bf710b25d..1bc233b8aa 100644 --- a/op-challenger/game/fault/register_task.go +++ b/op-challenger/game/fault/register_task.go @@ -273,7 +273,7 @@ func (e *RegisterTask) Register( return nil, fmt.Errorf("failed to load oracle for game %v: %w", game.Proxy, err) } oracles.RegisterOracle(oracle) - prestateBlock, poststateBlock, err := contract.GetBlockRange(ctx) + prestateBlock, poststateBlock, err := contract.GetGameRange(ctx) if err != nil { return nil, err } diff --git a/op-challenger/game/fault/register_task_test.go b/op-challenger/game/fault/register_task_test.go index 2e58fadf74..9bbefed1d2 100644 --- a/op-challenger/game/fault/register_task_test.go +++ b/op-challenger/game/fault/register_task_test.go @@ -47,7 +47,13 @@ func TestRegisterOracle_AddsOracle(t *testing.T) { vmAddr := common.Address{0xcc} oracleAddr := common.Address{0xdd} rpc := test.NewAbiBasedRpc(t, gameFactoryAddr, snapshots.LoadDisputeGameFactoryABI()) - rpc.AddContract(gameImplAddr, snapshots.LoadFaultDisputeGameABI()) + if gameType == faultTypes.CannonGameType { + rpc.AddContract(gameImplAddr, snapshots.LoadFaultDisputeGameABI()) + } else if gameType == faultTypes.SuperCannonGameType { + rpc.AddContract(gameImplAddr, snapshots.LoadSuperFaultDisputeGameABI()) + } else { + t.Fatalf("game type %v not supported", gameType) + } rpc.AddContract(vmAddr, snapshots.LoadMIPSABI()) rpc.AddContract(oracleAddr, snapshots.LoadPreimageOracleABI()) m := metrics.NoopMetrics diff --git a/op-challenger/game/fault/trace/super/super_cannon.go b/op-challenger/game/fault/trace/super/super_cannon.go index 50545637f7..679844d2eb 100644 --- a/op-challenger/game/fault/trace/super/super_cannon.go +++ b/op-challenger/game/fault/trace/super/super_cannon.go @@ -30,23 +30,23 @@ func NewSuperCannonTraceAccessor( dir string, l1Head eth.BlockID, splitDepth types.Depth, - prestateBlock uint64, - poststateBlock uint64, + prestateTimestamp uint64, + poststateTimestamp uint64, ) (*trace.Accessor, error) { rollupCfgs, err := NewRollupConfigs(cfg) if err != nil { return nil, fmt.Errorf("failed to load rollup configs: %w", err) } - outputProvider := NewSuperTraceProvider(logger, rollupCfgs, prestateProvider, rootProvider, l1Head, splitDepth, prestateBlock, poststateBlock) + outputProvider := NewSuperTraceProvider(logger, rollupCfgs, prestateProvider, rootProvider, l1Head, splitDepth, prestateTimestamp, poststateTimestamp) cannonCreator := func(ctx context.Context, localContext common.Hash, depth types.Depth, claimInfo ClaimInfo) (types.TraceProvider, error) { logger := logger.New("agreedPrestate", claimInfo.AgreedPrestate, "claim", claimInfo.Claim, "localContext", localContext) subdir := filepath.Join(dir, localContext.Hex()) localInputs := utils.LocalGameInputs{ - L1Head: l1Head.Hash, - L2OutputRoot: crypto.Keccak256Hash(claimInfo.AgreedPrestate), - AgreedPreState: claimInfo.AgreedPrestate, - L2Claim: claimInfo.Claim, - L2BlockNumber: new(big.Int).SetUint64(poststateBlock), + L1Head: l1Head.Hash, + L2OutputRoot: crypto.Keccak256Hash(claimInfo.AgreedPrestate), + AgreedPreState: claimInfo.AgreedPrestate, + L2Claim: claimInfo.Claim, + L2SequenceNumber: new(big.Int).SetUint64(poststateTimestamp), } provider := cannon.NewTraceProvider(logger, m.ToTypedVmMetrics(cfg.VmType.String()), cfg, serverExecutor, prestateProvider, cannonPrestate, localInputs, subdir, depth) return provider, nil diff --git a/op-challenger/game/fault/trace/utils/local.go b/op-challenger/game/fault/trace/utils/local.go index 3604bfbfa2..85f64e4021 100644 --- a/op-challenger/game/fault/trace/utils/local.go +++ b/op-challenger/game/fault/trace/utils/local.go @@ -10,12 +10,12 @@ import ( ) type LocalGameInputs struct { - L1Head common.Hash - L2Head common.Hash - L2OutputRoot common.Hash - AgreedPreState []byte - L2Claim common.Hash - L2BlockNumber *big.Int + L1Head common.Hash + L2Head common.Hash + L2OutputRoot common.Hash + AgreedPreState []byte + L2Claim common.Hash + L2SequenceNumber *big.Int } type L2HeaderSource interface { @@ -57,10 +57,10 @@ func FetchLocalInputsFromProposals(ctx context.Context, l1Head common.Hash, l2Cl l2Head := agreedHeader.Hash() return LocalGameInputs{ - L1Head: l1Head, - L2Head: l2Head, - L2OutputRoot: agreedOutput.OutputRoot, - L2Claim: claimedOutput.OutputRoot, - L2BlockNumber: claimedOutput.L2BlockNumber, + L1Head: l1Head, + L2Head: l2Head, + L2OutputRoot: agreedOutput.OutputRoot, + L2Claim: claimedOutput.OutputRoot, + L2SequenceNumber: claimedOutput.L2BlockNumber, }, nil } diff --git a/op-challenger/game/fault/trace/utils/local_test.go b/op-challenger/game/fault/trace/utils/local_test.go index ee80528b40..cb6224429a 100644 --- a/op-challenger/game/fault/trace/utils/local_test.go +++ b/op-challenger/game/fault/trace/utils/local_test.go @@ -38,7 +38,7 @@ func TestFetchLocalInputs(t *testing.T) { require.Equal(t, l2Client.header.Hash(), inputs.L2Head) require.EqualValues(t, contract.starting.OutputRoot, inputs.L2OutputRoot) require.EqualValues(t, contract.disputed.OutputRoot, inputs.L2Claim) - require.Equal(t, contract.disputed.L2BlockNumber, inputs.L2BlockNumber) + require.Equal(t, contract.disputed.L2BlockNumber, inputs.L2SequenceNumber) } func TestFetchLocalInputsFromProposals(t *testing.T) { @@ -66,7 +66,7 @@ func TestFetchLocalInputsFromProposals(t *testing.T) { require.Equal(t, l2Client.header.Hash(), inputs.L2Head) require.EqualValues(t, agreed.OutputRoot, inputs.L2OutputRoot) require.EqualValues(t, claimed.OutputRoot, inputs.L2Claim) - require.Equal(t, claimed.L2BlockNumber, inputs.L2BlockNumber) + require.Equal(t, claimed.L2BlockNumber, inputs.L2SequenceNumber) } type mockGameInputsSource struct { diff --git a/op-challenger/game/fault/trace/vm/executor_test.go b/op-challenger/game/fault/trace/vm/executor_test.go index 6deccea981..ba6c4f8a26 100644 --- a/op-challenger/game/fault/trace/vm/executor_test.go +++ b/op-challenger/game/fault/trace/vm/executor_test.go @@ -39,11 +39,11 @@ func TestGenerateProof(t *testing.T) { } inputs := utils.LocalGameInputs{ - L1Head: common.Hash{0x11}, - L2Head: common.Hash{0x22}, - L2OutputRoot: common.Hash{0x33}, - L2Claim: common.Hash{0x44}, - L2BlockNumber: big.NewInt(3333), + L1Head: common.Hash{0x11}, + L2Head: common.Hash{0x22}, + L2OutputRoot: common.Hash{0x33}, + L2Claim: common.Hash{0x44}, + L2SequenceNumber: big.NewInt(3333), } info := &mipsevm.DebugInfo{ diff --git a/op-challenger/game/fault/trace/vm/kona_server_executor.go b/op-challenger/game/fault/trace/vm/kona_server_executor.go index 7bb787004a..8f1e9b6087 100644 --- a/op-challenger/game/fault/trace/vm/kona_server_executor.go +++ b/op-challenger/game/fault/trace/vm/kona_server_executor.go @@ -36,7 +36,7 @@ func (s *KonaExecutor) OracleCommand(cfg Config, dataDir string, inputs utils.Lo "--l2-head", inputs.L2Head.Hex(), "--l2-output-root", inputs.L2OutputRoot.Hex(), "--l2-claim", inputs.L2Claim.Hex(), - "--l2-block-number", inputs.L2BlockNumber.Text(10), + "--l2-block-number", inputs.L2SequenceNumber.Text(10), } if s.nativeMode { diff --git a/op-challenger/game/fault/trace/vm/kona_server_executor_test.go b/op-challenger/game/fault/trace/vm/kona_server_executor_test.go index 74262bfb77..05413d8f4e 100644 --- a/op-challenger/game/fault/trace/vm/kona_server_executor_test.go +++ b/op-challenger/game/fault/trace/vm/kona_server_executor_test.go @@ -20,11 +20,11 @@ func TestKonaFillHostCommand(t *testing.T) { Networks: []string{"op-mainnet"}, } inputs := utils.LocalGameInputs{ - L1Head: common.Hash{0x11}, - L2Head: common.Hash{0x22}, - L2OutputRoot: common.Hash{0x33}, - L2Claim: common.Hash{0x44}, - L2BlockNumber: big.NewInt(3333), + L1Head: common.Hash{0x11}, + L2Head: common.Hash{0x22}, + L2OutputRoot: common.Hash{0x33}, + L2Claim: common.Hash{0x44}, + L2SequenceNumber: big.NewInt(3333), } vmConfig := NewKonaExecutor() diff --git a/op-challenger/game/fault/trace/vm/kona_super_server_executor.go b/op-challenger/game/fault/trace/vm/kona_super_server_executor.go index 0838955482..03a2747501 100644 --- a/op-challenger/game/fault/trace/vm/kona_super_server_executor.go +++ b/op-challenger/game/fault/trace/vm/kona_super_server_executor.go @@ -36,7 +36,7 @@ func (s *KonaSuperExecutor) OracleCommand(cfg Config, dataDir string, inputs uti "--l1-head", inputs.L1Head.Hex(), "--agreed-l2-pre-state", common.Bytes2Hex(inputs.AgreedPreState), "--claimed-l2-post-state", inputs.L2Claim.Hex(), - "--claimed-l2-timestamp", inputs.L2BlockNumber.Text(10), + "--claimed-l2-timestamp", inputs.L2SequenceNumber.Text(10), } if s.nativeMode { diff --git a/op-challenger/game/fault/trace/vm/op_program_server_executor.go b/op-challenger/game/fault/trace/vm/op_program_server_executor.go index c79a1bfcaf..f50196c1cb 100644 --- a/op-challenger/game/fault/trace/vm/op_program_server_executor.go +++ b/op-challenger/game/fault/trace/vm/op_program_server_executor.go @@ -28,7 +28,7 @@ func (s *OpProgramServerExecutor) OracleCommand(cfg Config, dataDir string, inpu "--datadir", dataDir, "--l1.head", inputs.L1Head.Hex(), "--l2.claim", inputs.L2Claim.Hex(), - "--l2.blocknumber", inputs.L2BlockNumber.Text(10), + "--l2.blocknumber", inputs.L2SequenceNumber.Text(10), } if inputs.L2Head != (common.Hash{}) { args = append(args, "--l2.head", inputs.L2Head.Hex()) diff --git a/op-challenger/game/fault/trace/vm/op_program_server_executor_test.go b/op-challenger/game/fault/trace/vm/op_program_server_executor_test.go index c990f03a8b..1120e02df8 100644 --- a/op-challenger/game/fault/trace/vm/op_program_server_executor_test.go +++ b/op-challenger/game/fault/trace/vm/op_program_server_executor_test.go @@ -39,11 +39,11 @@ func TestOpProgramFillHostCommand(t *testing.T) { Server: "./bin/mockserver", } inputs := utils.LocalGameInputs{ - L1Head: common.Hash{0x11}, - L2Head: common.Hash{0x22}, - L2OutputRoot: common.Hash{0x33}, - L2Claim: common.Hash{0x44}, - L2BlockNumber: big.NewInt(3333), + L1Head: common.Hash{0x11}, + L2Head: common.Hash{0x22}, + L2OutputRoot: common.Hash{0x33}, + L2Claim: common.Hash{0x44}, + L2SequenceNumber: big.NewInt(3333), } configModifier(&cfg, &inputs) executor := NewOpProgramServerExecutor(testlog.Logger(t, lvl)) @@ -59,7 +59,7 @@ func TestOpProgramFillHostCommand(t *testing.T) { require.Equal(t, dir, pairs["--datadir"]) require.Equal(t, inputs.L1Head.Hex(), pairs["--l1.head"]) require.Equal(t, inputs.L2Claim.Hex(), pairs["--l2.claim"]) - require.Equal(t, inputs.L2BlockNumber.String(), pairs["--l2.blocknumber"]) + require.Equal(t, inputs.L2SequenceNumber.String(), pairs["--l2.blocknumber"]) return pairs } diff --git a/op-challenger/runner/factory.go b/op-challenger/runner/factory.go index f53927a1bb..aeae94b434 100644 --- a/op-challenger/runner/factory.go +++ b/op-challenger/runner/factory.go @@ -29,7 +29,7 @@ func createTraceProvider( dir string, ) (types.TraceProvider, error) { switch traceType { - case types.TraceTypeCannon: + case types.TraceTypeCannon, types.TraceTypeSuperCannon: serverExecutor := vm.NewOpProgramServerExecutor(logger) stateConverter := cannon.NewStateConverter(cfg.Cannon) prestate, err := prestateSource.getPrestate(ctx, logger, cfg.CannonAbsolutePreStateBaseURL, cfg.CannonAbsolutePreState, dir, stateConverter) diff --git a/op-challenger/runner/game_inputs.go b/op-challenger/runner/game_inputs.go new file mode 100644 index 0000000000..46637daed5 --- /dev/null +++ b/op-challenger/runner/game_inputs.go @@ -0,0 +1,190 @@ +package runner + +import ( + "context" + "errors" + "fmt" + "math/big" + "math/rand" + + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/super" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/utils" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" + "github.com/ethereum-optimism/optimism/op-service/sources" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" +) + +func createGameInputs(ctx context.Context, log log.Logger, rollupClient *sources.RollupClient, supervisorClient *sources.SupervisorClient, typeName string, traceType types.TraceType) (utils.LocalGameInputs, error) { + switch traceType { + case types.TraceTypeSuperCannon, types.TraceTypeSuperPermissioned: + if supervisorClient == nil { + return utils.LocalGameInputs{}, fmt.Errorf("trace type %s requires supervisor rpc to be set", traceType) + } + return createGameInputsInterop(ctx, log, supervisorClient, typeName) + default: + if rollupClient == nil { + return utils.LocalGameInputs{}, fmt.Errorf("trace type %s requires rollup rpc to be set", traceType) + } + return createGameInputsSingle(ctx, log, rollupClient, typeName) + } +} + +func createGameInputsSingle(ctx context.Context, log log.Logger, client *sources.RollupClient, typeName string) (utils.LocalGameInputs, error) { + status, err := client.SyncStatus(ctx) + if err != nil { + return utils.LocalGameInputs{}, fmt.Errorf("failed to get rollup sync status: %w", err) + } + log.Info("Got sync status", "status", status, "type", typeName) + + if status.FinalizedL2.Number == 0 { + return utils.LocalGameInputs{}, errors.New("safe head is 0") + } + l1Head := status.FinalizedL1 + if status.FinalizedL1.Number > status.CurrentL1.Number { + // Restrict the L1 head to a block that has actually been processed by op-node. + // This only matters if op-node is behind and hasn't processed all finalized L1 blocks yet. + l1Head = status.CurrentL1 + log.Info("Node has not completed syncing finalized L1 block, using CurrentL1 instead", "type", typeName) + } else if status.FinalizedL1.Number == 0 { + // The node is resetting its pipeline and has set FinalizedL1 to 0, use the current L1 instead as it is the best + // hope of getting a non-zero L1 block + l1Head = status.CurrentL1 + log.Warn("Node has zero finalized L1 block, using CurrentL1 instead", "type", typeName) + } + log.Info("Using L1 head", "head", l1Head, "type", typeName) + if l1Head.Number == 0 { + return utils.LocalGameInputs{}, errors.New("l1 head is 0") + } + blockNumber, err := findL2BlockNumberToDispute(ctx, log, client, l1Head.Number, status.FinalizedL2.Number) + if err != nil { + return utils.LocalGameInputs{}, fmt.Errorf("failed to find l2 block number to dispute: %w", err) + } + claimOutput, err := client.OutputAtBlock(ctx, blockNumber) + if err != nil { + return utils.LocalGameInputs{}, fmt.Errorf("failed to get claim output: %w", err) + } + parentOutput, err := client.OutputAtBlock(ctx, blockNumber-1) + if err != nil { + return utils.LocalGameInputs{}, fmt.Errorf("failed to get claim output: %w", err) + } + localInputs := utils.LocalGameInputs{ + L1Head: l1Head.Hash, + L2Head: parentOutput.BlockRef.Hash, + L2OutputRoot: common.Hash(parentOutput.OutputRoot), + L2Claim: common.Hash(claimOutput.OutputRoot), + L2SequenceNumber: new(big.Int).SetUint64(blockNumber), + } + return localInputs, nil +} + +func createGameInputsInterop(ctx context.Context, log log.Logger, client *sources.SupervisorClient, typeName string) (utils.LocalGameInputs, error) { + status, err := client.SyncStatus(ctx) + if err != nil { + return utils.LocalGameInputs{}, fmt.Errorf("failed to get rollup sync status: %w", err) + } + log.Info("Got sync status", "status", status, "type", typeName) + + claimTimestamp := status.FinalizedTimestamp + agreedTimestamp := claimTimestamp - 1 + if claimTimestamp == 0 { + return utils.LocalGameInputs{}, errors.New("finalized timestamp is 0") + } + l1Head := status.MinSyncedL1 + log.Info("Using L1 head", "head", l1Head, "type", typeName) + if l1Head.Number == 0 { + return utils.LocalGameInputs{}, errors.New("l1 head is 0") + } + + prestateProvider := super.NewSuperRootPrestateProvider(client, agreedTimestamp) + gameDepth := types.Depth(30) + provider := super.NewSuperTraceProvider(log, nil, prestateProvider, client, l1Head.ID(), gameDepth, agreedTimestamp, claimTimestamp+10) + var agreedPrestate []byte + var claim common.Hash + switch 2 { //rand.Intn(3) { + case 0: // Derive block on first chain + log.Info("Running first chain") + prestate, err := prestateProvider.AbsolutePreState(ctx) + if err != nil { + return utils.LocalGameInputs{}, fmt.Errorf("failed to get pre-state commitment: %w", err) + } + agreedPrestate = prestate.Marshal() + claim, err = provider.Get(ctx, types.NewPosition(gameDepth, big.NewInt(0))) + if err != nil { + return utils.LocalGameInputs{}, fmt.Errorf("failed to get claim: %w", err) + } + case 1: // Derive block on second chain + log.Info("Deriving second chain") + agreedPrestate, err = provider.GetPreimageBytes(ctx, types.NewPosition(gameDepth, big.NewInt(0))) + if err != nil { + return utils.LocalGameInputs{}, fmt.Errorf("failed to get agreed prestate at position 0: %w", err) + } + claim, err = provider.Get(ctx, types.NewPosition(gameDepth, big.NewInt(1))) + if err != nil { + return utils.LocalGameInputs{}, fmt.Errorf("failed to get claim: %w", err) + } + case 2: // Consolidate + log.Info("Running consolidate step") + step := int64(super.StepsPerTimestamp - 1) + agreedPrestate, err = provider.GetPreimageBytes(ctx, types.NewPosition(gameDepth, big.NewInt(step-1))) + if err != nil { + return utils.LocalGameInputs{}, fmt.Errorf("failed to get agreed prestate at position 0: %w", err) + } + claim, err = provider.Get(ctx, types.NewPosition(gameDepth, big.NewInt(step))) + if err != nil { + return utils.LocalGameInputs{}, fmt.Errorf("failed to get claim: %w", err) + } + } + localInputs := utils.LocalGameInputs{ + L1Head: l1Head.Hash, + AgreedPreState: agreedPrestate, + L2OutputRoot: crypto.Keccak256Hash(agreedPrestate), + L2Claim: claim, + L2SequenceNumber: new(big.Int).SetUint64(claimTimestamp + 10), // Anything beyond the claim + } + return localInputs, nil +} + +func findL2BlockNumberToDispute(ctx context.Context, log log.Logger, client *sources.RollupClient, l1HeadNum uint64, l2BlockNum uint64) (uint64, error) { + // Try to find a L1 block prior to the batch that make l2BlockNum safe + // Limits how far back we search to 10 * 32 blocks + const skipSize = uint64(32) + for i := 0; i < 10; i++ { + if l1HeadNum < skipSize { + // Too close to genesis, give up and just use the original block + log.Info("Failed to find prior batch.") + return l2BlockNum, nil + } + l1HeadNum -= skipSize + prevSafeHead, err := client.SafeHeadAtL1Block(ctx, l1HeadNum) + if err != nil { + return 0, fmt.Errorf("failed to get prior safe head at L1 block %v: %w", l1HeadNum, err) + } + if prevSafeHead.SafeHead.Number < l2BlockNum { + switch rand.Intn(3) { + case 0: // First block of span batch + return prevSafeHead.SafeHead.Number + 1, nil + case 1: // Last block of span batch + return prevSafeHead.SafeHead.Number, nil + case 2: // Random block, probably but not guaranteed to be in the middle of a span batch + firstBlockInSpanBatch := prevSafeHead.SafeHead.Number + 1 + if l2BlockNum <= firstBlockInSpanBatch { + // There is only one block in the next batch so we just have to use it + return l2BlockNum, nil + } + offset := rand.Intn(int(l2BlockNum - firstBlockInSpanBatch)) + return firstBlockInSpanBatch + uint64(offset), nil + } + + } + if prevSafeHead.SafeHead.Number < l2BlockNum { + // We walked back far enough to be before the batch that included l2BlockNum + // So use the first block after the prior safe head as the disputed block. + // It must be the first block in a batch. + return prevSafeHead.SafeHead.Number + 1, nil + } + } + log.Warn("Failed to find prior batch", "l2BlockNum", l2BlockNum, "earliestCheckL1Block", l1HeadNum) + return l2BlockNum, nil +} diff --git a/op-challenger/runner/prestates.go b/op-challenger/runner/prestates.go index 19e3d40933..54bc8fa474 100644 --- a/op-challenger/runner/prestates.go +++ b/op-challenger/runner/prestates.go @@ -67,6 +67,14 @@ func (f *HashPrestateFetcher) getPrestate(ctx context.Context, _ log.Logger, pre return prestate, nil } +type LocalPrestateFetcher struct { + path string +} + +func (f *LocalPrestateFetcher) getPrestate(ctx context.Context, logger log.Logger, prestateBaseUrl *url.URL, _ string, dataDir string, stateConverter vm.StateConverter) (string, error) { + return f.path, nil +} + // NamedPrestateFetcher downloads a file with a specified name from the prestate base URL and uses it as the prestate. // The file is re-downloaded on each run rather than being cached. This makes it possible to run the latest builds // from develop. diff --git a/op-challenger/runner/runner.go b/op-challenger/runner/runner.go index 8ec7e95fb3..b717d286d8 100644 --- a/op-challenger/runner/runner.go +++ b/op-challenger/runner/runner.go @@ -4,15 +4,15 @@ import ( "context" "errors" "fmt" - "math/big" - "math/rand" "os" "path/filepath" "regexp" + "strings" "sync" "sync/atomic" "time" + "github.com/ethereum-optimism/optimism/op-service/client" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" @@ -85,9 +85,23 @@ func (r *Runner) Start(ctx context.Context) error { return fmt.Errorf("failed to start metrics: %w", err) } - rollupClient, err := dial.DialRollupClientWithTimeout(ctx, 1*time.Minute, r.log, r.cfg.RollupRpc) - if err != nil { - return fmt.Errorf("failed to dial rollup client: %w", err) + var rollupClient *sources.RollupClient + if r.cfg.RollupRpc != "" { + r.log.Info("Dialling rollup client", "url", r.cfg.RollupRpc) + cl, err := dial.DialRollupClientWithTimeout(ctx, 1*time.Minute, r.log, r.cfg.RollupRpc) + if err != nil { + return fmt.Errorf("failed to dial rollup client: %w", err) + } + rollupClient = cl + } + var supervisorClient *sources.SupervisorClient + if r.cfg.SupervisorRPC != "" { + r.log.Info("Dialling supervisor client", "url", r.cfg.SupervisorRPC) + rpcCl, err := dial.DialRPCClientWithTimeout(ctx, 1*time.Minute, r.log, r.cfg.SupervisorRPC) + if err != nil { + return fmt.Errorf("failed to dial rollup client: %w", err) + } + supervisorClient = sources.NewSupervisorClient(client.NewBaseRPCClient(rpcCl)) } l1Client, err := dial.DialRPCClientWithTimeout(ctx, 1*time.Minute, r.log, r.cfg.L1EthRpc) @@ -98,19 +112,19 @@ func (r *Runner) Start(ctx context.Context) error { for _, runConfig := range r.runConfigs { r.wg.Add(1) - go r.loop(ctx, runConfig, rollupClient, caller) + go r.loop(ctx, runConfig, rollupClient, supervisorClient, caller) } r.log.Info("Runners started", "num", len(r.runConfigs)) return nil } -func (r *Runner) loop(ctx context.Context, runConfig RunConfig, client *sources.RollupClient, caller *batching.MultiCaller) { +func (r *Runner) loop(ctx context.Context, runConfig RunConfig, rollupClient *sources.RollupClient, supervisorClient *sources.SupervisorClient, caller *batching.MultiCaller) { defer r.wg.Done() t := time.NewTicker(1 * time.Minute) defer t.Stop() for { - r.runAndRecordOnce(ctx, runConfig, client, caller) + r.runAndRecordOnce(ctx, runConfig, rollupClient, supervisorClient, caller) select { case <-t.C: case <-ctx.Done(): @@ -119,7 +133,7 @@ func (r *Runner) loop(ctx context.Context, runConfig RunConfig, client *sources. } } -func (r *Runner) runAndRecordOnce(ctx context.Context, runConfig RunConfig, client *sources.RollupClient, caller *batching.MultiCaller) { +func (r *Runner) runAndRecordOnce(ctx context.Context, runConfig RunConfig, rollupClient *sources.RollupClient, supervisorClient *sources.SupervisorClient, caller *batching.MultiCaller) { recordError := func(err error, traceType string, m Metricer, log log.Logger) { if errors.Is(err, ErrUnexpectedStatusCode) { log.Error("Incorrect status code", "type", runConfig.Name, "err", err) @@ -137,7 +151,11 @@ func (r *Runner) runAndRecordOnce(ctx context.Context, runConfig RunConfig, clie } var prestateSource prestateFetcher - if runConfig.PrestateFilename != "" { + if strings.HasPrefix(runConfig.PrestateFilename, "file:") { + path := runConfig.PrestateFilename[len("file:"):] + r.log.Info("Using local file prestate", "type", runConfig.TraceType, "path", path) + prestateSource = &LocalPrestateFetcher{path: path} + } else if runConfig.PrestateFilename != "" { r.log.Info("Using named prestate", "type", runConfig.TraceType, "filename", runConfig.PrestateFilename) prestateSource = &NamedPrestateFetcher{filename: runConfig.PrestateFilename} } else if runConfig.Prestate == (common.Hash{}) { @@ -153,13 +171,13 @@ func (r *Runner) runAndRecordOnce(ctx context.Context, runConfig RunConfig, clie prestateSource = &HashPrestateFetcher{prestateHash: runConfig.Prestate} } - localInputs, err := r.createGameInputs(ctx, client, runConfig.Name) + localInputs, err := createGameInputs(ctx, r.log, rollupClient, supervisorClient, runConfig.Name, runConfig.TraceType) if err != nil { recordError(err, runConfig.Name, r.m, r.log) return } - inputsLogger := r.log.New("l1", localInputs.L1Head, "l2", localInputs.L2Head, "l2Block", localInputs.L2BlockNumber, "claim", localInputs.L2Claim) + inputsLogger := r.log.New("l1", localInputs.L1Head, "l2", localInputs.L2Head, "l2Block", localInputs.L2SequenceNumber, "claim", localInputs.L2Claim) // Sanitize the directory name. safeName := regexp.MustCompile("[^a-zA-Z0-9_-]").ReplaceAllString(runConfig.Name, "") dir, err := r.prepDatadir(safeName) @@ -197,97 +215,6 @@ func (r *Runner) prepDatadir(name string) (string, error) { return dir, nil } -func (r *Runner) createGameInputs(ctx context.Context, client *sources.RollupClient, traceType string) (utils.LocalGameInputs, error) { - status, err := client.SyncStatus(ctx) - if err != nil { - return utils.LocalGameInputs{}, fmt.Errorf("failed to get rollup sync status: %w", err) - } - r.log.Info("Got sync status", "status", status, "type", traceType) - - if status.FinalizedL2.Number == 0 { - return utils.LocalGameInputs{}, errors.New("safe head is 0") - } - l1Head := status.FinalizedL1 - if status.FinalizedL1.Number > status.CurrentL1.Number { - // Restrict the L1 head to a block that has actually been processed by op-node. - // This only matters if op-node is behind and hasn't processed all finalized L1 blocks yet. - l1Head = status.CurrentL1 - r.log.Info("Node has not completed syncing finalized L1 block, using CurrentL1 instead", "type", traceType) - } else if status.FinalizedL1.Number == 0 { - // The node is resetting its pipeline and has set FinalizedL1 to 0, use the current L1 instead as it is the best - // hope of getting a non-zero L1 block - l1Head = status.CurrentL1 - r.log.Warn("Node has zero finalized L1 block, using CurrentL1 instead", "type", traceType) - } - r.log.Info("Using L1 head", "head", l1Head, "type", traceType) - if l1Head.Number == 0 { - return utils.LocalGameInputs{}, errors.New("l1 head is 0") - } - blockNumber, err := r.findL2BlockNumberToDispute(ctx, client, l1Head.Number, status.FinalizedL2.Number) - if err != nil { - return utils.LocalGameInputs{}, fmt.Errorf("failed to find l2 block number to dispute: %w", err) - } - claimOutput, err := client.OutputAtBlock(ctx, blockNumber) - if err != nil { - return utils.LocalGameInputs{}, fmt.Errorf("failed to get claim output: %w", err) - } - parentOutput, err := client.OutputAtBlock(ctx, blockNumber-1) - if err != nil { - return utils.LocalGameInputs{}, fmt.Errorf("failed to get claim output: %w", err) - } - localInputs := utils.LocalGameInputs{ - L1Head: l1Head.Hash, - L2Head: parentOutput.BlockRef.Hash, - L2OutputRoot: common.Hash(parentOutput.OutputRoot), - L2Claim: common.Hash(claimOutput.OutputRoot), - L2BlockNumber: new(big.Int).SetUint64(blockNumber), - } - return localInputs, nil -} - -func (r *Runner) findL2BlockNumberToDispute(ctx context.Context, client *sources.RollupClient, l1HeadNum uint64, l2BlockNum uint64) (uint64, error) { - // Try to find a L1 block prior to the batch that make l2BlockNum safe - // Limits how far back we search to 10 * 32 blocks - const skipSize = uint64(32) - for i := 0; i < 10; i++ { - if l1HeadNum < skipSize { - // Too close to genesis, give up and just use the original block - r.log.Info("Failed to find prior batch.") - return l2BlockNum, nil - } - l1HeadNum -= skipSize - prevSafeHead, err := client.SafeHeadAtL1Block(ctx, l1HeadNum) - if err != nil { - return 0, fmt.Errorf("failed to get prior safe head at L1 block %v: %w", l1HeadNum, err) - } - if prevSafeHead.SafeHead.Number < l2BlockNum { - switch rand.Intn(3) { - case 0: // First block of span batch - return prevSafeHead.SafeHead.Number + 1, nil - case 1: // Last block of span batch - return prevSafeHead.SafeHead.Number, nil - case 2: // Random block, probably but not guaranteed to be in the middle of a span batch - firstBlockInSpanBatch := prevSafeHead.SafeHead.Number + 1 - if l2BlockNum <= firstBlockInSpanBatch { - // There is only one block in the next batch so we just have to use it - return l2BlockNum, nil - } - offset := rand.Intn(int(l2BlockNum - firstBlockInSpanBatch)) - return firstBlockInSpanBatch + uint64(offset), nil - } - - } - if prevSafeHead.SafeHead.Number < l2BlockNum { - // We walked back far enough to be before the batch that included l2BlockNum - // So use the first block after the prior safe head as the disputed block. - // It must be the first block in a batch. - return prevSafeHead.SafeHead.Number + 1, nil - } - } - r.log.Warn("Failed to find prior batch", "l2BlockNum", l2BlockNum, "earliestCheckL1Block", l1HeadNum) - return l2BlockNum, nil -} - func (r *Runner) Stop(ctx context.Context) error { r.log.Info("Stopping") if !r.running.CompareAndSwap(true, false) { diff --git a/op-conductor/conductor/service_test.go b/op-conductor/conductor/service_test.go index ae7332af2b..704b52cc24 100644 --- a/op-conductor/conductor/service_test.go +++ b/op-conductor/conductor/service_test.go @@ -187,11 +187,11 @@ func updateStatusAndExecuteAction[T any](s *OpConductorTestSuite, ch chan T, sta } func (s *OpConductorTestSuite) updateLeaderStatusAndExecuteAction(status bool) { - updateStatusAndExecuteAction[bool](s, s.leaderUpdateCh, status) + updateStatusAndExecuteAction(s, s.leaderUpdateCh, status) } func (s *OpConductorTestSuite) updateHealthStatusAndExecuteAction(status error) { - updateStatusAndExecuteAction[error](s, s.healthUpdateCh, status) + updateStatusAndExecuteAction(s, s.healthUpdateCh, status) } func (s *OpConductorTestSuite) executeAction() { diff --git a/op-deployer/.goreleaser.yaml b/op-deployer/.goreleaser.yaml index 5040ed4bc4..eaf05a78dc 100644 --- a/op-deployer/.goreleaser.yaml +++ b/op-deployer/.goreleaser.yaml @@ -32,8 +32,8 @@ builds: ldflags: - -X main.GitCommit={{ .FullCommit }} - -X main.GitDate={{ .CommitDate }} - - -X github.com/ethereum-optimism/optimism/op-chain-ops/deployer/version.Version={{ .Version }} - - -X github.com/ethereum-optimism/optimism/op-chain-ops/deployer/version.Meta="unstable" + - -X github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/version.Version={{ .Version }} + - -X github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/version.Meta= archives: - format: tar.gz diff --git a/op-deployer/book/src/SUMMARY.md b/op-deployer/book/src/SUMMARY.md index 862b2bba9c..570d59ea81 100644 --- a/op-deployer/book/src/SUMMARY.md +++ b/op-deployer/book/src/SUMMARY.md @@ -6,9 +6,10 @@ - [Installation](user-guide/installation.md) - [Usage](user-guide/usage.md) + - [bootstrap](user-guide/bootstrap.md) - [init](user-guide/init.md) - [apply](user-guide/apply.md) - - [bootstrap](user-guide/bootstrap.md) + - [verify](user-guide/verify.md) - [Known Limitations](user-guide/known-limitations.md) # Reference Guide diff --git a/op-deployer/book/src/reference-guide/releases.md b/op-deployer/book/src/reference-guide/releases.md index aefad5d89d..b53591a863 100644 --- a/op-deployer/book/src/reference-guide/releases.md +++ b/op-deployer/book/src/reference-guide/releases.md @@ -2,28 +2,34 @@ ## Versioning -For all releases after `v0.0.11`, each minor version of OP Deployer will support the current governance-approved -release of the smart contracts as well as the tip of the `develop` branch at the time the tag was created. If you -want to deploy an earlier version of the contracts (which may be dangerous!), you should use an earlier version of -OP Deployer. This setup allows our smart contract developers to make breaking changes on `develop`, while still -allowing new chains to be deployed and upgraded using production-ready smart contracts. +For all releases after `v0.0.11`, each minor version of OP Deployer will support a single release of the +governance-approved smart contracts. If you want to deploy an earlier version of the contracts (which may be +dangerous!), you should use an earlier version of OP Deployer. This setup allows our smart contract developers to make +breaking changes on `develop`, while still allowing new chains to be deployed and upgraded using production-ready smart +contracts. + +If you deploy from an HTTPS or file [locator](./artifacts-locators.md), the deployment behavior will match the +contract's tag. For example, if version `v0.2.0` supports `v2.0.0` then the deployment will work as if you were +deploying `op-contracts/v2.0.0`. Typically, errors like `unknown selector: ` imply that you're using the wrong +version of OP Deployer for your contract artifacts. If this happens, we recommend trying different versions until you +get one that works. Note that this workflow is **not recommended** for production chains. -For example (note that these are just examples, check out the [releases][releases] page for the exact versions to use): +[releases]: https://github.com/ethereum-optimism/optimism/releases -- `v0.1.x` : Supports deploying `develop` and `op-contracts/v2.0.0`. -- `v0.2.x`: Supports deploying `develop` and `op-contracts/v3.0.0`. +## Version Backports -If you deploy from an HTTPS or file [locator](./artifacts-locators.md), the deployment behavior will match that of -the supported tag. For example, if you use `v0.1.x` then the deployment will work as if you were deploying -`op-contracts/v2.0.0`. Typically, errors like `unknown selector: ` imply that you're using the wrong -version of OP Deployer for your contract artifacts. If this happens, we recommend trying different versions until -you get one that works. Note that this workflow is **not recommended** for production chains. +From time to time, we may backport bugfixes from develop onto earlier versions of OP Deployer. The process for this is +as follows: -[releases]: https://github.com/ethereum-optimism/optimism/releases +1. If one doesn't exist already, make a new branch for the version lineage you're patching (e.g. `v0.2.x`). This branch + should be based on the latest release of that lineage. The branch should be named as follows: + `backports/op-deployer/`. +2. Open a PR with the backport against that branch. Be sure to reference the original commit in the backport. +3. Make and push a new tag on that lineage. ## Adding Support for New Contract Versions -Adding support for a new contract version is a multi-step process. Here's a high-level overview. For the sake of +Adding support for a new contract version is a multi-step process. Here's a high-level overview. For the sake of simplicity we will assume you are adding support for a new `rc` release. ### Step 1: Add Support on `develop` @@ -48,7 +54,7 @@ cd ../../op-deployer just calculate-artifacts-hash ``` -This will calculate the checksum of your artifacts as well as the hash of the artifacts tarball. OP Deployer uses +This will calculate the checksum of your artifacts as well as the hash of the artifacts tarball. OP Deployer uses these values to download and verify tagged contract locators. Now, update `standard/standard.go` with these values so that the new artifacts tarball can be downloaded: @@ -59,9 +65,9 @@ Now, update `standard/standard.go` with these values so that the new artifacts t const ContractsVXTag = "op-contracts/vX.Y.Z" var taggedReleases = map[string]TaggedRelease{ - // Other releases... - ContractsVXTag: { - ArtifactsHash: common.HexToHash(""), + // Other releases... + ContractsVXTag: { + ArtifactsHash: common.HexToHash(""), ContentHash: common.HexToHash(""), }, } @@ -80,7 +86,7 @@ Add the new RC to the [standard versions][std-vers] in the Superchain Registry. ### Step 4: Update the `validation` Package -The SR is pulled into OP Deployer via the `validation` package. Update it by running the following command from the +The SR is pulled into OP Deployer via the `validation` package. Update it by running the following command from the root of the monorepo: ```shell diff --git a/op-deployer/book/src/user-guide/usage.md b/op-deployer/book/src/user-guide/usage.md index 03a5b7073e..1eac72d431 100644 --- a/op-deployer/book/src/user-guide/usage.md +++ b/op-deployer/book/src/user-guide/usage.md @@ -7,9 +7,12 @@ Deployer, you can use `op-deployer help` to view the available commands. This following sections provide in-depth information on the different commands available. +- [`op-deployer bootstrap`][bootstrap]: Deploys shared contract instances for use with future invocations of OP Deployer. - [`op-deployer init`][init]: Initializes a new intent and state file. - [`op-deployer apply`][apply]: Deploys a new OP Chain based on the supplied intent. -- `op-deployer bootstrap`: Deploys shared contract instances for use with future invocations of OP Deployer. +- [`op-deployer verify`][verify]: Verifies the source code of deployed contracts on Etherscan. +[bootstrap]: bootstrap.md [init]: init.md -[apply]: apply.md \ No newline at end of file +[apply]: apply.md +[verify]: verify.md diff --git a/op-deployer/book/src/user-guide/verify.md b/op-deployer/book/src/user-guide/verify.md new file mode 100644 index 0000000000..ce43f47a4d --- /dev/null +++ b/op-deployer/book/src/user-guide/verify.md @@ -0,0 +1,86 @@ +# The Verify Command + +Once you have deployed contracts via [bootstrap][bootstrap], you can use the `verify` command to verify the source code on Etherscan. Constructor args used in the verification request are extracted automatically from contract initcode via the tx that created the contract. + +[bootstrap]: bootstrap.md + +You can call the `verify` command like this: + +```shell +op-deployer verify \ + --l1-rpc-url \ + --input-file \ + --etherscan-api-key \ + --artifacts-locator +``` + +## CLI Args + +### `--l1-rpc-url` + +Defines the RPC URL of the L1 chain to deploy to (currently only supports mainnet and sepolia). + +### `--input-file` + +The full filepath to the input .json file. This file should be a key/value store where the key is a contract name and the value is the contract address. The output of the `bootstrap superchain|implementations` commands is a good example of this format, and those output files can be fed directly into `verify`. Unless the `--contract-name` flag is passed, all contracts in the input file will be verified. + +Example: +```json +{ + "opcmAddress": "0x437d303c20ea12e0edba02478127b12cbad54626", + "opcmContractsContainerAddress": "0xf89d7ce62fc3a18354b37b045017d585f7e332ab", + "opcmGameTypeAdderAddress": "0x9aa4b6c0575e978dbe6d6bc31b7e4403ea8bd81d", + "opcmDeployerAddress": "0x535388c15294dc77a287430926aba5ba5fe6016a", + "opcmUpgraderAddress": "0x68a7a93750eb56dd043f5baa41022306e6cd50fa", + "delayedWETHImplAddress": "0x33ddc90167c923651e5aef8b14bc197f3e8e7b56", + "optimismPortalImplAddress": "0x54b75cb6f44e36768912e070cd9cb995fc887e6c", + "ethLockboxImplAddress": "0x05484deeb3067a5332960ca77a5f5603df878ced", + "preimageOracleSingletonAddress": "0xfbcd4b365f97cb020208b5875ceaf6de76ec068b", + "mipsSingletonAddress": "0xcc50288ad0d79278397785607ed675292dce37b1", + "systemConfigImplAddress": "0xfb24aa6d99824b2c526768e97b23694aa3fe31d6", + "l1CrossDomainMessengerImplAddress": "0x957c0bf84fe541efe46b020a6797fb1fb2eaa6ac", + "l1ERC721BridgeImplAddress": "0x62786d16978436f5d85404735a28b9eb237e63d0", + "l1StandardBridgeImplAddress": "0x6c9b377c00ec7e6755aec402cd1cfff34fa75728", + "optimismMintableERC20FactoryImplAddress": "0x3842175f3af499c27593c772c0765f862b909b93", + "disputeGameFactoryImplAddress": "0x70ed1725abb48e96be9f610811e33ed8a0fa97f9", + "anchorStateRegistryImplAddress": "0xce2206af314e5ed99b48239559bdf8a47b7524d4", + "superchainConfigImplAddress": "0x77008cdc99fb1cf559ac33ca3a67a4a2f04cc5ef", + "protocolVersionsImplAddress": "0x32e07ddb36833cae3ca1ec5f73ca348a7e9467f4" +} +``` + +### `--contract-name` (optional) + +Specifies a single contract name, matching a contract key within the input file, to verify. If not provided, all contracts in the input file will be verified. + +### `--artifacts-locator` + +The locator to forge-artifacts containing the output of the `forge build` command (i.e. compiled bytecode and solidity source code). This can be a local path (with a `file://` prefix), remote URL (with a `http://` or `https://` prefix), or standard contracts tag (with a `tag://op-contracts/v` prefix). + +## Output + +Output logs will be printed to the console and look something like the following. If the final results show `numFailed=0`, all contracts were verified successfully. +```sh +INFO [03-05|15:56:55.900] Formatting etherscan verify request name=superchainConfigProxyAddress address=0x805fc6750ec23bdD58f7BBd6ce073649134C638A +INFO [03-05|15:56:55.900] Opening artifact path=Proxy.sol/Proxy.json name=superchainConfigProxyAddress +INFO [03-05|15:56:55.905] contractName name=src/universal/Proxy.sol:Proxy +INFO [03-05|15:56:55.905] Extracting constructor args from initcode address=0x805fc6750ec23bdD58f7BBd6ce073649134C638A argSlots=1 +INFO [03-05|15:56:56.087] Contract creation tx hash txHash=0x71b377ccc11304afc32e1016c4828a34010a0d3d81701c7164fb19525ba4fbc4 +INFO [03-05|15:56:56.494] Successfully extracted constructor args address=0x805fc6750ec23bdD58f7BBd6ce073649134C638A +INFO [03-05|15:56:56.683] Verification request submitted name=superchainConfigProxyAddress address=0x805fc6750ec23bdD58f7BBd6ce073649134C638A +INFO [03-05|15:57:02.035] Verification complete name=superchainConfigProxyAddress address=0x805fc6750ec23bdD58f7BBd6ce073649134C638A +INFO [03-05|15:57:02.208] Formatting etherscan verify request name=protocolVersionsImplAddress address=0x658812BEb9bF6286D03fBF1B5B936e1af490b768 +INFO [03-05|15:57:02.208] Opening artifact path=ProtocolVersions.sol/ProtocolVersions.json name=protocolVersionsImplAddress +INFO [03-05|15:57:02.215] contractName name=src/L1/ProtocolVersions.sol:ProtocolVersions +INFO [03-05|15:57:02.418] Verification request submitted name=protocolVersionsImplAddress address=0x658812BEb9bF6286D03fBF1B5B936e1af490b768 +INFO [03-05|15:57:07.789] Verification complete name=protocolVersionsImplAddress address=0x658812BEb9bF6286D03fBF1B5B936e1af490b768 +INFO [03-05|15:57:07.971] Contract is already verified name=protocolVersionsProxyAddress address=0x17C64430Fa08475D41801Dfe36bAFeE9667c6fA7 +INFO [03-05|15:57:07.971] --- COMPLETE --- +INFO [03-05|15:57:07.971] final results numVerified=4 numSkipped=1 numFailed=0 +``` + +## Known Limitations + +- Does not currently work for contracts in the `opchain` bundle (deployed via `op-deployer apply`) that have constructor args. Those constructors args cannot be extracted from the deployment `tx.Data()` since `OPContractsManager.deploy()` uses factory pattern with CREATE2 to deploy those contracts. + +- Currently only supports etherscan block explorers. Blockscout support is planned but not yet implemented. diff --git a/op-deployer/cmd/op-deployer/main.go b/op-deployer/cmd/op-deployer/main.go index a3bd836398..2560d6a0c0 100644 --- a/op-deployer/cmd/op-deployer/main.go +++ b/op-deployer/cmd/op-deployer/main.go @@ -6,6 +6,7 @@ import ( "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/clean" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/upgrade" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/verify" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/bootstrap" @@ -66,6 +67,12 @@ func main() { Usage: "cleans up various things", Subcommands: clean.Commands, }, + { + Name: "verify", + Usage: "verifies deployed contracts on Etherscan", + Flags: cliapp.ProtectFlags(deployer.VerifyFlags), + Action: verify.VerifyCLI, + }, } app.Writer = os.Stdout app.ErrWriter = os.Stderr diff --git a/op-deployer/pkg/deployer/artifacts/download.go b/op-deployer/pkg/deployer/artifacts/download.go index 3840a4c44e..7fa543fe5a 100644 --- a/op-deployer/pkg/deployer/artifacts/download.go +++ b/op-deployer/pkg/deployer/artifacts/download.go @@ -112,7 +112,7 @@ func (d *HTTPDownloader) Download(ctx context.Context, url string, progress Down defer res.Body.Close() if err := os.MkdirAll(targetDir, 0755); err != nil { - return "", fmt.Errorf("failed to ensure cache directory: %w", err) + return "", fmt.Errorf("failed to ensure cache directory '%s': %w", targetDir, err) } tmpFile, err := os.CreateTemp(targetDir, "op-deployer-artifacts-*") if err != nil { diff --git a/op-deployer/pkg/deployer/bootstrap/flags.go b/op-deployer/pkg/deployer/bootstrap/flags.go index ba29f61411..e55bc204ed 100644 --- a/op-deployer/pkg/deployer/bootstrap/flags.go +++ b/op-deployer/pkg/deployer/bootstrap/flags.go @@ -10,7 +10,6 @@ import ( const ( OutfileFlagName = "outfile" - ArtifactsLocatorFlagName = "artifacts-locator" WithdrawalDelaySecondsFlagName = "withdrawal-delay-seconds" MinProposalSizeBytesFlagName = "min-proposal-size-bytes" ChallengePeriodSecondsFlagName = "challenge-period-seconds" @@ -19,6 +18,7 @@ const ( MIPSVersionFlagName = "mips-version" ProxyOwnerFlagName = "proxy-owner" SuperchainProxyAdminOwnerFlagName = "superchain-proxy-admin-owner" + L1ContractsReleaseFlagName = "l1-contracts-release" ProtocolVersionsOwnerFlagName = "protocol-versions-owner" GuardianFlagName = "guardian" PausedFlagName = "paused" @@ -33,11 +33,6 @@ var ( EnvVars: deployer.PrefixEnvVar("OUTFILE"), Value: "-", } - ArtifactsLocatorFlag = &cli.StringFlag{ - Name: ArtifactsLocatorFlagName, - Usage: "Locator for artifacts.", - EnvVars: deployer.PrefixEnvVar("ARTIFACTS_LOCATOR"), - } WithdrawalDelaySecondsFlag = &cli.Uint64Flag{ Name: WithdrawalDelaySecondsFlagName, Usage: "Withdrawal delay in seconds.", @@ -114,7 +109,7 @@ var ( EnvVars: deployer.PrefixEnvVar("RECOMMENDED_PROTOCOL_VERSION"), } L1ContractsReleaseFlag = &cli.StringFlag{ - Name: "l1-contracts-release", + Name: L1ContractsReleaseFlagName, Usage: "Release version to set OPCM implementations for, of the format `op-contracts/vX.Y.Z`.", EnvVars: deployer.PrefixEnvVar("L1_CONTRACTS_RELEASE"), } @@ -149,7 +144,7 @@ var ImplementationsFlags = []cli.Flag{ deployer.L1RPCURLFlag, deployer.PrivateKeyFlag, OutfileFlag, - ArtifactsLocatorFlag, + deployer.ArtifactsLocatorFlag, L1ContractsReleaseFlag, MIPSVersionFlag, WithdrawalDelaySecondsFlag, @@ -167,7 +162,7 @@ var ProxyFlags = []cli.Flag{ deployer.L1RPCURLFlag, deployer.PrivateKeyFlag, OutfileFlag, - ArtifactsLocatorFlag, + deployer.ArtifactsLocatorFlag, ProxyOwnerFlag, } @@ -175,7 +170,7 @@ var SuperchainFlags = []cli.Flag{ deployer.L1RPCURLFlag, deployer.PrivateKeyFlag, OutfileFlag, - ArtifactsLocatorFlag, + deployer.ArtifactsLocatorFlag, SuperchainProxyAdminOwnerFlag, ProtocolVersionsOwnerFlag, GuardianFlag, @@ -188,7 +183,7 @@ var ValidatorFlags = []cli.Flag{ deployer.L1RPCURLFlag, deployer.PrivateKeyFlag, OutfileFlag, - ArtifactsLocatorFlag, + deployer.ArtifactsLocatorFlag, ConfigFileFlag, } diff --git a/op-deployer/pkg/deployer/bootstrap/proxy.go b/op-deployer/pkg/deployer/bootstrap/proxy.go index 06f4f762b5..865dfe5b8a 100644 --- a/op-deployer/pkg/deployer/bootstrap/proxy.go +++ b/op-deployer/pkg/deployer/bootstrap/proxy.go @@ -77,7 +77,7 @@ func ProxyCLI(cliCtx *cli.Context) error { l1RPCUrl := cliCtx.String(deployer.L1RPCURLFlagName) privateKey := cliCtx.String(deployer.PrivateKeyFlagName) outfile := cliCtx.String(OutfileFlagName) - artifactsURLStr := cliCtx.String(ArtifactsLocatorFlagName) + artifactsURLStr := cliCtx.String(deployer.ArtifactsLocatorFlagName) cacheDir := cliCtx.String(deployer.CacheDirFlag.Name) artifactsLocator := new(artifacts.Locator) diff --git a/op-deployer/pkg/deployer/bootstrap/superchain.go b/op-deployer/pkg/deployer/bootstrap/superchain.go index 95055f2a2a..7628f257b3 100644 --- a/op-deployer/pkg/deployer/bootstrap/superchain.go +++ b/op-deployer/pkg/deployer/bootstrap/superchain.go @@ -87,7 +87,7 @@ func SuperchainCLI(cliCtx *cli.Context) error { l1RPCUrl := cliCtx.String(deployer.L1RPCURLFlagName) privateKey := cliCtx.String(deployer.PrivateKeyFlagName) - artifactsURLStr := cliCtx.String(ArtifactsLocatorFlagName) + artifactsURLStr := cliCtx.String(deployer.ArtifactsLocatorFlagName) artifactsLocator := new(artifacts.Locator) if err := artifactsLocator.UnmarshalText([]byte(artifactsURLStr)); err != nil { return fmt.Errorf("failed to parse artifacts URL: %w", err) diff --git a/op-deployer/pkg/deployer/bootstrap/validator.go b/op-deployer/pkg/deployer/bootstrap/validator.go index f32b00b13d..b37283fe97 100644 --- a/op-deployer/pkg/deployer/bootstrap/validator.go +++ b/op-deployer/pkg/deployer/bootstrap/validator.go @@ -47,6 +47,7 @@ type ValidatorInput struct { ProtocolVersionsImpl common.Address `json:"protocolVersionsImpl"` L1ERC721BridgeImpl common.Address `json:"l1ERC721BridgeImpl"` OptimismPortalImpl common.Address `json:"optimismPortalImpl"` + ETHLockboxImpl common.Address `json:"ethLockboxImpl" evm:"ethLockboxImpl"` SystemConfigImpl common.Address `json:"systemConfigImpl"` OptimismMintableERC20FactoryImpl common.Address `json:"optimismMintableERC20FactoryImpl"` L1CrossDomainMessengerImpl common.Address `json:"l1CrossDomainMessengerImpl"` @@ -95,7 +96,7 @@ func ValidatorCLI(cliCtx *cli.Context) error { l1RPCUrl := cliCtx.String(deployer.L1RPCURLFlagName) privateKey := cliCtx.String(deployer.PrivateKeyFlagName) outfile := cliCtx.String(OutfileFlagName) - artifactsURLStr := cliCtx.String(ArtifactsLocatorFlagName) + artifactsURLStr := cliCtx.String(deployer.ArtifactsLocatorFlagName) configFile := cliCtx.String(ConfigFileFlag.Name) cacheDir := cliCtx.String(deployer.CacheDirFlag.Name) diff --git a/op-deployer/pkg/deployer/bootstrap/validator_test.go b/op-deployer/pkg/deployer/bootstrap/validator_test.go index ff58e71486..e2450b599b 100644 --- a/op-deployer/pkg/deployer/bootstrap/validator_test.go +++ b/op-deployer/pkg/deployer/bootstrap/validator_test.go @@ -62,6 +62,7 @@ func testValidator(t *testing.T, forkRPCURL string, loc *artifacts.Locator, rele ProtocolVersionsImpl: common.Address{'2'}, L1ERC721BridgeImpl: common.Address{'3'}, OptimismPortalImpl: common.Address{'4'}, + ETHLockboxImpl: common.Address{'5'}, SystemConfigImpl: common.Address{'5'}, OptimismMintableERC20FactoryImpl: common.Address{'6'}, L1CrossDomainMessengerImpl: common.Address{'7'}, diff --git a/op-deployer/pkg/deployer/flags.go b/op-deployer/pkg/deployer/flags.go index 09e300981a..5ec2e00294 100644 --- a/op-deployer/pkg/deployer/flags.go +++ b/op-deployer/pkg/deployer/flags.go @@ -2,6 +2,7 @@ package deployer import ( "fmt" + "log" "os" "path" @@ -13,15 +14,19 @@ import ( ) const ( - EnvVarPrefix = "DEPLOYER" - L1RPCURLFlagName = "l1-rpc-url" - CacheDirFlagName = "cache-dir" - L1ChainIDFlagName = "l1-chain-id" - L2ChainIDsFlagName = "l2-chain-ids" - WorkdirFlagName = "workdir" - OutdirFlagName = "outdir" - PrivateKeyFlagName = "private-key" - IntentTypeFlagName = "intent-type" + EnvVarPrefix = "DEPLOYER" + L1RPCURLFlagName = "l1-rpc-url" + CacheDirFlagName = "cache-dir" + L1ChainIDFlagName = "l1-chain-id" + ArtifactsLocatorFlagName = "artifacts-locator" + L2ChainIDsFlagName = "l2-chain-ids" + WorkdirFlagName = "workdir" + OutdirFlagName = "outdir" + PrivateKeyFlagName = "private-key" + IntentTypeFlagName = "intent-type" + EtherscanAPIKeyFlagName = "etherscan-api-key" + InputFileFlagName = "input-file" + ContractNameFlagName = "contract-name" ) type DeploymentTarget string @@ -48,14 +53,14 @@ func NewDeploymentTarget(s string) (DeploymentTarget, error) { } } -var homeDir string - -func init() { - var err error - homeDir, err = os.UserHomeDir() +func GetDefaultCacheDir() string { + homeDir, err := os.UserHomeDir() if err != nil { - panic(fmt.Sprintf("failed to get home directory: %s", err)) + fallbackDir := ".op-deployer/cache" + log.Printf("error getting user home directory: %v, using fallback directory: %s\n", err, fallbackDir) + return fallbackDir } + return path.Join(homeDir, ".op-deployer/cache") } var ( @@ -67,12 +72,17 @@ var ( "L1_RPC_URL", }, } + ArtifactsLocatorFlag = &cli.StringFlag{ + Name: ArtifactsLocatorFlagName, + Usage: "Locator for artifacts.", + EnvVars: PrefixEnvVar("ARTIFACTS_LOCATOR"), + } CacheDirFlag = &cli.StringFlag{ Name: CacheDirFlagName, Usage: "Cache directory. " + "If set, the deployer will attempt to cache downloaded artifacts in the specified directory.", EnvVars: PrefixEnvVar("CACHE_DIR"), - Value: path.Join(homeDir, ".op-deployer/cache"), + Value: GetDefaultCacheDir(), } L1ChainIDFlag = &cli.Uint64Flag{ Name: L1ChainIDFlagName, @@ -117,6 +127,22 @@ var ( "intent-config-type", }, } + EtherscanAPIKeyFlag = &cli.StringFlag{ + Name: EtherscanAPIKeyFlagName, + Usage: "etherscan API key for contract verification.", + EnvVars: PrefixEnvVar("ETHERSCAN_API_KEY"), + Required: true, + } + InputFileFlag = &cli.StringFlag{ + Name: InputFileFlagName, + Usage: "filepath of input file for command", + EnvVars: PrefixEnvVar("INPUT_FILE"), + } + ContractNameFlag = &cli.StringFlag{ + Name: ContractNameFlagName, + Usage: "contract name (matching a field within a contract bundle struct)", + EnvVars: PrefixEnvVar("CONTRACT_NAME"), + } ) var GlobalFlags = append([]cli.Flag{CacheDirFlag}, oplog.CLIFlags(EnvVarPrefix)...) @@ -141,6 +167,14 @@ var UpgradeFlags = []cli.Flag{ DeploymentTargetFlag, } +var VerifyFlags = []cli.Flag{ + L1RPCURLFlag, + ArtifactsLocatorFlag, + EtherscanAPIKeyFlag, + InputFileFlag, + ContractNameFlag, +} + func PrefixEnvVar(name string) []string { return op_service.PrefixEnvVar(EnvVarPrefix, name) } diff --git a/op-deployer/pkg/deployer/inspect/l1.go b/op-deployer/pkg/deployer/inspect/l1.go index cabcc997c4..b5205c42c9 100644 --- a/op-deployer/pkg/deployer/inspect/l1.go +++ b/op-deployer/pkg/deployer/inspect/l1.go @@ -2,6 +2,7 @@ package inspect import ( "fmt" + "reflect" "github.com/ethereum-optimism/optimism/op-chain-ops/genesis" @@ -20,6 +21,39 @@ type L1Contracts struct { ImplementationsDeployment ImplementationsDeployment `json:"implementationsDeployment"` } +const ( + SuperchainBundle = "superchain" + ImplementationsBundle = "implementations" + OpChainBundle = "opchain" +) + +var ContractBundles = []string{ + SuperchainBundle, + ImplementationsBundle, + OpChainBundle, +} + +func (l L1Contracts) GetContractAddress(name string, bundleName string) (common.Address, error) { + var bundle interface{} + switch bundleName { + case SuperchainBundle: + bundle = l.SuperchainDeployment + case ImplementationsBundle: + bundle = l.ImplementationsDeployment + case OpChainBundle: + bundle = l.OpChainDeployment + default: + return common.Address{}, fmt.Errorf("invalid contract bundle type: %s", bundleName) + } + + field := reflect.ValueOf(bundle).FieldByName(name) + if !field.IsValid() { + return common.Address{}, fmt.Errorf("contract %s not found in %s bundle", name, bundleName) + } + + return field.Interface().(common.Address), nil +} + func (l L1Contracts) AsL1Deployments() *genesis.L1Deployments { return &genesis.L1Deployments{ AddressManager: l.OpChainDeployment.AddressManagerAddress, @@ -37,6 +71,8 @@ func (l L1Contracts) AsL1Deployments() *genesis.L1Deployments { OptimismMintableERC20FactoryProxy: l.OpChainDeployment.OptimismMintableERC20FactoryProxyAddress, OptimismPortal: l.ImplementationsDeployment.OptimismPortalImplAddress, OptimismPortalProxy: l.OpChainDeployment.OptimismPortalProxyAddress, + ETHLockbox: l.ImplementationsDeployment.ETHLockboxImplAddress, + ETHLockboxProxy: l.OpChainDeployment.ETHLockboxProxyAddress, ProxyAdmin: l.OpChainDeployment.ProxyAdminAddress, SystemConfig: l.ImplementationsDeployment.SystemConfigImplAddress, SystemConfigProxy: l.OpChainDeployment.SystemConfigProxyAddress, @@ -64,6 +100,7 @@ type OpChainDeployment struct { L1StandardBridgeProxyAddress common.Address `json:"l1StandardBridgeProxyAddress"` L1CrossDomainMessengerProxyAddress common.Address `json:"l1CrossDomainMessengerProxyAddress"` OptimismPortalProxyAddress common.Address `json:"optimismPortalProxyAddress"` + ETHLockboxProxyAddress common.Address `json:"ethLockboxProxyAddress"` DisputeGameFactoryProxyAddress common.Address `json:"disputeGameFactoryProxyAddress"` AnchorStateRegistryProxyAddress common.Address `json:"anchorStateRegistryProxyAddress"` AnchorStateRegistryImplAddress common.Address `json:"anchorStateRegistryImplAddress"` @@ -79,6 +116,7 @@ type ImplementationsDeployment struct { OpcmAddress common.Address `json:"opcmAddress"` DelayedWETHImplAddress common.Address `json:"delayedWETHImplAddress"` OptimismPortalImplAddress common.Address `json:"optimismPortalImplAddress"` + ETHLockboxImplAddress common.Address `json:"ethLockboxImplAddress"` PreimageOracleSingletonAddress common.Address `json:"preimageOracleSingletonAddress"` MipsSingletonAddress common.Address `json:"mipsSingletonAddress"` SystemConfigImplAddress common.Address `json:"systemConfigImplAddress"` @@ -135,6 +173,7 @@ func L1(globalState *state.State, chainID common.Hash) (*L1Contracts, error) { L1StandardBridgeProxyAddress: chainState.L1StandardBridgeProxyAddress, L1CrossDomainMessengerProxyAddress: chainState.L1CrossDomainMessengerProxyAddress, OptimismPortalProxyAddress: chainState.OptimismPortalProxyAddress, + ETHLockboxProxyAddress: chainState.ETHLockboxProxyAddress, DisputeGameFactoryProxyAddress: chainState.DisputeGameFactoryProxyAddress, AnchorStateRegistryProxyAddress: chainState.AnchorStateRegistryProxyAddress, FaultDisputeGameAddress: chainState.FaultDisputeGameAddress, @@ -148,6 +187,7 @@ func L1(globalState *state.State, chainID common.Hash) (*L1Contracts, error) { OpcmAddress: globalState.ImplementationsDeployment.OpcmAddress, DelayedWETHImplAddress: globalState.ImplementationsDeployment.DelayedWETHImplAddress, OptimismPortalImplAddress: globalState.ImplementationsDeployment.OptimismPortalImplAddress, + ETHLockboxImplAddress: globalState.ImplementationsDeployment.ETHLockboxImplAddress, PreimageOracleSingletonAddress: globalState.ImplementationsDeployment.PreimageOracleSingletonAddress, MipsSingletonAddress: globalState.ImplementationsDeployment.MipsSingletonAddress, SystemConfigImplAddress: globalState.ImplementationsDeployment.SystemConfigImplAddress, diff --git a/op-deployer/pkg/deployer/integration_test/apply_test.go b/op-deployer/pkg/deployer/integration_test/apply_test.go index 83884e07ba..8bdcf1d56a 100644 --- a/op-deployer/pkg/deployer/integration_test/apply_test.go +++ b/op-deployer/pkg/deployer/integration_test/apply_test.go @@ -7,6 +7,7 @@ import ( "fmt" "log/slog" "math/big" + "os" "strings" "testing" @@ -52,6 +53,72 @@ func (d *deployerKey) String() string { return "deployer-key" } +func TestLiveChain(t *testing.T) { + op_e2e.InitParallel(t) + + for _, network := range []string{"mainnet", "sepolia"} { + t.Run(network, func(t *testing.T) { + testLiveChainNetwork(t, network) + }) + } +} + +func testLiveChainNetwork(t *testing.T, network string) { + op_e2e.InitParallel(t) + lgr := testlog.Logger(t, slog.LevelInfo) + rpcURL := os.Getenv(fmt.Sprintf("%s_RPC_URL", strings.ToUpper(network))) + require.NotEmpty(t, rpcURL) + + forkedL1, cleanup, err := devnet.NewForked(lgr, rpcURL) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, cleanup()) + }) + + l1Client, err := ethclient.Dial(forkedL1.RPCUrl()) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + l1ChainID, err := l1Client.ChainID(ctx) + require.NoError(t, err) + + pk, err := crypto.HexToECDSA("ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80") + require.NoError(t, err) + dk, err := devkeys.NewMnemonicDevKeys(devkeys.TestMnemonic) + require.NoError(t, err) + + testCacheDir := testutils.IsolatedTestDirWithAutoCleanup(t) + + intent, st := newIntent( + t, + l1ChainID, + dk, + uint256.NewInt(9999), + artifacts.DefaultL1ContractsLocator, + artifacts.DefaultL2ContractsLocator, + ) + cg := ethClientCodeGetter(ctx, l1Client) + + require.NoError(t, deployer.ApplyPipeline( + ctx, + deployer.ApplyPipelineOpts{ + DeploymentTarget: deployer.DeploymentTargetLive, + L1RPCUrl: forkedL1.RPCUrl(), + DeployerPrivateKey: pk, + Intent: intent, + State: st, + Logger: lgr, + StateWriter: pipeline.NoopStateWriter(), + CacheDir: testCacheDir, + }, + )) + + validateSuperchainDeployment(t, st, cg, false) + validateOPChainDeployment(t, cg, st, intent, false) +} + func TestEndToEndApply(t *testing.T) { op_e2e.InitParallel(t) @@ -120,7 +187,7 @@ func TestEndToEndApply(t *testing.T) { }, )) - validateSuperchainDeployment(t, st, cg) + validateSuperchainDeployment(t, st, cg, true) validateOPChainDeployment(t, cg, st, intent, false) }) @@ -144,7 +211,7 @@ func TestEndToEndApply(t *testing.T) { }, )) - validateSuperchainDeployment(t, st, cg) + validateSuperchainDeployment(t, st, cg, true) validateOPChainDeployment(t, cg, st, intent, false) }) @@ -228,7 +295,7 @@ func TestApplyGenesisStrategy(t *testing.T) { require.NoError(t, deployer.ApplyPipeline(ctx, opts)) cg := stateDumpCodeGetter(st) - validateSuperchainDeployment(t, st, cg) + validateSuperchainDeployment(t, st, cg, true) for i := range intent.Chains { t.Run(fmt.Sprintf("chain-%d", i), func(t *testing.T) { @@ -297,7 +364,7 @@ func TestProofParamOverrides(t *testing.T) { { "disputeGameFinalityDelaySeconds", uint64Caster, - st.ImplementationsDeployment.OptimismPortalImplAddress, + st.ImplementationsDeployment.AnchorStateRegistryImplAddress, }, { "faultGameAbsolutePrestate", @@ -334,24 +401,6 @@ func TestProofParamOverrides(t *testing.T) { } } -func TestInteropDeployment(t *testing.T) { - op_e2e.InitParallel(t) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - opts, intent, st := setupGenesisChain(t, defaultL1ChainID) - intent.UseInterop = true - - require.NoError(t, deployer.ApplyPipeline(ctx, opts)) - - chainState := st.Chains[0] - depManagerSlot := common.HexToHash("0x1708e077affb93e89be2665fb0fb72581be66f84dc00d25fed755ae911905b1c") - checkImmutable(t, st.L1StateDump.Data.Accounts, st.ImplementationsDeployment.SystemConfigImplAddress, depManagerSlot) - proxyAdminOwnerHash := common.BytesToHash(intent.Chains[0].Roles.SystemConfigOwner.Bytes()) - checkStorageSlot(t, st.L1StateDump.Data.Accounts, chainState.SystemConfigProxyAddress, depManagerSlot, proxyAdminOwnerHash) -} - func TestAltDADeployment(t *testing.T) { op_e2e.InitParallel(t) @@ -650,20 +699,25 @@ func stateDumpCodeGetter(st *state.State) codeGetter { } } -func validateSuperchainDeployment(t *testing.T, st *state.State, cg codeGetter) { - addrs := []struct { +func validateSuperchainDeployment(t *testing.T, st *state.State, cg codeGetter, includeSuperchainImpls bool) { + type addrTuple struct { name string addr common.Address - }{ + } + addrs := []addrTuple{ {"SuperchainProxyAdmin", st.SuperchainDeployment.ProxyAdminAddress}, {"SuperchainConfigProxy", st.SuperchainDeployment.SuperchainConfigProxyAddress}, - {"SuperchainConfigImpl", st.SuperchainDeployment.SuperchainConfigImplAddress}, {"ProtocolVersionsProxy", st.SuperchainDeployment.ProtocolVersionsProxyAddress}, - {"ProtocolVersionsImpl", st.SuperchainDeployment.ProtocolVersionsImplAddress}, {"Opcm", st.ImplementationsDeployment.OpcmAddress}, {"PreimageOracleSingleton", st.ImplementationsDeployment.PreimageOracleSingletonAddress}, {"MipsSingleton", st.ImplementationsDeployment.MipsSingletonAddress}, } + + if includeSuperchainImpls { + addrs = append(addrs, addrTuple{"SuperchainConfigImpl", st.SuperchainDeployment.SuperchainConfigImplAddress}) + addrs = append(addrs, addrTuple{"ProtocolVersionsImpl", st.SuperchainDeployment.ProtocolVersionsImplAddress}) + } + for _, addr := range addrs { t.Run(addr.name, func(t *testing.T) { code := cg(t, addr.addr) @@ -675,10 +729,11 @@ func validateSuperchainDeployment(t *testing.T, st *state.State, cg codeGetter) func validateOPChainDeployment(t *testing.T, cg codeGetter, st *state.State, intent *state.Intent, govEnabled bool) { // Validate that the implementation addresses are always set, even in subsequent deployments // that pull from an existing OPCM deployment. - implAddrs := []struct { + type addrTuple struct { name string addr common.Address - }{ + } + implAddrs := []addrTuple{ {"DelayedWETHImplAddress", st.ImplementationsDeployment.DelayedWETHImplAddress}, {"OptimismPortalImplAddress", st.ImplementationsDeployment.OptimismPortalImplAddress}, {"SystemConfigImplAddress", st.ImplementationsDeployment.SystemConfigImplAddress}, @@ -690,6 +745,11 @@ func validateOPChainDeployment(t *testing.T, cg codeGetter, st *state.State, int {"MipsSingletonAddress", st.ImplementationsDeployment.MipsSingletonAddress}, {"PreimageOracleSingletonAddress", st.ImplementationsDeployment.PreimageOracleSingletonAddress}, } + + if !intent.L1ContractsLocator.IsTag() { + implAddrs = append(implAddrs, addrTuple{"ETHLockboxImplAddress", st.ImplementationsDeployment.ETHLockboxImplAddress}) + } + for _, addr := range implAddrs { require.NotEmpty(t, addr.addr, "%s should be set", addr.name) code := cg(t, addr.addr) diff --git a/op-deployer/pkg/deployer/opcm/dispute_game_factory.go b/op-deployer/pkg/deployer/opcm/dispute_game_factory.go index 6e79f0683a..6148052351 100644 --- a/op-deployer/pkg/deployer/opcm/dispute_game_factory.go +++ b/op-deployer/pkg/deployer/opcm/dispute_game_factory.go @@ -8,7 +8,6 @@ import ( type SetDisputeGameImplInput struct { Factory common.Address Impl common.Address - Portal common.Address AnchorStateRegistry common.Address GameType uint32 } diff --git a/op-deployer/pkg/deployer/opcm/implementations.go b/op-deployer/pkg/deployer/opcm/implementations.go index 163e899fc9..66be66c9be 100644 --- a/op-deployer/pkg/deployer/opcm/implementations.go +++ b/op-deployer/pkg/deployer/opcm/implementations.go @@ -30,24 +30,25 @@ func (input *DeployImplementationsInput) InputSet() bool { } type DeployImplementationsOutput struct { - Opcm common.Address - OpcmContractsContainer common.Address - OpcmGameTypeAdder common.Address - OpcmDeployer common.Address - OpcmUpgrader common.Address - DelayedWETHImpl common.Address - OptimismPortalImpl common.Address - PreimageOracleSingleton common.Address - MipsSingleton common.Address - SystemConfigImpl common.Address - L1CrossDomainMessengerImpl common.Address - L1ERC721BridgeImpl common.Address - L1StandardBridgeImpl common.Address - OptimismMintableERC20FactoryImpl common.Address - DisputeGameFactoryImpl common.Address - AnchorStateRegistryImpl common.Address - SuperchainConfigImpl common.Address - ProtocolVersionsImpl common.Address + Opcm common.Address `json:"opcmAddress"` + OpcmContractsContainer common.Address `json:"opcmContractsContainerAddress"` + OpcmGameTypeAdder common.Address `json:"opcmGameTypeAdderAddress"` + OpcmDeployer common.Address `json:"opcmDeployerAddress"` + OpcmUpgrader common.Address `json:"opcmUpgraderAddress"` + DelayedWETHImpl common.Address `json:"delayedWETHImplAddress"` + OptimismPortalImpl common.Address `json:"optimismPortalImplAddress"` + ETHLockboxImpl common.Address `json:"ethLockboxImplAddress" evm:"ethLockboxImpl"` + PreimageOracleSingleton common.Address `json:"preimageOracleSingletonAddress"` + MipsSingleton common.Address `json:"mipsSingletonAddress"` + SystemConfigImpl common.Address `json:"systemConfigImplAddress"` + L1CrossDomainMessengerImpl common.Address `json:"l1CrossDomainMessengerImplAddress"` + L1ERC721BridgeImpl common.Address `json:"l1ERC721BridgeImplAddress"` + L1StandardBridgeImpl common.Address `json:"l1StandardBridgeImplAddress"` + OptimismMintableERC20FactoryImpl common.Address `json:"optimismMintableERC20FactoryImplAddress"` + DisputeGameFactoryImpl common.Address `json:"disputeGameFactoryImplAddress"` + AnchorStateRegistryImpl common.Address `json:"anchorStateRegistryImplAddress"` + SuperchainConfigImpl common.Address `json:"superchainConfigImplAddress"` + ProtocolVersionsImpl common.Address `json:"protocolVersionsImplAddress"` } func (output *DeployImplementationsOutput) CheckOutput(input common.Address) error { @@ -80,9 +81,6 @@ func DeployImplementations( defer cleanupOutput() implContract := "DeployImplementations" - if input.UseInterop { - implContract = "DeployImplementationsInterop" - } deployScript, cleanupDeploy, err := script.WithScript[DeployImplementationsScript](host, "DeployImplementations.s.sol", implContract) if err != nil { return output, fmt.Errorf("failed to load %s script: %w", implContract, err) @@ -90,15 +88,12 @@ func DeployImplementations( defer cleanupDeploy() opcmContract := "OPContractsManager" - if err := host.RememberOnLabel("OPContractsManager", "OPContractsManager.sol", opcmContract); err != nil { + if err := host.RememberOnLabel("OPContractsManager", opcmContract+".sol", opcmContract); err != nil { return output, fmt.Errorf("failed to link OPContractsManager label: %w", err) } // So we can see in detail where the SystemConfig interop initializer fails sysConfig := "SystemConfig" - if input.UseInterop { - sysConfig = "SystemConfigInterop" - } if err := host.RememberOnLabel("SystemConfigImpl", sysConfig+".sol", sysConfig); err != nil { return output, fmt.Errorf("failed to link SystemConfig label: %w", err) } diff --git a/op-deployer/pkg/deployer/opcm/manage_dependencies.go b/op-deployer/pkg/deployer/opcm/manage_dependencies.go deleted file mode 100644 index 2a05de55f3..0000000000 --- a/op-deployer/pkg/deployer/opcm/manage_dependencies.go +++ /dev/null @@ -1,21 +0,0 @@ -package opcm - -import ( - "math/big" - - "github.com/ethereum-optimism/optimism/op-chain-ops/script" - "github.com/ethereum/go-ethereum/common" -) - -type ManageDependenciesInput struct { - ChainId *big.Int - SystemConfig common.Address - Remove bool -} - -func ManageDependencies( - host *script.Host, - input ManageDependenciesInput, -) error { - return RunScriptVoid[ManageDependenciesInput](host, input, "ManageDependencies.s.sol", "ManageDependencies") -} diff --git a/op-deployer/pkg/deployer/opcm/opchain.go b/op-deployer/pkg/deployer/opcm/opchain.go index a9015044b9..9d87fb4979 100644 --- a/op-deployer/pkg/deployer/opcm/opchain.go +++ b/op-deployer/pkg/deployer/opcm/opchain.go @@ -63,6 +63,7 @@ type DeployOPChainOutput struct { L1CrossDomainMessengerProxy common.Address // Fault proof contracts below. OptimismPortalProxy common.Address + ETHLockboxProxy common.Address `evm:"ethLockboxProxy"` DisputeGameFactoryProxy common.Address AnchorStateRegistryProxy common.Address FaultDisputeGame common.Address @@ -92,6 +93,7 @@ type ReadImplementationAddressesInput struct { type ReadImplementationAddressesOutput struct { DelayedWETH common.Address OptimismPortal common.Address + ETHLockbox common.Address `evm:"ethLockbox"` SystemConfig common.Address L1CrossDomainMessenger common.Address L1ERC721Bridge common.Address diff --git a/op-deployer/pkg/deployer/opcm/superchain.go b/op-deployer/pkg/deployer/opcm/superchain.go index 35811bd693..abc9a1bd12 100644 --- a/op-deployer/pkg/deployer/opcm/superchain.go +++ b/op-deployer/pkg/deployer/opcm/superchain.go @@ -26,11 +26,11 @@ func (dsi *DeploySuperchainInput) InputSet() bool { } type DeploySuperchainOutput struct { - SuperchainProxyAdmin common.Address - SuperchainConfigImpl common.Address - SuperchainConfigProxy common.Address - ProtocolVersionsImpl common.Address - ProtocolVersionsProxy common.Address + SuperchainProxyAdmin common.Address `json:"proxyAdminAddress"` + SuperchainConfigImpl common.Address `json:"superchainConfigImplAddress"` + SuperchainConfigProxy common.Address `json:"superchainConfigProxyAddress"` + ProtocolVersionsImpl common.Address `json:"protocolVersionsImplAddress"` + ProtocolVersionsProxy common.Address `json:"protocolVersionsProxyAddress"` } func (output *DeploySuperchainOutput) CheckOutput(input common.Address) error { diff --git a/op-deployer/pkg/deployer/pipeline/dispute_games.go b/op-deployer/pkg/deployer/pipeline/dispute_games.go index dd95398556..5363ba7c49 100644 --- a/op-deployer/pkg/deployer/pipeline/dispute_games.go +++ b/op-deployer/pkg/deployer/pipeline/dispute_games.go @@ -127,13 +127,12 @@ func deployDisputeGame( lgr.Info("setting dispute game impl on factory", "respected", game.MakeRespected) sdgiInput := opcm.SetDisputeGameImplInput{ - Factory: thisState.DisputeGameFactoryProxyAddress, - Impl: out.DisputeGameImpl, - GameType: game.DisputeGameType, - AnchorStateRegistry: thisState.AnchorStateRegistryProxyAddress, + Factory: thisState.DisputeGameFactoryProxyAddress, + Impl: out.DisputeGameImpl, + GameType: game.DisputeGameType, } if game.MakeRespected { - sdgiInput.Portal = thisState.OptimismPortalProxyAddress + sdgiInput.AnchorStateRegistry = thisState.AnchorStateRegistryProxyAddress } if err := opcm.SetDisputeGameImpl( env.L1ScriptHost, diff --git a/op-deployer/pkg/deployer/pipeline/implementations.go b/op-deployer/pkg/deployer/pipeline/implementations.go index ef591210b6..a88bec4354 100644 --- a/op-deployer/pkg/deployer/pipeline/implementations.go +++ b/op-deployer/pkg/deployer/pipeline/implementations.go @@ -72,6 +72,7 @@ func DeployImplementations(env *Env, intent *state.Intent, st *state.State) erro OpcmUpgraderAddress: dio.OpcmUpgrader, DelayedWETHImplAddress: dio.DelayedWETHImpl, OptimismPortalImplAddress: dio.OptimismPortalImpl, + ETHLockboxImplAddress: dio.ETHLockboxImpl, PreimageOracleSingletonAddress: dio.PreimageOracleSingleton, MipsSingletonAddress: dio.MipsSingleton, SystemConfigImplAddress: dio.SystemConfigImpl, diff --git a/op-deployer/pkg/deployer/pipeline/init.go b/op-deployer/pkg/deployer/pipeline/init.go index 2b5d239657..516edd991a 100644 --- a/op-deployer/pkg/deployer/pipeline/init.go +++ b/op-deployer/pkg/deployer/pipeline/init.go @@ -38,25 +38,34 @@ func InitLiveStrategy(ctx context.Context, env *Env, intent *state.Intent, st *s return fmt.Errorf("unsupported L2 version: %s", intent.L2ContractsLocator.Tag) } - if isL1Tag && hasPredeployedOPCM { - superCfg, err := standard.SuperchainFor(intent.L1ChainID) + isStandardIntent := intent.ConfigType == state.IntentTypeStandard || + intent.ConfigType == state.IntentTypeStandardOverrides + if isL1Tag && hasPredeployedOPCM && isStandardIntent { + stdRoles, err := state.GetStandardSuperchainRoles(intent.L1ChainID) if err != nil { - return fmt.Errorf("error getting superchain config: %w", err) + return fmt.Errorf("error getting superchain roles: %w", err) } - proxyAdmin, err := standard.SuperchainProxyAdminAddrFor(intent.L1ChainID) - if err != nil { - return fmt.Errorf("error getting superchain proxy admin address: %w", err) - } - - st.SuperchainDeployment = &state.SuperchainDeployment{ - ProxyAdminAddress: proxyAdmin, - ProtocolVersionsProxyAddress: superCfg.ProtocolVersionsAddr, - SuperchainConfigProxyAddress: superCfg.SuperchainConfigAddr, - } - - st.ImplementationsDeployment = &state.ImplementationsDeployment{ - OpcmAddress: opcmAddress, + if *intent.SuperchainRoles == *stdRoles { + superCfg, err := standard.SuperchainFor(intent.L1ChainID) + if err != nil { + return fmt.Errorf("error getting superchain config: %w", err) + } + + proxyAdmin, err := standard.SuperchainProxyAdminAddrFor(intent.L1ChainID) + if err != nil { + return fmt.Errorf("error getting superchain proxy admin address: %w", err) + } + + st.SuperchainDeployment = &state.SuperchainDeployment{ + ProxyAdminAddress: proxyAdmin, + ProtocolVersionsProxyAddress: superCfg.ProtocolVersionsAddr, + SuperchainConfigProxyAddress: superCfg.SuperchainConfigAddr, + } + + st.ImplementationsDeployment = &state.ImplementationsDeployment{ + OpcmAddress: opcmAddress, + } } } diff --git a/op-deployer/pkg/deployer/pipeline/init_test.go b/op-deployer/pkg/deployer/pipeline/init_test.go new file mode 100644 index 0000000000..121ca7b3d9 --- /dev/null +++ b/op-deployer/pkg/deployer/pipeline/init_test.go @@ -0,0 +1,171 @@ +package pipeline + +import ( + "context" + "log/slog" + "os" + "testing" + "time" + + "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-service/testlog" + "github.com/ethereum-optimism/optimism/op-service/testutils/devnet" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/stretchr/testify/require" +) + +func TestInitLiveStrategy_OPCMReuseLogicSepolia(t *testing.T) { + rpcURL := os.Getenv("SEPOLIA_RPC_URL") + require.NotEmpty(t, rpcURL, "SEPOLIA_RPC_URL must be set") + + lgr := testlog.Logger(t, slog.LevelInfo) + retryProxy := devnet.NewRetryProxy(lgr, rpcURL) + require.NoError(t, retryProxy.Start()) + t.Cleanup(func() { + require.NoError(t, retryProxy.Stop()) + }) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + client, err := ethclient.Dial(retryProxy.Endpoint()) + require.NoError(t, err) + + l1ChainID := uint64(11155111) + t.Run("untagged L1 locator", func(t *testing.T) { + st := &state.State{ + Version: 1, + } + require.NoError(t, InitLiveStrategy( + ctx, + &Env{ + L1Client: client, + Logger: lgr, + }, + &state.Intent{ + L1ChainID: l1ChainID, + L1ContractsLocator: artifacts.MustNewLocatorFromURL("file:///not-a-path"), + L2ContractsLocator: artifacts.MustNewLocatorFromURL("file:///not-a-path"), + }, + st, + )) + + // Defining a file locator will always deploy a new superchain and OPCM + require.Nil(t, st.SuperchainDeployment) + require.Nil(t, st.ImplementationsDeployment) + }) + + t.Run("tagged L1 locator with standard intent types and standard roles", func(t *testing.T) { + runTest := func(configType state.IntentType) { + stdSuperchainRoles, err := state.GetStandardSuperchainRoles(l1ChainID) + require.NoError(t, err) + + intent := &state.Intent{ + ConfigType: configType, + L1ChainID: l1ChainID, + L1ContractsLocator: artifacts.DefaultL1ContractsLocator, + L2ContractsLocator: artifacts.DefaultL2ContractsLocator, + SuperchainRoles: stdSuperchainRoles, + } + st := &state.State{ + Version: 1, + } + require.NoError(t, InitLiveStrategy( + ctx, + &Env{ + L1Client: client, + Logger: lgr, + }, + intent, + st, + )) + + // Defining a file locator will always deploy a new superchain and OPCM + superCfg, err := standard.SuperchainFor(l1ChainID) + require.NoError(t, err) + proxyAdmin, err := standard.SuperchainProxyAdminAddrFor(l1ChainID) + require.NoError(t, err) + opcmAddr, err := standard.ManagerImplementationAddrFor(l1ChainID, intent.L1ContractsLocator.Tag) + require.NoError(t, err) + + expDeployment := &state.SuperchainDeployment{ + ProxyAdminAddress: proxyAdmin, + ProtocolVersionsProxyAddress: superCfg.ProtocolVersionsAddr, + SuperchainConfigProxyAddress: superCfg.SuperchainConfigAddr, + } + + // Tagged locator will reuse the existing superchain and OPCM + require.NotNil(t, st.SuperchainDeployment) + require.NotNil(t, st.ImplementationsDeployment) + require.Equal(t, *expDeployment, *st.SuperchainDeployment) + require.Equal(t, opcmAddr, st.ImplementationsDeployment.OpcmAddress) + } + + runTest(state.IntentTypeStandard) + runTest(state.IntentTypeStandardOverrides) + }) + + t.Run("tagged L1 locator with standard intent types and modified roles", func(t *testing.T) { + runTest := func(configType state.IntentType) { + intent := &state.Intent{ + ConfigType: configType, + L1ChainID: l1ChainID, + L1ContractsLocator: artifacts.DefaultL1ContractsLocator, + L2ContractsLocator: artifacts.DefaultL2ContractsLocator, + SuperchainRoles: &state.SuperchainRoles{ + Guardian: common.Address{0: 99}, + }, + } + st := &state.State{ + Version: 1, + } + require.NoError(t, InitLiveStrategy( + ctx, + &Env{ + L1Client: client, + Logger: lgr, + }, + intent, + st, + )) + + // Modified roles will cause a new superchain and OPCM to be deployed + require.Nil(t, st.SuperchainDeployment) + require.Nil(t, st.ImplementationsDeployment) + } + + runTest(state.IntentTypeStandard) + runTest(state.IntentTypeStandardOverrides) + }) + + t.Run("tagged locator with custom intent type", func(t *testing.T) { + intent := &state.Intent{ + ConfigType: state.IntentTypeCustom, + L1ChainID: l1ChainID, + L1ContractsLocator: artifacts.DefaultL1ContractsLocator, + L2ContractsLocator: artifacts.DefaultL2ContractsLocator, + SuperchainRoles: &state.SuperchainRoles{ + Guardian: common.Address{0: 99}, + }, + } + st := &state.State{ + Version: 1, + } + require.NoError(t, InitLiveStrategy( + ctx, + &Env{ + L1Client: client, + Logger: lgr, + }, + intent, + st, + )) + + // Custom intent types always deploy a new superchain and OPCM + require.Nil(t, st.SuperchainDeployment) + require.Nil(t, st.ImplementationsDeployment) + }) +} diff --git a/op-deployer/pkg/deployer/pipeline/opchain.go b/op-deployer/pkg/deployer/pipeline/opchain.go index 03d5b37567..fd6c647667 100644 --- a/op-deployer/pkg/deployer/pipeline/opchain.go +++ b/op-deployer/pkg/deployer/pipeline/opchain.go @@ -58,6 +58,7 @@ func DeployOPChain(env *Env, intent *state.Intent, st *state.State, chainID comm st.ImplementationsDeployment.DelayedWETHImplAddress = impls.DelayedWETH st.ImplementationsDeployment.OptimismPortalImplAddress = impls.OptimismPortal + st.ImplementationsDeployment.ETHLockboxImplAddress = impls.ETHLockbox st.ImplementationsDeployment.SystemConfigImplAddress = impls.SystemConfig st.ImplementationsDeployment.L1CrossDomainMessengerImplAddress = impls.L1CrossDomainMessenger st.ImplementationsDeployment.L1ERC721BridgeImplAddress = impls.L1ERC721Bridge @@ -123,6 +124,7 @@ func makeChainState(chainID common.Hash, dco opcm.DeployOPChainOutput) *state.Ch L1StandardBridgeProxyAddress: dco.L1StandardBridgeProxy, L1CrossDomainMessengerProxyAddress: dco.L1CrossDomainMessengerProxy, OptimismPortalProxyAddress: dco.OptimismPortalProxy, + ETHLockboxProxyAddress: dco.ETHLockboxProxy, DisputeGameFactoryProxyAddress: dco.DisputeGameFactoryProxy, AnchorStateRegistryProxyAddress: dco.AnchorStateRegistryProxy, FaultDisputeGameAddress: dco.FaultDisputeGame, diff --git a/op-deployer/pkg/deployer/pipeline/start_block.go b/op-deployer/pkg/deployer/pipeline/start_block.go index 19e8ee5aea..260391ae34 100644 --- a/op-deployer/pkg/deployer/pipeline/start_block.go +++ b/op-deployer/pkg/deployer/pipeline/start_block.go @@ -7,10 +7,34 @@ import ( "github.com/ethereum-optimism/optimism/op-chain-ops/genesis" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/state" + "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/rpc" ) +type l1BlockRefJSON struct { + Hash common.Hash `json:"hash"` + Number hexutil.Uint64 `json:"number"` + ParentHash common.Hash `json:"parentHash"` + Time hexutil.Uint64 `json:"timestamp"` +} + +func blockRefFromRpc(ctx context.Context, l1Client *rpc.Client, numberArg string) (eth.BlockRef, error) { + var l1BRJ l1BlockRefJSON + if err := l1Client.CallContext(ctx, &l1BRJ, "eth_getBlockByNumber", numberArg, false); err != nil { + return eth.BlockRef{}, fmt.Errorf("failed to get L1 block header for block: %w", err) + } + + headerBlockRef := eth.BlockRef{ + Number: uint64(l1BRJ.Number), + Hash: l1BRJ.Hash, + ParentHash: l1BRJ.ParentHash, + Time: uint64(l1BRJ.Time), + } + return headerBlockRef, nil +} + func SetStartBlockLiveStrategy(ctx context.Context, env *Env, st *state.State, chainID common.Hash) error { lgr := env.Logger.New("stage", "set-start-block", "strategy", "live") lgr.Info("setting start block", "id", chainID.Hex()) @@ -20,11 +44,12 @@ func SetStartBlockLiveStrategy(ctx context.Context, env *Env, st *state.State, c return fmt.Errorf("failed to get chain state: %w", err) } - startHeader, err := env.L1Client.HeaderByNumber(ctx, nil) + headerBlockRef, err := blockRefFromRpc(ctx, env.L1Client.Client(), "latest") if err != nil { - return fmt.Errorf("failed to get start block: %w", err) + return fmt.Errorf("failed to get L1 block header: %w", err) } - thisChainState.StartBlock = startHeader + + thisChainState.StartBlock = &headerBlockRef return nil } @@ -57,7 +82,7 @@ func SetStartBlockGenesisStrategy(env *Env, st *state.State, chainID common.Hash if err != nil { return fmt.Errorf("failed to build L1 developer genesis: %w", err) } - thisChainState.StartBlock = devGenesis.ToBlock().Header() + thisChainState.StartBlock = eth.BlockRefFromHeader(devGenesis.ToBlock().Header()) return nil } diff --git a/op-deployer/pkg/deployer/standard/standard.go b/op-deployer/pkg/deployer/standard/standard.go index 1f43b33fe1..a16aac6649 100644 --- a/op-deployer/pkg/deployer/standard/standard.go +++ b/op-deployer/pkg/deployer/standard/standard.go @@ -25,7 +25,7 @@ const ( ChallengePeriodSeconds uint64 = 86400 ProofMaturityDelaySeconds uint64 = 604800 DisputeGameFinalityDelaySeconds uint64 = 302400 - MIPSVersion uint64 = 1 + MIPSVersion uint64 = 2 DisputeGameType uint32 = 1 // PERMISSIONED game type DisputeMaxGameDepth uint64 = 73 DisputeSplitDepth uint64 = 30 @@ -39,13 +39,14 @@ const ( ContractsV180Tag = "op-contracts/v1.8.0-rc.4" ContractsV170Beta1L2Tag = "op-contracts/v1.7.0-beta.1+l2-contracts" ContractsV200Tag = "op-contracts/v2.0.0-rc.1" + ContractsV300Tag = "op-contracts/v3.0.0-rc.2" ) var DisputeAbsolutePrestate = common.HexToHash("0x038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c") -var DefaultL1ContractsTag = ContractsV200Tag +var DefaultL1ContractsTag = ContractsV300Tag -var DefaultL2ContractsTag = ContractsV170Beta1L2Tag +var DefaultL2ContractsTag = ContractsV300Tag type TaggedRelease struct { ArtifactsHash common.Hash @@ -73,16 +74,20 @@ var taggedReleases = map[string]TaggedRelease{ ArtifactsHash: common.HexToHash("32e11c96e07b83619f419595facb273368dccfe2439287549e7b436c9b522204"), ContentHash: common.HexToHash("1cec51ed629c0394b8fb17ff2c6fa45c406c30f94ebbd37d4c90ede6c29ad608"), }, + ContractsV300Tag: { + ArtifactsHash: common.HexToHash("40661d078e6efe7106b95d6fc5c4fda8db144487d85a47abd246cb3afcb41ab2"), + ContentHash: common.HexToHash("147b9fae70608da2975a01be3d98948306f89ba1930af7c917eea41a54d87cdb"), + }, } var _ embed.FS func IsSupportedL1Version(tag string) bool { - return tag == ContractsV200Tag + return tag == ContractsV300Tag } func IsSupportedL2Version(tag string) bool { - return tag == ContractsV170Beta1L2Tag + return tag == ContractsV300Tag } func L1VersionsFor(chainID uint64) (validation.Versions, error) { @@ -166,6 +171,17 @@ func L1ProxyAdminOwner(chainID uint64) (common.Address, error) { } } +func L2ProxyAdminOwner(chainID uint64) (common.Address, error) { + switch chainID { + case 1: + return common.Address(validation.StandardConfigRolesMainnet.L2ProxyAdminOwner), nil + case 11155111: + return common.Address(validation.StandardConfigRolesSepolia.L2ProxyAdminOwner), nil + default: + return common.Address{}, fmt.Errorf("unsupported chain ID: %d", chainID) + } +} + func ProtocolVersionsOwner(chainID uint64) (common.Address, error) { switch chainID { case 1: @@ -211,8 +227,11 @@ func DefaultHardforkScheduleForTag(tag string) *genesis.UpgradeScheduleDeployCon switch tag { case ContractsV160Tag, ContractsV170Beta1L2Tag: return sched + case ContractsV180Tag, ContractsV200Tag, ContractsV300Tag: + sched.ActivateForkAtGenesis(rollup.Holocene) default: sched.ActivateForkAtGenesis(rollup.Holocene) + sched.ActivateForkAtGenesis(rollup.Isthmus) } return sched diff --git a/op-deployer/pkg/deployer/standard/standard_test.go b/op-deployer/pkg/deployer/standard/standard_test.go index a35d77e7f1..83d2e8e956 100644 --- a/op-deployer/pkg/deployer/standard/standard_test.go +++ b/op-deployer/pkg/deployer/standard/standard_test.go @@ -6,6 +6,8 @@ import ( "strings" "testing" + "github.com/ethereum-optimism/superchain-registry/validation" + "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" @@ -14,9 +16,23 @@ import ( func TestDefaultHardforkScheduleForTag(t *testing.T) { sched := DefaultHardforkScheduleForTag(ContractsV160Tag) require.Nil(t, sched.HoloceneTime(0)) + require.Nil(t, sched.IsthmusTime(0)) sched = DefaultHardforkScheduleForTag(ContractsV180Tag) require.NotNil(t, sched.HoloceneTime(0)) + require.Nil(t, sched.IsthmusTime(0)) + + sched = DefaultHardforkScheduleForTag(ContractsV200Tag) + require.NotNil(t, sched.HoloceneTime(0)) + require.Nil(t, sched.IsthmusTime(0)) + + sched = DefaultHardforkScheduleForTag(ContractsV300Tag) + require.NotNil(t, sched.HoloceneTime(0)) + require.Nil(t, sched.IsthmusTime(0)) + + sched = DefaultHardforkScheduleForTag("") + require.NotNil(t, sched.HoloceneTime(0)) + require.NotNil(t, sched.IsthmusTime(0)) } func TestStandardAddresses(t *testing.T) { @@ -62,3 +78,24 @@ func TestStandardAddresses(t *testing.T) { }) } } + +func TestL2ProxyAdminOwner(t *testing.T) { + tests := []struct { + chainID uint64 + expAddr validation.Address + }{ + { + 1, + validation.StandardConfigRolesMainnet.L2ProxyAdminOwner, + }, + { + 11155111, + validation.StandardConfigRolesSepolia.L2ProxyAdminOwner, + }, + } + for _, test := range tests { + addr, err := L2ProxyAdminOwner(test.chainID) + require.NoError(t, err) + require.Equal(t, common.Address(test.expAddr), addr) + } +} diff --git a/op-deployer/pkg/deployer/state/deploy_config.go b/op-deployer/pkg/deployer/state/deploy_config.go index 50b2f12841..02bfe941dd 100644 --- a/op-deployer/pkg/deployer/state/deploy_config.go +++ b/op-deployer/pkg/deployer/state/deploy_config.go @@ -124,7 +124,7 @@ func CombineDeployConfig(intent *Intent, chainIntent *ChainIntent, state *State, BlockNumber: &num, } } else { - startHash := chainState.StartBlock.Hash() + startHash := chainState.StartBlock.Hash cfg.L1StartingBlockTag = &genesis.MarshalableRPCBlockNumberOrHash{ BlockHash: &startHash, } diff --git a/op-deployer/pkg/deployer/state/intent.go b/op-deployer/pkg/deployer/state/intent.go index e142b59ec3..edd4cbfd5c 100644 --- a/op-deployer/pkg/deployer/state/intent.go +++ b/op-deployer/pkg/deployer/state/intent.go @@ -121,7 +121,7 @@ func (c *Intent) validateStandardValues() error { return err } - standardSuperchainRoles, err := getStandardSuperchainRoles(c.L1ChainID) + standardSuperchainRoles, err := GetStandardSuperchainRoles(c.L1ChainID) if err != nil { return fmt.Errorf("error getting standard superchain roles: %w", err) } @@ -157,7 +157,7 @@ func (c *Intent) validateStandardValues() error { return nil } -func getStandardSuperchainRoles(l1ChainId uint64) (*SuperchainRoles, error) { +func GetStandardSuperchainRoles(l1ChainId uint64) (*SuperchainRoles, error) { proxyAdminOwner, err := standard.L1ProxyAdminOwner(l1ChainId) if err != nil { return nil, fmt.Errorf("error getting L1ProxyAdminOwner: %w", err) @@ -286,14 +286,24 @@ func NewIntentStandard(l1ChainId uint64, l2ChainIds []common.Hash) (Intent, erro L2ContractsLocator: artifacts.DefaultL2ContractsLocator, } - superchainRoles, err := getStandardSuperchainRoles(l1ChainId) + superchainRoles, err := GetStandardSuperchainRoles(l1ChainId) if err != nil { return Intent{}, fmt.Errorf("error getting standard superchain roles: %w", err) } intent.SuperchainRoles = superchainRoles - challenger, _ := standard.ChallengerAddressFor(l1ChainId) - l1ProxyAdminOwner, _ := standard.L1ProxyAdminOwner(l1ChainId) + challenger, err := standard.ChallengerAddressFor(l1ChainId) + if err != nil { + return Intent{}, fmt.Errorf("error getting challenger address: %w", err) + } + l1ProxyAdminOwner, err := standard.L1ProxyAdminOwner(l1ChainId) + if err != nil { + return Intent{}, fmt.Errorf("error getting L1ProxyAdminOwner: %w", err) + } + l2ProxyAdminOwner, err := standard.L2ProxyAdminOwner(l1ChainId) + if err != nil { + return Intent{}, fmt.Errorf("error getting L2ProxyAdminOwner: %w", err) + } for _, l2ChainID := range l2ChainIds { intent.Chains = append(intent.Chains, &ChainIntent{ @@ -304,6 +314,7 @@ func NewIntentStandard(l1ChainId uint64, l2ChainIds []common.Hash) (Intent, erro Roles: ChainRoles{ Challenger: challenger, L1ProxyAdminOwner: l1ProxyAdminOwner, + L2ProxyAdminOwner: l2ProxyAdminOwner, }, }) } diff --git a/op-deployer/pkg/deployer/state/state.go b/op-deployer/pkg/deployer/state/state.go index 0ec6c923f3..e5f8d56dc2 100644 --- a/op-deployer/pkg/deployer/state/state.go +++ b/op-deployer/pkg/deployer/state/state.go @@ -5,9 +5,8 @@ import ( "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/broadcaster" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum-optimism/optimism/op-chain-ops/foundry" + "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/ioutil" "github.com/ethereum-optimism/optimism/op-service/jsonutil" "github.com/ethereum/go-ethereum/common" @@ -76,6 +75,7 @@ type ImplementationsDeployment struct { OpcmUpgraderAddress common.Address `json:"opcmUpgraderAddress"` DelayedWETHImplAddress common.Address `json:"delayedWETHImplAddress"` OptimismPortalImplAddress common.Address `json:"optimismPortalImplAddress"` + ETHLockboxImplAddress common.Address `json:"ethLockboxImplAddress"` PreimageOracleSingletonAddress common.Address `json:"preimageOracleSingletonAddress"` MipsSingletonAddress common.Address `json:"mipsSingletonAddress"` SystemConfigImplAddress common.Address `json:"systemConfigImplAddress"` @@ -106,6 +106,7 @@ type ChainState struct { L1StandardBridgeProxyAddress common.Address `json:"l1StandardBridgeProxyAddress"` L1CrossDomainMessengerProxyAddress common.Address `json:"l1CrossDomainMessengerProxyAddress"` OptimismPortalProxyAddress common.Address `json:"optimismPortalProxyAddress"` + ETHLockboxProxyAddress common.Address `json:"ethLockboxProxyAddress"` DisputeGameFactoryProxyAddress common.Address `json:"disputeGameFactoryProxyAddress"` AnchorStateRegistryProxyAddress common.Address `json:"anchorStateRegistryProxyAddress"` FaultDisputeGameAddress common.Address `json:"faultDisputeGameAddress"` @@ -118,5 +119,5 @@ type ChainState struct { Allocs *GzipData[foundry.ForgeAllocs] `json:"allocs"` - StartBlock *types.Header `json:"startBlock"` + StartBlock *eth.BlockRef `json:"startBlock"` } diff --git a/op-deployer/pkg/deployer/upgrade/flags.go b/op-deployer/pkg/deployer/upgrade/flags.go index 51f15aa88b..9be34a23f1 100644 --- a/op-deployer/pkg/deployer/upgrade/flags.go +++ b/op-deployer/pkg/deployer/upgrade/flags.go @@ -3,6 +3,7 @@ package upgrade import ( "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer" v200 "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/upgrade/v2_0_0" + v300 "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/upgrade/v3_0_0" oplog "github.com/ethereum-optimism/optimism/op-service/log" "github.com/urfave/cli/v2" ) @@ -24,11 +25,19 @@ var Commands = cli.Commands{ Usage: "upgrades a chain to version v2.0.0", Flags: append([]cli.Flag{ deployer.L1RPCURLFlag, - deployer.DeploymentTargetFlag, - deployer.PrivateKeyFlag, ConfigFlag, OverrideArtifactsURLFlag, }, oplog.CLIFlags(deployer.EnvVarPrefix)...), Action: UpgradeCLI(v200.DefaultUpgrader), }, + &cli.Command{ + Name: "v3.0.0", + Usage: "upgrades a chain to version v3.0.0", + Flags: append([]cli.Flag{ + deployer.L1RPCURLFlag, + ConfigFlag, + OverrideArtifactsURLFlag, + }, oplog.CLIFlags(deployer.EnvVarPrefix)...), + Action: UpgradeCLI(v300.DefaultUpgrader), + }, } diff --git a/op-deployer/pkg/deployer/upgrade/upgrader.go b/op-deployer/pkg/deployer/upgrade/upgrader.go index 89c33202f8..2bd6edd2a8 100644 --- a/op-deployer/pkg/deployer/upgrade/upgrader.go +++ b/op-deployer/pkg/deployer/upgrade/upgrader.go @@ -11,18 +11,14 @@ import ( "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/artifacts" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/broadcaster" "github.com/ethereum-optimism/optimism/op-deployer/pkg/env" - opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto" oplog "github.com/ethereum-optimism/optimism/op-service/log" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/rpc" "github.com/urfave/cli/v2" ) type Upgrader interface { Upgrade(host *script.Host, input json.RawMessage) error - SupportsVersion(version string) bool ArtifactsURL() string } @@ -39,10 +35,6 @@ func UpgradeCLI(upgrader Upgrader) func(*cli.Context) error { if l1RPC == "" { return fmt.Errorf("missing required flag: %s", deployer.L1RPCURLFlag.Name) } - deploymentTarget, err := deployer.NewDeploymentTarget(cliCtx.String(deployer.DeploymentTargetFlag.Name)) - if err != nil { - return fmt.Errorf("failed to parse deployment target: %w", err) - } artifactsURL := upgrader.ArtifactsURL() overrideArtifactsURL := cliCtx.String(OverrideArtifactsURLFlag.Name) @@ -58,49 +50,9 @@ func UpgradeCLI(upgrader Upgrader) func(*cli.Context) error { if err != nil { return fmt.Errorf("failed to dial RPC %s: %w", l1RPC, err) } - ethClient := ethclient.NewClient(rpcClient) - - chainID, err := ethClient.ChainID(ctx) - if err != nil { - return fmt.Errorf("failed to get chain ID: %w", err) - } - var bcaster broadcaster.Broadcaster + bcaster := new(broadcaster.CalldataBroadcaster) depAddr := common.Address{'D'} - switch deploymentTarget { - case deployer.DeploymentTargetLive: - privateKeyHex := cliCtx.String(deployer.PrivateKeyFlag.Name) - if privateKeyHex == "" { - return fmt.Errorf("%s flag is required for live deployment", deployer.PrivateKeyFlag.Name) - } - - pk, err := crypto.HexToECDSA(privateKeyHex) - if err != nil { - return fmt.Errorf("failed to parse private key: %w", err) - } - - depAddr = crypto.PubkeyToAddress(pk.PublicKey) - - bcaster, err = broadcaster.NewKeyedBroadcaster(broadcaster.KeyedBroadcasterOpts{ - Logger: lgr, - ChainID: chainID, - Client: ethClient, - Signer: opcrypto.SignerFnFromBind(opcrypto.PrivateKeySignerFn(pk, chainID)), - From: depAddr, - }) - if err != nil { - return fmt.Errorf("failed to create broadcaster: %w", err) - } - case deployer.DeploymentTargetCalldata: - bcaster = new(broadcaster.CalldataBroadcaster) - case deployer.DeploymentTargetNoop: - bcaster = broadcaster.NoopBroadcaster() - case deployer.DeploymentTargetGenesis: - return fmt.Errorf("cannot upgrade into a genesis deployment") - default: - return fmt.Errorf("unknown deployment target: %s", deploymentTarget) - } - cacheDir := cliCtx.String(deployer.CacheDirFlag.Name) artifactsFS, err := artifacts.Download(ctx, artifactsLocator, artifacts.BarProgressor(), cacheDir) @@ -132,21 +84,15 @@ func UpgradeCLI(upgrader Upgrader) func(*cli.Context) error { return fmt.Errorf("failed to upgrade: %w", err) } - if deploymentTarget == deployer.DeploymentTargetCalldata { - dump, err := bcaster.(*broadcaster.CalldataBroadcaster).Dump() - if err != nil { - return fmt.Errorf("failed to dump calldata: %w", err) - } + dump, err := bcaster.Dump() + if err != nil { + return fmt.Errorf("failed to dump calldata: %w", err) + } - enc := json.NewEncoder(os.Stdout) - enc.SetIndent("", " ") - if err := enc.Encode(dump); err != nil { - return fmt.Errorf("failed to encode calldata: %w", err) - } - } else if deploymentTarget == deployer.DeploymentTargetLive { - if _, err := bcaster.Broadcast(ctx); err != nil { - return fmt.Errorf("failed to broadcast: %w", err) - } + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + if err := enc.Encode(dump); err != nil { + return fmt.Errorf("failed to encode calldata: %w", err) } return nil diff --git a/op-deployer/pkg/deployer/upgrade/v2_0_0/upgrade.go b/op-deployer/pkg/deployer/upgrade/v2_0_0/upgrade.go index a5926899d0..d59326f1e4 100644 --- a/op-deployer/pkg/deployer/upgrade/v2_0_0/upgrade.go +++ b/op-deployer/pkg/deployer/upgrade/v2_0_0/upgrade.go @@ -52,10 +52,6 @@ func (u *Upgrader) Upgrade(host *script.Host, input json.RawMessage) error { return Upgrade(host, upgradeInput) } -func (u *Upgrader) SupportsVersion(version string) bool { - return version == "2.0.0" -} - func (u *Upgrader) ArtifactsURL() string { return "tag://" + standard.ContractsV200Tag } diff --git a/op-deployer/pkg/deployer/upgrade/v3_0_0/upgrade.go b/op-deployer/pkg/deployer/upgrade/v3_0_0/upgrade.go new file mode 100644 index 0000000000..f99866bcfb --- /dev/null +++ b/op-deployer/pkg/deployer/upgrade/v3_0_0/upgrade.go @@ -0,0 +1,25 @@ +// Package v3_0_0 implements the upgrade to v3.0.0 (U14). The interface for the upgrade is identical +// to the upgrade for v2.0.0 (U13), so all this package does is implement the Upgrader interface and +// call into the v2.0.0 upgrade. +package v3_0_0 + +import ( + "encoding/json" + + "github.com/ethereum-optimism/optimism/op-chain-ops/script" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard" + v200 "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/upgrade/v2_0_0" +) + +type Upgrader struct { +} + +func (u *Upgrader) Upgrade(host *script.Host, input json.RawMessage) error { + return v200.DefaultUpgrader.Upgrade(host, input) +} + +func (u *Upgrader) ArtifactsURL() string { + return "tag://" + standard.ContractsV300Tag +} + +var DefaultUpgrader = new(Upgrader) diff --git a/op-deployer/pkg/deployer/verify/artifacts.go b/op-deployer/pkg/deployer/verify/artifacts.go new file mode 100644 index 0000000000..7e84f2025b --- /dev/null +++ b/op-deployer/pkg/deployer/verify/artifacts.go @@ -0,0 +1,98 @@ +package verify + +import ( + "encoding/json" + "fmt" + "path" + "strings" + + "github.com/ethereum-optimism/optimism/op-chain-ops/foundry" + "github.com/ethereum/go-ethereum/accounts/abi" +) + +type contractArtifact struct { + ContractName string + CompilerVersion string + Optimizer OptimizerSettings + EVMVersion string + Sources map[string]SourceContent + ConstructorArgs abi.Arguments +} + +// Map state.json struct fields to forge artifact paths +var contractNameExceptions = map[string]string{ + "OptimismPortalImpl": "OptimismPortal2.sol/OptimismPortal2.json", + "L1StandardBridgeProxy": "L1ChugSplashProxy.sol/L1ChugSplashProxy.json", + "L1CrossDomainMessengerProxy": "ResolvedDelegateProxy.sol/ResolvedDelegateProxy.json", + "Opcm": "OPContractsManager.sol/OPContractsManager.json", + "OpcmContractsContainer": "OPContractsManager.sol/OPContractsManagerContractsContainer.json", + "OpcmGameTypeAdder": "OPContractsManager.sol/OPContractsManagerGameTypeAdder.json", + "OpcmDeployer": "OPContractsManager.sol/OPContractsManagerDeployer.json", + "OpcmUpgrader": "OPContractsManager.sol/OPContractsManagerUpgrader.json", +} + +func getArtifactPath(name string) string { + lookupName := strings.TrimSuffix(name, "Address") + lookupName = strings.ToUpper(string(lookupName[0])) + lookupName[1:] + + if artifactPath, exists := contractNameExceptions[lookupName]; exists { + return artifactPath + } + + lookupName = strings.TrimSuffix(lookupName, "Proxy") + lookupName = strings.TrimSuffix(lookupName, "Impl") + lookupName = strings.TrimSuffix(lookupName, "Singleton") + + // If it was a proxy and not a special case, return "Proxy" + if strings.HasSuffix(name, "ProxyAddress") { + return path.Join("Proxy.sol", "Proxy.json") + } + + return path.Join(lookupName+".sol", lookupName+".json") +} + +func (v *Verifier) getContractArtifact(name string) (*contractArtifact, error) { + artifactPath := getArtifactPath(name) + + v.log.Info("Opening artifact", "path", artifactPath, "name", name) + f, err := v.artifactsFS.Open(artifactPath) + if err != nil { + return nil, fmt.Errorf("failed to open artifact: %w", err) + } + defer f.Close() + + var art foundry.Artifact + if err := json.NewDecoder(f).Decode(&art); err != nil { + return nil, fmt.Errorf("failed to decode artifact: %w", err) + } + + // Add all sources (main contract and dependencies) + sources := make(map[string]SourceContent) + for sourcePath, sourceInfo := range art.Metadata.Sources { + remappedKey := art.SearchRemappings(sourcePath) + sources[remappedKey] = SourceContent{Content: sourceInfo.Content} + v.log.Debug("added source contract", "originalPath", sourcePath, "remappedKey", remappedKey) + } + + var optimizer OptimizerSettings + if err := json.Unmarshal(art.Metadata.Settings.Optimizer, &optimizer); err != nil { + return nil, fmt.Errorf("failed to parse optimizer settings: %w", err) + } + + // Get the contract name from the compilation target + var contractName string + for contractFile, name := range art.Metadata.Settings.CompilationTarget { + contractName = contractFile + ":" + name + break + } + v.log.Info("Compilation target", "target", contractName) + + return &contractArtifact{ + ContractName: contractName, + CompilerVersion: art.Metadata.Compiler.Version, + Optimizer: optimizer, + EVMVersion: art.Metadata.Settings.EVMVersion, + Sources: sources, + ConstructorArgs: art.ABI.Constructor.Inputs, + }, nil +} diff --git a/op-deployer/pkg/deployer/verify/constructors.go b/op-deployer/pkg/deployer/verify/constructors.go new file mode 100644 index 0000000000..e355adea4d --- /dev/null +++ b/op-deployer/pkg/deployer/verify/constructors.go @@ -0,0 +1,61 @@ +package verify + +import ( + "context" + "encoding/hex" + "fmt" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" +) + +func (v *Verifier) getConstructorArgs(ctx context.Context, address common.Address, artifact *contractArtifact) (string, error) { + argSlots := 0 + for _, arg := range artifact.ConstructorArgs { + argSlots += calculateTypeSlots(arg.Type) + } + if argSlots == 0 { + return "", nil + } + + v.log.Info("Extracting constructor args from initcode", "address", address.Hex(), "argSlots", argSlots) + txHash, err := v.etherscan.getContractCreation(address) + if err != nil { + return "", fmt.Errorf("failed to get contract creation tx: %w", err) + } + v.log.Info("Contract creation tx hash", "txHash", txHash.Hex()) + + tx, isPending, err := v.l1Client.TransactionByHash(ctx, txHash) + if err != nil { + return "", fmt.Errorf("failed to get transaction: %w", err) + } + + if isPending { + return "", fmt.Errorf("transaction is still pending") + } + + // tx.Data contains bytecode + constructor args, so we strip the + // constructor args off of the end + txInput := hex.EncodeToString(tx.Data()) + constructorArgs := txInput[len(txInput)-(argSlots*64):] + v.log.Info("Successfully extracted constructor args", "address", address.Hex()) + + return constructorArgs, nil +} + +// Helper function to calculate slots needed for a abi.Type, handling nested tuples +func calculateTypeSlots(t abi.Type) int { + if t.String() == "string" { + return 3 // 1 slot each for: offset, length, value (assuming value only takes 1 slot) + } else if strings.HasPrefix(t.String(), "(") { + // loop through nested tuple elements + totalSlots := 0 + for _, elem := range t.TupleElems { + totalSlots += calculateTypeSlots(*elem) + } + return totalSlots + } else { + return 1 + } +} diff --git a/op-deployer/pkg/deployer/verify/constructors_test.go b/op-deployer/pkg/deployer/verify/constructors_test.go new file mode 100644 index 0000000000..20a3dc0c2e --- /dev/null +++ b/op-deployer/pkg/deployer/verify/constructors_test.go @@ -0,0 +1,171 @@ +package verify + +import ( + "encoding/json" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/stretchr/testify/require" +) + +func TestCalculateTypeSlots(t *testing.T) { + t.Run("nested tuple", func(t *testing.T) { + constructorArgsJSON := `[ + { + "name": "_superchainConfig", + "type": "address", + "internalType": "contract ISuperchainConfig" + }, + { + "name": "_protocolVersions", + "type": "address", + "internalType": "contract IProtocolVersions" + }, + { + "name": "_superchainProxyAdmin", + "type": "address", + "internalType": "contract IProxyAdmin" + }, + { + "name": "_l1ContractsRelease", + "type": "string", + "internalType": "string" + }, + { + "name": "_blueprints", + "type": "tuple", + "internalType": "struct OPContractsManager.Blueprints", + "components": [ + { + "name": "addressManager", + "type": "address", + "internalType": "address" + }, + { + "name": "proxy", + "type": "address", + "internalType": "address" + }, + { + "name": "proxyAdmin", + "type": "address", + "internalType": "address" + }, + { + "name": "l1ChugSplashProxy", + "type": "address", + "internalType": "address" + }, + { + "name": "resolvedDelegateProxy", + "type": "address", + "internalType": "address" + }, + { + "name": "permissionedDisputeGame1", + "type": "address", + "internalType": "address" + }, + { + "name": "permissionedDisputeGame2", + "type": "address", + "internalType": "address" + }, + { + "name": "permissionlessDisputeGame1", + "type": "address", + "internalType": "address" + }, + { + "name": "permissionlessDisputeGame2", + "type": "address", + "internalType": "address" + } + ] + }, + { + "name": "_implementations", + "type": "tuple", + "internalType": "struct OPContractsManager.Implementations", + "components": [ + { + "name": "superchainConfigImpl", + "type": "address", + "internalType": "address" + }, + { + "name": "protocolVersionsImpl", + "type": "address", + "internalType": "address" + }, + { + "name": "l1ERC721BridgeImpl", + "type": "address", + "internalType": "address" + }, + { + "name": "optimismPortalImpl", + "type": "address", + "internalType": "address" + }, + { + "name": "systemConfigImpl", + "type": "address", + "internalType": "address" + }, + { + "name": "optimismMintableERC20FactoryImpl", + "type": "address", + "internalType": "address" + }, + { + "name": "l1CrossDomainMessengerImpl", + "type": "address", + "internalType": "address" + }, + { + "name": "l1StandardBridgeImpl", + "type": "address", + "internalType": "address" + }, + { + "name": "disputeGameFactoryImpl", + "type": "address", + "internalType": "address" + }, + { + "name": "anchorStateRegistryImpl", + "type": "address", + "internalType": "address" + }, + { + "name": "delayedWETHImpl", + "type": "address", + "internalType": "address" + }, + { + "name": "mipsImpl", + "type": "address", + "internalType": "address" + } + ] + }, + { + "name": "_upgradeController", + "type": "address", + "internalType": "address" + } + ]` + + var args abi.Arguments + err := json.Unmarshal([]byte(constructorArgsJSON), &args) + require.NoError(t, err) + + totalSlots := 0 + for _, arg := range args { + totalSlots += calculateTypeSlots(arg.Type) + } + + require.Equal(t, 28, totalSlots) + }) +} diff --git a/op-deployer/pkg/deployer/verify/etherscan.go b/op-deployer/pkg/deployer/verify/etherscan.go new file mode 100644 index 0000000000..15578a413a --- /dev/null +++ b/op-deployer/pkg/deployer/verify/etherscan.go @@ -0,0 +1,262 @@ +package verify + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" + "time" + + "github.com/ethereum/go-ethereum/common" + "golang.org/x/time/rate" +) + +type EtherscanGenericResp struct { + Status string `json:"status"` + Message string `json:"message"` + Result string `json:"result"` +} + +type EtherscanContractCreationResp struct { + Status string `json:"status"` + Message string `json:"message"` + Result []struct { + ContractCreator string `json:"contractCreator"` + TxHash string `json:"txHash"` + } `json:"result"` +} + +type EtherscanClient struct { + apiKey string + url string + rateLimiter *rate.Limiter +} + +func getAPIEndpoint(l1ChainID uint64) (string, error) { + switch l1ChainID { + case 1: + return "https://api.etherscan.io/api", nil // mainnet + case 11155111: + return "https://api-sepolia.etherscan.io/api", nil // sepolia + default: + return "", fmt.Errorf("unsupported L1 chain ID: %d", l1ChainID) + } +} + +func NewEtherscanClient(apiKey string, url string, rateLimiter *rate.Limiter) *EtherscanClient { + return &EtherscanClient{ + apiKey: apiKey, + url: url, + rateLimiter: rateLimiter, + } +} + +// sendRateLimitedRequest is a helper function which waits for a rate limit token +// before sending a request +func (c *EtherscanClient) sendRateLimitedRequest(req *http.Request) (*http.Response, error) { + if err := c.rateLimiter.Wait(context.Background()); err != nil { + return nil, fmt.Errorf("rate limiter error: %w", err) + } + return http.DefaultClient.Do(req) +} + +// getContractCreation returns the txHash of the contract creation tx +// (useful for extracting constructor args) +func (c *EtherscanClient) getContractCreation(address common.Address) (common.Hash, error) { + req, err := http.NewRequest("GET", fmt.Sprintf("%s?module=contract&action=getcontractcreation&contractaddresses=%s&apikey=%s", + c.url, address.Hex(), c.apiKey), nil) + if err != nil { + return common.Hash{}, fmt.Errorf("failed to create contract creation request: %w", err) + } + + resp, err := c.sendRateLimitedRequest(req) + if err != nil { + return common.Hash{}, fmt.Errorf("failed to send contract creation request: %w", err) + } + defer resp.Body.Close() + + var creationResp EtherscanContractCreationResp + if err := json.NewDecoder(resp.Body).Decode(&creationResp); err != nil { + return common.Hash{}, fmt.Errorf("failed to decode contract creation response: %w", err) + } + if creationResp.Status != "1" { + return common.Hash{}, fmt.Errorf("contract creation query failed: %s", creationResp.Message) + } + + txHash := common.HexToHash(creationResp.Result[0].TxHash) + return txHash, nil +} + +func (c *EtherscanClient) verifySourceCode(address common.Address, artifact *contractArtifact, constructorArgs string) (string, error) { + optimized := "0" + if artifact.Optimizer.Enabled { + optimized = "1" + } + + standardInput := newStandardInput(artifact) + standardInputJSON, err := json.Marshal(standardInput) + if err != nil { + return "", fmt.Errorf("failed to generate standard input: %w", err) + } + + data := url.Values{ + "apikey": {c.apiKey}, + "module": {"contract"}, + "action": {"verifysourcecode"}, + "contractaddress": {address.Hex()}, + "codeformat": {"solidity-standard-json-input"}, + "sourceCode": {string(standardInputJSON)}, + "contractname": {artifact.ContractName}, + "compilerversion": {fmt.Sprintf("v%s", artifact.CompilerVersion)}, + "optimizationUsed": {optimized}, + "runs": {fmt.Sprintf("%d", artifact.Optimizer.Runs)}, + "evmversion": {artifact.EVMVersion}, + "constructorArguements": {constructorArgs}, + } + + req, err := http.NewRequest("POST", c.url, strings.NewReader(data.Encode())) + if err != nil { + return "", fmt.Errorf("failed to create verification request: %w", err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + resp, err := c.sendRateLimitedRequest(req) + if err != nil { + return "", fmt.Errorf("failed to submit verification request: %w", err) + } + defer resp.Body.Close() + + var result EtherscanGenericResp + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return "", fmt.Errorf("failed to decode response: %w", err) + } + + if result.Status != "1" { + return "", fmt.Errorf("verification request failed: status=%s message=%s result=%s", + result.Status, result.Message, result.Result) + } + + return result.Result, nil +} + +func (c *EtherscanClient) isVerified(address common.Address) (bool, error) { + req, err := http.NewRequest("GET", fmt.Sprintf("%s?module=contract&action=getabi&address=%s&apikey=%s", + c.url, address.Hex(), c.apiKey), nil) + if err != nil { + return false, err + } + + resp, err := c.sendRateLimitedRequest(req) + if err != nil { + return false, err + } + defer resp.Body.Close() + + var result EtherscanGenericResp + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return false, err + } + + return result.Status == "1", nil +} + +func (c *EtherscanClient) pollVerificationStatus(reqId string) error { + req, err := http.NewRequest("GET", fmt.Sprintf("%s?apikey=%s&module=contract&action=checkverifystatus&guid=%s", + c.url, c.apiKey, reqId), nil) + if err != nil { + return fmt.Errorf("failed to create checkverifystatus request: %w", err) + } + + for i := 0; i < 10; i++ { // Try 10 times with increasing delays + resp, err := c.sendRateLimitedRequest(req) + if err != nil { + return fmt.Errorf("failed to send checkverifystatus request: %w", err) + } + defer resp.Body.Close() + + var result EtherscanGenericResp + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return fmt.Errorf("failed to decode checkverifystatus response: %w", err) + } + + if result.Status == "1" { + return nil + } + if result.Result == "Already Verified" { + return nil + } + if result.Result != "Pending in queue" { + return fmt.Errorf("verification failed: %s, %s", result.Result, result.Message) + } + time.Sleep(time.Duration(i+2) * time.Second) + } + return fmt.Errorf("verification timed out") +} + +type StandardInput struct { + Language string `json:"language"` + Sources map[string]SourceContent `json:"sources"` + Settings Settings `json:"settings"` +} + +type SourceContent struct { + Content string `json:"content"` +} + +type Settings struct { + Optimizer OptimizerSettings `json:"optimizer"` + EVMVersion string `json:"evmVersion"` + Metadata MetadataSettings `json:"metadata"` + OutputSelection OutputSelection `json:"outputSelection"` +} + +type OptimizerSettings struct { + Enabled bool `json:"enabled"` + Runs int `json:"runs"` +} + +type MetadataSettings struct { + UseLiteralContent bool `json:"useLiteralContent"` + BytecodeHash string `json:"bytecodeHash"` +} + +type OutputSelection struct { + All map[string]OutputSelectionDetails `json:"*"` +} + +type OutputSelectionDetails struct { + All []string `json:"*"` +} + +func newStandardInput(artifact *contractArtifact) StandardInput { + return StandardInput{ + Language: "Solidity", + Sources: artifact.Sources, + Settings: Settings{ + Optimizer: OptimizerSettings{ + Enabled: artifact.Optimizer.Enabled, + Runs: artifact.Optimizer.Runs, + }, + EVMVersion: artifact.EVMVersion, + Metadata: MetadataSettings{ + UseLiteralContent: true, + BytecodeHash: "none", + }, + OutputSelection: OutputSelection{ + All: map[string]OutputSelectionDetails{ + "*": { + All: []string{ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap", + "metadata", + }, + }, + }, + }, + }, + } +} diff --git a/op-deployer/pkg/deployer/verify/etherscan_test.go b/op-deployer/pkg/deployer/verify/etherscan_test.go new file mode 100644 index 0000000000..8bd0fe16bf --- /dev/null +++ b/op-deployer/pkg/deployer/verify/etherscan_test.go @@ -0,0 +1,278 @@ +package verify + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "golang.org/x/time/rate" +) + +const ( + testAPIKey = "test_api_key" + testAddressHex = "0x1234567890123456789012345678901234567890" + testTxHashHex = "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + testGUID = "verification_guid_12345" +) + +// createTestClient creates a new EtherscanClient with a mock server for testing +func createTestClient(t *testing.T, handler http.HandlerFunc) (*EtherscanClient, *httptest.Server) { + server := httptest.NewServer(handler) + t.Cleanup(func() { server.Close() }) + + // Use a fast rate limiter for testing + limiter := rate.NewLimiter(rate.Every(time.Millisecond), 10) + return NewEtherscanClient(testAPIKey, server.URL, limiter), server +} + +// createTestArtifact creates a contract artifact for testing +func createTestArtifact() *contractArtifact { + return &contractArtifact{ + ContractName: "TestContract", + CompilerVersion: "0.8.10", + EVMVersion: "london", + Optimizer: OptimizerSettings{ + Enabled: true, + Runs: 200, + }, + Sources: map[string]SourceContent{ + "TestContract.sol": {Content: "contract TestContract {}"}, + }, + } +} + +func TestGetAPIEndpoint(t *testing.T) { + tests := []struct { + name string + chainID uint64 + expected string + expectedError bool + }{ + {"Mainnet", 1, "https://api.etherscan.io/api", false}, + {"Sepolia", 11155111, "https://api-sepolia.etherscan.io/api", false}, + {"Unknown", 999, "", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := getAPIEndpoint(tt.chainID) + if tt.expectedError { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.expected, result) + } + }) + } +} + +func TestGetContractCreation(t *testing.T) { + client, _ := createTestClient(t, func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "GET", r.Method) + require.Contains(t, r.URL.String(), "module=contract") + require.Contains(t, r.URL.String(), "action=getcontractcreation") + + testAddr := common.HexToAddress(testAddressHex) + require.Contains(t, r.URL.String(), testAddr.Hex()) + + resp := EtherscanContractCreationResp{ + Status: "1", + Message: "OK", + Result: []struct { + ContractCreator string `json:"contractCreator"` + TxHash string `json:"txHash"` + }{ + { + ContractCreator: "0xabcdef1234567890abcdef1234567890abcdef12", + TxHash: testTxHashHex, + }, + }, + } + + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(resp) + require.NoError(t, err) + }) + + testAddr := common.HexToAddress(testAddressHex) + txHash, err := client.getContractCreation(testAddr) + + require.NoError(t, err) + require.Equal(t, testTxHashHex, txHash.Hex()) +} + +func TestGetContractCreationError(t *testing.T) { + client, _ := createTestClient(t, func(w http.ResponseWriter, r *http.Request) { + resp := EtherscanContractCreationResp{ + Status: "0", + Message: "Error", + Result: nil, + } + + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(resp) + require.NoError(t, err) + }) + + testAddr := common.HexToAddress(testAddressHex) + _, err := client.getContractCreation(testAddr) + + require.Error(t, err) + require.Contains(t, err.Error(), "contract creation query failed") +} + +func TestVerifySourceCode(t *testing.T) { + client, _ := createTestClient(t, func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "POST", r.Method) + require.Equal(t, "application/x-www-form-urlencoded", r.Header.Get("Content-Type")) + + err := r.ParseForm() + require.NoError(t, err) + + require.Equal(t, testAPIKey, r.Form.Get("apikey")) + require.Equal(t, "contract", r.Form.Get("module")) + require.Equal(t, "verifysourcecode", r.Form.Get("action")) + require.Equal(t, testAddressHex, r.Form.Get("contractaddress")) + + resp := EtherscanGenericResp{ + Status: "1", + Message: "OK", + Result: testGUID, + } + + w.Header().Set("Content-Type", "application/json") + err = json.NewEncoder(w).Encode(resp) + require.NoError(t, err) + }) + + testAddr := common.HexToAddress(testAddressHex) + artifact := createTestArtifact() + + guid, err := client.verifySourceCode(testAddr, artifact, "0x1234") + + require.NoError(t, err) + require.Equal(t, testGUID, guid) +} + +func TestIsVerified(t *testing.T) { + tests := []struct { + name string + responseStatus string + expected bool + }{ + {"Verified", "1", true}, + {"Not Verified", "0", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, _ := createTestClient(t, func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "GET", r.Method) + require.Contains(t, r.URL.String(), "module=contract") + require.Contains(t, r.URL.String(), "action=getabi") + + resp := EtherscanGenericResp{ + Status: tt.responseStatus, + Message: "OK", + Result: "test result", + } + + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(resp) + require.NoError(t, err) + }) + + testAddr := common.HexToAddress(testAddressHex) + result, err := client.isVerified(testAddr) + + require.NoError(t, err) + require.Equal(t, tt.expected, result) + }) + } +} + +func TestPollVerificationStatus(t *testing.T) { + tests := []struct { + name string + responses []EtherscanGenericResp + expectError bool + errorMessage string + }{ + { + name: "Success After Pending", + responses: []EtherscanGenericResp{ + {Status: "0", Message: "OK", Result: "Pending in queue"}, + {Status: "1", Message: "OK", Result: "Pass - Verified"}, + }, + expectError: false, + }, + { + name: "Already Verified", + responses: []EtherscanGenericResp{ + {Status: "0", Message: "OK", Result: "Already Verified"}, + }, + expectError: false, + }, + { + name: "Verification Failed", + responses: []EtherscanGenericResp{ + {Status: "0", Message: "OK", Result: "Fail - Unable to verify"}, + }, + expectError: true, + errorMessage: "verification failed", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + responseIndex := 0 + + client, _ := createTestClient(t, func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "GET", r.Method) + require.Contains(t, r.URL.String(), "module=contract") + require.Contains(t, r.URL.String(), "action=checkverifystatus") + + // Get the appropriate response based on the current index + resp := tt.responses[responseIndex] + if responseIndex < len(tt.responses)-1 { + responseIndex++ + } + + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(resp) + require.NoError(t, err) + }) + + err := client.pollVerificationStatus("test_guid") + + if tt.expectError { + require.Error(t, err) + require.Contains(t, err.Error(), tt.errorMessage) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestNewStandardInput(t *testing.T) { + artifact := createTestArtifact() + + result := newStandardInput(artifact) + + require.Equal(t, "Solidity", result.Language) + require.Equal(t, artifact.Sources, result.Sources) + require.Equal(t, artifact.Optimizer.Enabled, result.Settings.Optimizer.Enabled) + require.Equal(t, artifact.Optimizer.Runs, result.Settings.Optimizer.Runs) + require.Equal(t, artifact.EVMVersion, result.Settings.EVMVersion) + require.True(t, result.Settings.Metadata.UseLiteralContent) + require.Equal(t, "none", result.Settings.Metadata.BytecodeHash) + + require.Contains(t, result.Settings.OutputSelection.All["*"].All, "abi") + require.Contains(t, result.Settings.OutputSelection.All["*"].All, "evm.bytecode.object") + require.Contains(t, result.Settings.OutputSelection.All["*"].All, "metadata") +} diff --git a/op-deployer/pkg/deployer/verify/verifier.go b/op-deployer/pkg/deployer/verify/verifier.go new file mode 100644 index 0000000000..4779d924d5 --- /dev/null +++ b/op-deployer/pkg/deployer/verify/verifier.go @@ -0,0 +1,194 @@ +package verify + +import ( + "context" + "encoding/json" + "fmt" + "os" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/log" + "github.com/urfave/cli/v2" + "golang.org/x/time/rate" + + "github.com/ethereum-optimism/optimism/op-chain-ops/foundry" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/artifacts" + "github.com/ethereum-optimism/optimism/op-service/ctxinterrupt" + oplog "github.com/ethereum-optimism/optimism/op-service/log" +) + +type Verifier struct { + l1ChainID uint64 + artifactsFS foundry.StatDirFs + log log.Logger + etherscan *EtherscanClient + l1Client *ethclient.Client + numVerified int + numSkipped int + numFailed int +} + +func NewVerifier(apiKey string, l1ChainID uint64, artifactsFS foundry.StatDirFs, l log.Logger, l1Client *ethclient.Client) (*Verifier, error) { + etherscanUrl, err := getAPIEndpoint(l1ChainID) + if err != nil { + return nil, fmt.Errorf("unsupported L1 chain ID: %d", l1ChainID) + } + + etherscan := NewEtherscanClient(apiKey, etherscanUrl, rate.NewLimiter(rate.Limit(3), 2)) + + return &Verifier{ + l1ChainID: l1ChainID, + artifactsFS: artifactsFS, + log: l, + l1Client: l1Client, + etherscan: etherscan, + }, nil +} + +func VerifyCLI(cliCtx *cli.Context) error { + logCfg := oplog.ReadCLIConfig(cliCtx) + l := oplog.NewLogger(oplog.AppOut(cliCtx), logCfg) + oplog.SetGlobalLogHandler(l.Handler()) + + l1RPCUrl := cliCtx.String(deployer.L1RPCURLFlagName) + etherscanAPIKey := cliCtx.String(deployer.EtherscanAPIKeyFlagName) + if etherscanAPIKey == "" { + return fmt.Errorf("etherscan-api-key is required") + } + + inputFile := cliCtx.String(deployer.InputFileFlagName) + if inputFile == "" { + return fmt.Errorf("input-file is required") + } + contractName := cliCtx.String(deployer.ContractNameFlagName) + + l1ContractsLocator := cliCtx.String(deployer.ArtifactsLocatorFlagName) + if l1ContractsLocator == "" { + return fmt.Errorf("artifacts-locator is required") + } + + ctx := ctxinterrupt.WithCancelOnInterrupt(cliCtx.Context) + + l1Client, err := ethclient.Dial(l1RPCUrl) + if err != nil { + return fmt.Errorf("failed to connect to L1: %w", err) + } + defer l1Client.Close() + + chainId, err := l1Client.ChainID(ctx) + if err != nil { + return fmt.Errorf("failed to get chain ID: %w", err) + } + l1ChainId := chainId.Uint64() + + locator, err := artifacts.NewLocatorFromURL(l1ContractsLocator) + if err != nil { + return fmt.Errorf("failed to parse l1 contracts release locator: %w", err) + } + artifactsFS, err := artifacts.Download(ctx, locator, nil, deployer.GetDefaultCacheDir()) + if err != nil { + return fmt.Errorf("failed to get artifacts: %w", err) + } + l.Info("Downloaded artifacts", "path", artifactsFS) + + v, err := NewVerifier(etherscanAPIKey, l1ChainId, artifactsFS, l, l1Client) + if err != nil { + return fmt.Errorf("failed to create verifier: %w", err) + } + + defer func() { + v.log.Info("final results", "numVerified", v.numVerified, "numSkipped", v.numSkipped, "numFailed", v.numFailed) + }() + + if err := v.verifyContractBundle(ctx, inputFile, contractName); err != nil { + return err + } + v.log.Info("--- COMPLETE ---") + return nil +} + +func (v *Verifier) getContractBundle(filepath string) (map[string]common.Address, error) { + _, err := os.Stat(filepath) + if err != nil { + return nil, fmt.Errorf("input file not found: %s", filepath) + } + + bundleData, err := os.ReadFile(filepath) + if err != nil { + return nil, fmt.Errorf("failed to read bundle file %s: %w", filepath, err) + } + + var bundle map[string]common.Address + if err := json.Unmarshal(bundleData, &bundle); err != nil { + return nil, fmt.Errorf("failed to parse superchain bundle: %w", err) + } + + return bundle, nil +} + +func (v *Verifier) verifyContractBundle(ctx context.Context, filepath string, contractName string) error { + bundle, err := v.getContractBundle(filepath) + if err != nil { + return fmt.Errorf("failed to retrieve bundle: %w", err) + } + + if contractName != "" { + addr, ok := bundle[contractName] + if !ok { + return fmt.Errorf("contract %s not found in bundle", contractName) + } + if err := v.verifySingleContract(ctx, addr, contractName); err != nil { + return fmt.Errorf("failed to verify contract %s: %w", contractName, err) + } + return nil + } + + for contractName, addr := range bundle { + if addr != (common.Address{}) { // skip zero addresses + if err := v.verifySingleContract(ctx, addr, contractName); err != nil { + v.numFailed++ + v.log.Error("failed to verify contract", "name", contractName, "error", err) + } + } + } + return nil +} + +func (v *Verifier) verifySingleContract(ctx context.Context, address common.Address, contractName string) error { + verified, err := v.etherscan.isVerified(address) + if err != nil { + return fmt.Errorf("failed to check verification status: %w", err) + } + if verified { + v.log.Info("Contract is already verified", "name", contractName, "address", address.Hex()) + v.numSkipped++ + return nil + } + + v.log.Info("Formatting etherscan verify request", "name", contractName, "address", address.Hex()) + artifact, err := v.getContractArtifact(contractName) + if err != nil { + return fmt.Errorf("failed to get contract source: %w", err) + } + + constructorArgs, err := v.getConstructorArgs(ctx, address, artifact) + if err != nil { + return fmt.Errorf("failed to get constructor args: %w", err) + } + + reqId, err := v.etherscan.verifySourceCode(address, artifact, constructorArgs) + if err != nil { + return fmt.Errorf("failed to verify contract: %w", err) + } + v.log.Info("Verification request submitted", "name", contractName, "address", address.Hex()) + + if err = v.etherscan.pollVerificationStatus(reqId); err != nil { + return fmt.Errorf("failed when checking verification status: %w", err) + } + + v.log.Info("Verification complete", "name", contractName, "address", address.Hex()) + v.numVerified++ + return nil +} diff --git a/op-dispute-mon/mon/extract/caller_test.go b/op-dispute-mon/mon/extract/caller_test.go index 8892c2a794..c69f2d51c2 100644 --- a/op-dispute-mon/mon/extract/caller_test.go +++ b/op-dispute-mon/mon/extract/caller_test.go @@ -89,6 +89,9 @@ func TestMetadataCreator_CreateContract(t *testing.T) { func setupMetadataLoaderTest(t *testing.T, gameType uint32) (*batching.MultiCaller, *mockCacheMetrics) { fdgAbi := snapshots.LoadFaultDisputeGameABI() + if gameType == uint32(faultTypes.SuperPermissionedGameType) || gameType == uint32(faultTypes.SuperCannonGameType) { + fdgAbi = snapshots.LoadSuperFaultDisputeGameABI() + } stubRpc := batchingTest.NewAbiBasedRpc(t, fdgAddr, fdgAbi) caller := batching.NewMultiCaller(stubRpc, batching.DefaultBatchSize) stubRpc.SetResponse(fdgAddr, "version", rpcblock.Latest, nil, []interface{}{"0.18.0"}) diff --git a/op-dispute-mon/mon/extract/extractor.go b/op-dispute-mon/mon/extract/extractor.go index c40b31ccb6..3ac1870f5f 100644 --- a/op-dispute-mon/mon/extract/extractor.go +++ b/op-dispute-mon/mon/extract/extractor.go @@ -152,7 +152,7 @@ func (e *Extractor) enrichGame(ctx context.Context, blockHash common.Hash, game LastUpdateTime: e.clock.Now(), GameMetadata: game, L1Head: meta.L1Head, - L2BlockNumber: meta.L2BlockNum, + L2BlockNumber: meta.L2SequenceNum, RootClaim: meta.RootClaim, Status: meta.Status, MaxClockDuration: meta.MaxClockDuration, diff --git a/op-e2e/actions/helpers/l1_miner.go b/op-e2e/actions/helpers/l1_miner.go index b33a68c12c..d05f364744 100644 --- a/op-e2e/actions/helpers/l1_miner.go +++ b/op-e2e/actions/helpers/l1_miner.go @@ -103,7 +103,8 @@ func (s *L1Miner) ActL1StartBlock(timeDelta uint64) Action { if s.l1Cfg.Config.IsCancun(header.Number, header.Time) { header.BlobGasUsed = new(uint64) - header.ExcessBlobGas = new(uint64) + excessBlobGas := eip4844.CalcExcessBlobGas(s.l1Cfg.Config, parent, header.Time) + header.ExcessBlobGas = &excessBlobGas root := crypto.Keccak256Hash([]byte("fake-beacon-block-root"), header.Number.Bytes()) header.ParentBeaconRoot = &root @@ -165,7 +166,7 @@ func (s *L1Miner) ActL1IncludeTxByHash(txHash common.Hash) Action { } } -func (s *L1Miner) IncludeTx(t Testing, tx *types.Transaction) { +func (s *L1Miner) IncludeTx(t Testing, tx *types.Transaction) *types.Receipt { from, err := s.l1Signer.Sender(tx) require.NoError(t, err) s.log.Info("including tx", "nonce", tx.Nonce(), "from", from, "to", tx.To()) @@ -174,7 +175,7 @@ func (s *L1Miner) IncludeTx(t Testing, tx *types.Transaction) { } if tx.Gas() > uint64(*s.L1GasPool) { t.InvalidAction("action takes too much gas: %d, only have %d", tx.Gas(), uint64(*s.L1GasPool)) - return + return nil } s.l1BuildingState.SetTxContext(tx.Hash(), len(s.L1Transactions)) blockCtx := core.NewEVMBlockContext(s.l1BuildingHeader, s.l1Chain, nil, s.l1Cfg.Config, s.l1BuildingState) @@ -195,6 +196,7 @@ func (s *L1Miner) IncludeTx(t Testing, tx *types.Transaction) { } *s.l1BuildingHeader.BlobGasUsed += receipt.BlobGasUsed } + return receipt } func (s *L1Miner) ActL1SetFeeRecipient(coinbase common.Address) { @@ -206,6 +208,7 @@ func (s *L1Miner) ActL1SetFeeRecipient(coinbase common.Address) { // ActL1EndBlock finishes the new L1 block, and applies it to the chain as unsafe block func (s *L1Miner) ActL1EndBlock(t Testing) *types.Block { + t.Helper() if !s.l1Building { t.InvalidAction("cannot end L1 block when not building block") return nil @@ -220,14 +223,14 @@ func (s *L1Miner) ActL1EndBlock(t Testing) *types.Block { withdrawals = make([]*types.Withdrawal, 0) } - block := types.NewBlock(s.l1BuildingHeader, &types.Body{Transactions: s.L1Transactions, Withdrawals: withdrawals}, s.l1Receipts, trie.NewStackTrie(nil), types.DefaultBlockConfig) - isCancun := s.l1Cfg.Config.IsCancun(s.l1BuildingHeader.Number, s.l1BuildingHeader.Time) - if isCancun { - parent := s.l1Chain.GetHeaderByHash(s.l1BuildingHeader.ParentHash) - excessBlobGas := eip4844.CalcExcessBlobGas(s.l1Cfg.Config, parent, s.l1BuildingHeader.Time) - s.l1BuildingHeader.ExcessBlobGas = &excessBlobGas + if s.l1Cfg.Config.IsPrague(s.l1BuildingHeader.Number, s.l1BuildingHeader.Time) { + // Don't process requests for now. + s.l1BuildingHeader.RequestsHash = &types.EmptyRequestsHash } + block := types.NewBlock(s.l1BuildingHeader, &types.Body{Transactions: s.L1Transactions, Withdrawals: withdrawals}, s.l1Receipts, trie.NewStackTrie(nil), types.DefaultBlockConfig) + + isCancun := s.l1Cfg.Config.IsCancun(s.l1BuildingHeader.Number, s.l1BuildingHeader.Time) // Write state changes to db root, err := s.l1BuildingState.Commit(s.l1BuildingHeader.Number.Uint64(), s.l1Cfg.Config.IsEIP158(s.l1BuildingHeader.Number), isCancun) if err != nil { @@ -246,12 +249,13 @@ func (s *L1Miner) ActL1EndBlock(t Testing) *types.Block { } _, err = s.l1Chain.InsertChain(types.Blocks{block}) if err != nil { - t.Fatalf("failed to insert block into l1 chain") + t.Fatalf("failed to insert block into l1 chain: %v", err) } return block } func (s *L1Miner) ActEmptyBlock(t Testing) *types.Block { + t.Helper() s.ActL1StartBlock(12)(t) return s.ActL1EndBlock(t) } diff --git a/op-e2e/actions/helpers/l2_batcher.go b/op-e2e/actions/helpers/l2_batcher.go index 3e67166d92..833572e697 100644 --- a/op-e2e/actions/helpers/l2_batcher.go +++ b/op-e2e/actions/helpers/l2_batcher.go @@ -528,3 +528,49 @@ func (s *L2Batcher) ActSubmitAllMultiBlobs(t Testing, numBlobs int) { s.ActL2ChannelClose(t) s.ActL2BatchSubmitMultiBlob(t, numBlobs) } + +// ActSubmitSetCodeTx submits a SetCodeTx to the batch inbox. This models a malicious +// batcher and is only used to tests the derivation pipeline follows spec and ignores +// the SetCodeTx. +func (s *L2Batcher) ActSubmitSetCodeTx(t Testing) { + chainId := *uint256.MustFromBig(s.rollupCfg.L1ChainID) + + nonce, err := s.l1.PendingNonceAt(t.Ctx(), s.BatcherAddr) + require.NoError(t, err, "need batcher nonce") + + tx, err := PrepareSignedSetCodeTx(chainId, s.l2BatcherCfg.BatcherKey, s.l1Signer, nonce, s.rollupCfg.BatchInboxAddress, s.ReadNextOutputFrame(t)) + require.NoError(t, err, "need to sign tx") + + t.Log("submitting EIP 7702 Set Code Batcher Transaction...") + err = s.l1.SendTransaction(t.Ctx(), tx) + require.NoError(t, err, "need to send tx") + s.LastSubmitted = tx +} + +func PrepareSignedSetCodeTx(chainId uint256.Int, privateKey *ecdsa.PrivateKey, signer types.Signer, nonce uint64, to common.Address, data []byte) (*types.Transaction, error) { + + setCodeAuthorization := types.SetCodeAuthorization{ + ChainID: chainId, + Address: common.HexToAddress("0xab"), // arbitrary nonzero address + Nonce: nonce, + } + + signedAuth, err := types.SignSetCode(privateKey, setCodeAuthorization) + if err != nil { + return nil, err + } + + txData := &types.SetCodeTx{ + ChainID: &chainId, + Nonce: nonce, + To: to, + Value: uint256.NewInt(0), + Data: data, + AccessList: types.AccessList{}, + AuthList: []types.SetCodeAuthorization{signedAuth}, + Gas: 1_000_000, + GasFeeCap: uint256.NewInt(1_000_000_000), + } + + return types.SignNewTx(privateKey, signer, txData) +} diff --git a/op-e2e/actions/helpers/l2_engine.go b/op-e2e/actions/helpers/l2_engine.go index 53f3791193..3a101a70d4 100644 --- a/op-e2e/actions/helpers/l2_engine.go +++ b/op-e2e/actions/helpers/l2_engine.go @@ -200,7 +200,10 @@ func (e *L2Engine) ActL2IncludeTxIgnoreForcedEmpty(from common.Address) Action { } tx := firstValidTx(t, from, e.EngineApi.PendingIndices, e.Eth.TxPool().ContentFrom, e.EthClient().NonceAt) + prevState := e.EngineApi.ForcedEmpty() + e.EngineApi.SetForceEmpty(false) // ensure the engine API can include it _, err := e.EngineApi.IncludeTx(tx, from) + e.EngineApi.SetForceEmpty(prevState) if errors.Is(err, engineapi.ErrNotBuildingBlock) { t.InvalidAction(err.Error()) } else if errors.Is(err, engineapi.ErrUsesTooMuchGas) { diff --git a/op-e2e/actions/helpers/l2_proposer.go b/op-e2e/actions/helpers/l2_proposer.go index dbed03a02c..d657db583d 100644 --- a/op-e2e/actions/helpers/l2_proposer.go +++ b/op-e2e/actions/helpers/l2_proposer.go @@ -198,33 +198,13 @@ func (p *L2Proposer) sendTx(t Testing, data []byte) { // will be created so pending must be used. func estimateGasPending(ctx context.Context, ec *ethclient.Client, msg ethereum.CallMsg) (uint64, error) { var hex hexutil.Uint64 - err := ec.Client().CallContext(ctx, &hex, "eth_estimateGas", toCallArg(msg), "pending") + err := ec.Client().CallContext(ctx, &hex, "eth_estimateGas", sources.ToCallArg(msg), "pending") if err != nil { return 0, err } return uint64(hex), nil } -func toCallArg(msg ethereum.CallMsg) interface{} { - arg := map[string]interface{}{ - "from": msg.From, - "to": msg.To, - } - if len(msg.Data) > 0 { - arg["data"] = hexutil.Bytes(msg.Data) - } - if msg.Value != nil { - arg["value"] = (*hexutil.Big)(msg.Value) - } - if msg.Gas != 0 { - arg["gas"] = hexutil.Uint64(msg.Gas) - } - if msg.GasPrice != nil { - arg["gasPrice"] = (*hexutil.Big)(msg.GasPrice) - } - return arg -} - func (p *L2Proposer) fetchNextOutput(t Testing) (source.Proposal, bool, error) { if p.allocType.UsesProofs() { output, shouldPropose, err := p.driver.FetchDGFOutput(t.Ctx()) diff --git a/op-e2e/actions/helpers/user.go b/op-e2e/actions/helpers/user.go index d7215b8065..f85368baaa 100644 --- a/op-e2e/actions/helpers/user.go +++ b/op-e2e/actions/helpers/user.go @@ -235,6 +235,10 @@ func (s *BasicUser[B]) LastTxReceipt(t Testing) *types.Receipt { return receipt } +func (s *BasicUser[B]) Secret() *ecdsa.PrivateKey { + return s.account +} + func (s *BasicUser[B]) MakeTransaction(t Testing) *types.Transaction { gas, err := s.env.EthCl.EstimateGas(t.Ctx(), ethereum.CallMsg{ From: s.address, diff --git a/op-e2e/actions/helpers/user_test.go b/op-e2e/actions/helpers/user_test.go index 00b27dea1f..ae3a30feab 100644 --- a/op-e2e/actions/helpers/user_test.go +++ b/op-e2e/actions/helpers/user_test.go @@ -67,10 +67,6 @@ func TestCrossLayerUser_Standard(t *testing.T) { testCrossLayerUser(t, config.AllocTypeStandard) } -func TestCrossLayerUser_L2OO(t *testing.T) { - testCrossLayerUser(t, config.AllocTypeL2OO) -} - // TestCrossLayerUser tests that common actions of the CrossLayerUser actor work in various hardfork configurations: // - transact on L1 // - transact on L2 @@ -308,6 +304,12 @@ func runCrossLayerUserTest(gt *testing.T, test hardforkScheduledTest) { require.Equal(t, types.ReceiptStatusSuccessful, receipt.Status, "proposal failed") } + // Mine an empty block so that the timestamp is updated. Otherwise ActProveWithdrawal will fail + // because it tries to estimate gas based on the current timestamp, which is the same timestamp + // as the dispute game creation timestamp, which causes proveWithdrawalTransaction to revert. + miner.ActL1StartBlock(12)(t) + miner.ActL1EndBlock(t) + // prove our withdrawal on L1 alice.ActProveWithdrawal(t) // include proved withdrawal in new L1 block diff --git a/op-e2e/actions/interop/dsl/dsl.go b/op-e2e/actions/interop/dsl/dsl.go index cf123c13cd..dd46bff928 100644 --- a/op-e2e/actions/interop/dsl/dsl.go +++ b/op-e2e/actions/interop/dsl/dsl.go @@ -175,6 +175,12 @@ type SubmitBatchDataOpts struct { SkipCrossSafeUpdate bool } +func WithSkipCrossSafeUpdate() func(*SubmitBatchDataOpts) { + return func(o *SubmitBatchDataOpts) { + o.SkipCrossSafeUpdate = true + } +} + // SubmitBatchData submits batch data to L1 and processes the new L1 blocks, advancing the safe heads. // By default, submits all batch data for all chains. func (d *InteropDSL) SubmitBatchData(optionalArgs ...func(*SubmitBatchDataOpts)) { @@ -250,6 +256,12 @@ type AdvanceL1Opts struct { TxInclusion []helpers.Action } +func WithActIncludeTx(includeTxAction helpers.Action) func(*AdvanceL1Opts) { + return func(o *AdvanceL1Opts) { + o.TxInclusion = append(o.TxInclusion, includeTxAction) + } +} + // AdvanceL1 adds a new L1 block with the specified transactions and ensures it is processed by the specified chains // and the supervisor. func (d *InteropDSL) AdvanceL1(optionalArgs ...func(*AdvanceL1Opts)) { @@ -285,3 +297,64 @@ func (d *InteropDSL) AdvanceL1(optionalArgs ...func(*AdvanceL1Opts)) { require.Equalf(d.t, newBlock, status.CurrentL1, "Chain %v did not process new L1 head", chain.ChainID) } } + +// DeployEmitterContracts deploys an emitter contract on both chains +func (d *InteropDSL) DeployEmitterContracts() *EmitterContract { + emitter := NewEmitterContract(d.t) + alice := d.CreateUser() + d.AddL2Block(d.Actors.ChainA, WithL2BlockTransactions( + emitter.Deploy(alice), + )) + d.AddL2Block(d.Actors.ChainB, WithL2BlockTransactions( + emitter.Deploy(alice), + )) + return emitter +} + +type AdvanceSafeHeadsOpts struct { + SingleBatch bool +} + +func WithSingleBatch() func(*AdvanceSafeHeadsOpts) { + return func(o *AdvanceSafeHeadsOpts) { + o.SingleBatch = true + } +} + +// AdvanceSafeHeads advances the safe heads for all chains by adding a new L2 block and submitting batch data for each chain. +// By default, submits batch data for each chain in separate L1 blocks. +func (d *InteropDSL) AdvanceSafeHeads(optionalArgs ...func(*AdvanceSafeHeadsOpts)) { + opts := AdvanceSafeHeadsOpts{ + SingleBatch: false, + } + for _, arg := range optionalArgs { + arg(&opts) + } + + d.AddL2Block(d.Actors.ChainA) + d.AddL2Block(d.Actors.ChainB) + if opts.SingleBatch { + d.SubmitBatchData() + } else { + d.SubmitBatchData(func(opts *SubmitBatchDataOpts) { + opts.SetChains(d.Actors.ChainA) + }) + d.SubmitBatchData(func(opts *SubmitBatchDataOpts) { + opts.SetChains(d.Actors.ChainB) + }) + } +} + +// AdvanceL2ToLastBlockOfOrigin advances the chain to the last block of the epoch at the specified L1 origin. +func (d *InteropDSL) AdvanceL2ToLastBlockOfOrigin(chain *Chain, l1OriginHeight uint64) { + const l1BlockTime = uint64(12) + require.Equal(d.t, l1BlockTime%chain.RollupCfg.BlockTime, uint64(0), "L2 block time must be a multiple of L1 block time") + endOfEpoch := (l1BlockTime/chain.RollupCfg.BlockTime)*(l1OriginHeight+1) - 1 + require.LessOrEqual(d.t, chain.Sequencer.L2Unsafe().Number, endOfEpoch, "end of epoch is in the future") + for { + if n := chain.Sequencer.L2Unsafe().Number; n == endOfEpoch { + break + } + d.AddL2Block(chain) + } +} diff --git a/op-e2e/actions/interop/dsl/dsl_user.go b/op-e2e/actions/interop/dsl/dsl_user.go index ec17e8060c..57268a6bd4 100644 --- a/op-e2e/actions/interop/dsl/dsl_user.go +++ b/op-e2e/actions/interop/dsl/dsl_user.go @@ -18,10 +18,10 @@ type DSLUser struct { keys devkeys.Keys } -func (u *DSLUser) TransactOpts(chain *Chain) (*bind.TransactOpts, common.Address) { - privKey, err := u.keys.Secret(devkeys.ChainUserKeys(chain.ChainID.ToBig())(u.index)) +func (u *DSLUser) TransactOpts(chainID *big.Int) (*bind.TransactOpts, common.Address) { + privKey, err := u.keys.Secret(devkeys.ChainUserKeys(chainID)(u.index)) require.NoError(u.t, err) - opts, err := bind.NewKeyedTransactorWithChainID(privKey, chain.ChainID.ToBig()) + opts, err := bind.NewKeyedTransactorWithChainID(privKey, chainID) require.NoError(u.t, err) opts.GasTipCap = big.NewInt(params.GWei) diff --git a/op-e2e/actions/interop/dsl/emitter.go b/op-e2e/actions/interop/dsl/emitter.go index 60fcecdf0a..62ffc0c7fe 100644 --- a/op-e2e/actions/interop/dsl/emitter.go +++ b/op-e2e/actions/interop/dsl/emitter.go @@ -24,7 +24,7 @@ func NewEmitterContract(t helpers.Testing) *EmitterContract { func (c *EmitterContract) Deploy(user *DSLUser) TransactionCreator { return func(chain *Chain) *GeneratedTransaction { - opts, from := user.TransactOpts(chain) + opts, from := user.TransactOpts(chain.ChainID.ToBig()) emitContract, tx, _, err := emit.DeployEmit(opts, chain.SequencerEngine.EthClient()) require.NoError(c.t, err) c.addressByChain[chain.ChainID] = emitContract @@ -34,7 +34,7 @@ func (c *EmitterContract) Deploy(user *DSLUser) TransactionCreator { func (c *EmitterContract) EmitMessage(user *DSLUser, message string) TransactionCreator { return func(chain *Chain) *GeneratedTransaction { - opts, from := user.TransactOpts(chain) + opts, from := user.TransactOpts(chain.ChainID.ToBig()) address, ok := c.addressByChain[chain.ChainID] require.Truef(c.t, ok, "not deployed on chain %d", chain.ChainID) bindings, err := emit.NewEmitTransactor(address, chain.SequencerEngine.EthClient()) @@ -51,3 +51,9 @@ func (c *EmitterContract) LastEmittedMessage() *GeneratedTransaction { require.NotZero(c.t, c.EmittedMessages, "no messages have been emitted") return c.EmittedMessages[len(c.EmittedMessages)-1] } + +func (c *EmitterContract) Address(chain *Chain) common.Address { + address, ok := c.addressByChain[chain.ChainID] + require.Truef(c.t, ok, "not deployed on chain %d", chain.ChainID) + return address +} diff --git a/op-e2e/actions/interop/dsl/inbox.go b/op-e2e/actions/interop/dsl/inbox.go index fbd84b6495..0a75ce7239 100644 --- a/op-e2e/actions/interop/dsl/inbox.go +++ b/op-e2e/actions/interop/dsl/inbox.go @@ -1,11 +1,18 @@ package dsl import ( + "math/big" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum-optimism/optimism/op-e2e/actions/helpers" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/interop/contracts/bindings/inbox" + "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/predeploys" - "github.com/ethereum/go-ethereum/crypto" - "github.com/stretchr/testify/require" + stypes "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" ) type InboxContract struct { @@ -37,6 +44,27 @@ func WithPayload(payload []byte) func(opts *ExecuteOpts) { } } +func WithPendingMessage(emitter *EmitterContract, chain *Chain, number uint64, logIndex int, msg string) func(opts *ExecuteOpts) { + return func(opts *ExecuteOpts) { + blockTime := chain.RollupCfg.TimestampForBlock(number) + id := inbox.Identifier{ + Origin: emitter.Address(chain), + BlockNumber: big.NewInt(int64(number)), + LogIndex: big.NewInt(int64(logIndex)), + Timestamp: big.NewInt(int64(blockTime)), + ChainId: chain.RollupCfg.L2ChainID, + } + opts.Identifier = &id + + topic := crypto.Keccak256Hash([]byte("DataEmitted(bytes)")) + var payload []byte + payload = append(payload, topic.Bytes()...) + msgHash := crypto.Keccak256Hash([]byte(msg)) + payload = append(payload, msgHash.Bytes()...) + opts.Payload = &payload + } +} + func (i *InboxContract) Execute(user *DSLUser, initTx *GeneratedTransaction, args ...func(opts *ExecuteOpts)) TransactionCreator { opts := ExecuteOpts{} for _, arg := range args { @@ -58,10 +86,24 @@ func (i *InboxContract) Execute(user *DSLUser, initTx *GeneratedTransaction, arg } else { payload = initTx.MessagePayload() } - txOpts, from := user.TransactOpts(chain) + txOpts, from := user.TransactOpts(chain.ChainID.ToBig()) contract, err := inbox.NewInbox(predeploys.CrossL2InboxAddr, chain.SequencerEngine.EthClient()) require.NoError(i.t, err) - tx, err := contract.ValidateMessage(txOpts, ident, crypto.Keccak256Hash(payload)) + id := stypes.Identifier{ + Origin: ident.Origin, + BlockNumber: ident.BlockNumber.Uint64(), + LogIndex: uint32(ident.LogIndex.Uint64()), + Timestamp: ident.Timestamp.Uint64(), + ChainID: eth.ChainIDFromBig(ident.ChainId), + } + msgHash := crypto.Keccak256Hash(payload) + access := id.ChecksumArgs(msgHash).Access() + inboxAccessList := stypes.EncodeAccessList([]stypes.Access{access}) + txOpts.AccessList = types.AccessList{types.AccessTuple{ + Address: predeploys.CrossL2InboxAddr, + StorageKeys: inboxAccessList, + }} + tx, err := contract.ValidateMessage(txOpts, ident, msgHash) require.NoError(i.t, err) genTx := NewGeneratedTransaction(i.t, chain, tx, from) i.Transactions = append(i.Transactions, genTx) diff --git a/op-e2e/actions/interop/dsl/message.go b/op-e2e/actions/interop/dsl/message.go new file mode 100644 index 0000000000..9ed433c275 --- /dev/null +++ b/op-e2e/actions/interop/dsl/message.go @@ -0,0 +1,117 @@ +package dsl + +import ( + "github.com/ethereum-optimism/optimism/op-e2e/actions/helpers" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/interop/contracts/bindings/inbox" + "github.com/ethereum-optimism/optimism/op-service/predeploys" + "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/require" +) + +type Message struct { + t helpers.Testing + user *DSLUser + chain *Chain + message string + emitter *EmitterContract + inbox *InboxContract + l1Miner *helpers.L1Miner + + initTx *GeneratedTransaction + execTx *GeneratedTransaction +} + +func NewMessage(dsl *InteropDSL, chain *Chain, emitter *EmitterContract, message string) *Message { + return &Message{ + t: dsl.t, + user: dsl.CreateUser(), + chain: chain, + emitter: emitter, + inbox: dsl.InboxContract, + l1Miner: dsl.Actors.L1Miner, + message: message, + } +} + +func (m *Message) Emit() *Message { + emitAction := m.emitter.EmitMessage(m.user, m.message) + m.initTx = emitAction(m.chain) + m.initTx.IncludeOK() + return m +} + +// EmitDeposit emits a message via a user deposit transaction. +func (m *Message) EmitDeposit(l1User *DSLUser) *Message { + emitAction := m.emitter.EmitMessage(m.user, m.message) + m.initTx = emitAction(m.chain) + opts, _ := m.user.TransactOpts(m.chain.ChainID.ToBig()) + m.initTx.IncludeDepositOK(l1User, opts, m.l1Miner) + return m +} + +// ActEmitDeposit returns an action that emits a message via a user deposit transaction. +func (m *Message) ActEmitDeposit(l1User *DSLUser) helpers.Action { + return func(t helpers.Testing) { + m.EmitDeposit(l1User) + } +} + +func sanityCheckAccessList(t helpers.Testing, li types.AccessList) { + for _, e := range li { + if e.Address == predeploys.CrossL2InboxAddr { + if len(e.StorageKeys) > 0 { + return + } + } + } + t.Fatal("expected executing-message entries in access-list") +} + +func (m *Message) ExecuteOn(target *Chain, execOpts ...func(*ExecuteOpts)) *Message { + require.NotNil(m.t, m.initTx, "message must be emitted before it can be executed") + execAction := m.inbox.Execute(m.user, m.initTx, execOpts...) + m.execTx = execAction(target) + sanityCheckAccessList(m.t, m.execTx.tx.AccessList()) + m.execTx.IncludeOK() + return m +} + +// ExecutePendingOn executes a message that may not have been emitted yet. +func (m *Message) ExecutePendingOn(target *Chain, pendingMessageBlockNumber uint64, execOpts ...func(*ExecuteOpts)) *Message { + var opts []func(*ExecuteOpts) + opts = append(opts, WithPendingMessage(m.emitter, m.chain, pendingMessageBlockNumber, 0, m.message)) + opts = append(opts, execOpts...) + execAction := m.inbox.Execute(m.user, nil, opts...) + m.execTx = execAction(target) + sanityCheckAccessList(m.t, m.execTx.tx.AccessList()) + m.execTx.IncludeOK() + return m +} + +func (m *Message) CheckEmitted() { + require.NotNil(m.t, m.initTx, "message must be emitted before it can be checked") + m.initTx.CheckIncluded() +} + +func (m *Message) CheckNotEmitted() { + require.NotNil(m.t, m.initTx, "message must be emitted before it can be checked") + m.initTx.CheckNotIncluded() +} + +func (m *Message) CheckNotExecuted() { + require.NotNil(m.t, m.execTx, "message must be executed before it can be checked") + m.execTx.CheckNotIncluded() +} + +func (m *Message) CheckExecuted() { + require.NotNil(m.t, m.execTx, "message must be executed before it can be checked") + m.execTx.CheckIncluded() +} + +func (m *Message) ExecutePayload() []byte { + return m.execTx.MessagePayload() +} + +func (m *Message) ExecuteIdentifier() inbox.Identifier { + return m.execTx.Identifier() +} diff --git a/op-e2e/actions/interop/dsl/transactions.go b/op-e2e/actions/interop/dsl/transactions.go index 941d239b9e..7cdc2de7f3 100644 --- a/op-e2e/actions/interop/dsl/transactions.go +++ b/op-e2e/actions/interop/dsl/transactions.go @@ -4,9 +4,11 @@ import ( "math/big" "github.com/ethereum-optimism/optimism/op-e2e/actions/helpers" + "github.com/ethereum-optimism/optimism/op-e2e/bindingspreview" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/interop/contracts/bindings/inbox" stypes "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/stretchr/testify/require" @@ -40,6 +42,31 @@ func (m *GeneratedTransaction) Include() { m.rcpt = rcpt } +func (m *GeneratedTransaction) IncludeOK() { + rcpt, err := m.chain.SequencerEngine.EngineApi.IncludeTx(m.tx, m.from) + require.NoError(m.t, err) + m.rcpt = rcpt + require.Equal(m.t, types.ReceiptStatusSuccessful, rcpt.Status) +} + +// IncludeDepositOK includes the GeneratedTransaction via a user deposit transaction. +func (m *GeneratedTransaction) IncludeDepositOK(l1User *DSLUser, depositTxOpts *bind.TransactOpts, l1Miner *helpers.L1Miner) { + optimismPortal2, err := bindingspreview.NewOptimismPortal2(m.chain.RollupCfg.DepositContractAddress, l1Miner.EthClient()) + require.NoError(m.t, err) + + l1Opts, _ := l1User.TransactOpts(l1Miner.L1Chain().Config().ChainID) + l1Opts.Value = depositTxOpts.Value + + to := m.tx.To() + min, err := optimismPortal2.MinimumGasLimit(&bind.CallOpts{}, uint64(len(m.tx.Data()))) + require.NoError(m.t, err) + gas := max(m.tx.Gas(), min) + tx, err := optimismPortal2.DepositTransaction(l1Opts, *to, m.tx.Value(), gas, to == nil, m.tx.Data()) + require.NoError(m.t, err, "failed to create deposit tx") + rcpt := l1Miner.IncludeTx(m.t, tx) + require.Equal(m.t, types.ReceiptStatusSuccessful, rcpt.Status, "deposit tx failed") +} + func (m *GeneratedTransaction) Identifier() inbox.Identifier { require.NotZero(m.t, len(m.rcpt.Logs), "Transaction did not include any logs to reference") @@ -64,8 +91,8 @@ func (m *GeneratedTransaction) MessagePayload() []byte { func (m *GeneratedTransaction) CheckIncluded() { rcpt, err := m.chain.SequencerEngine.EthClient().TransactionReceipt(m.t.Ctx(), m.tx.Hash()) - require.NoError(m.t, err) - require.NotNil(m.t, rcpt) + require.NoError(m.t, err, "Transaction should have been included") + require.NotNil(m.t, rcpt, "No receipt found") } func (m *GeneratedTransaction) CheckNotIncluded() { @@ -73,3 +100,15 @@ func (m *GeneratedTransaction) CheckNotIncluded() { require.ErrorIs(m.t, err, ethereum.NotFound) require.Nil(m.t, rcpt) } + +func (m *GeneratedTransaction) PendingIdentifier(chain *Chain, logIndex int) inbox.Identifier { + head := chain.Sequencer.L2Unsafe() + blockTime := chain.RollupCfg.TimestampForBlock(head.Number) + return inbox.Identifier{ + Origin: *m.tx.To(), + BlockNumber: big.NewInt(int64(head.Number + 1)), + LogIndex: big.NewInt(int64(logIndex)), + Timestamp: big.NewInt(int64(blockTime)), + ChainId: chain.RollupCfg.L2ChainID, + } +} diff --git a/op-e2e/actions/interop/emitter_contract_test.go b/op-e2e/actions/interop/emitter_contract_test.go index 756e5e84ad..cf2d742cf6 100644 --- a/op-e2e/actions/interop/emitter_contract_test.go +++ b/op-e2e/actions/interop/emitter_contract_test.go @@ -5,21 +5,23 @@ import ( "math/big" "testing" - "github.com/ethereum-optimism/optimism/op-e2e/actions/interop/dsl" + "github.com/stretchr/testify/require" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" - "github.com/stretchr/testify/require" "github.com/ethereum-optimism/optimism/op-chain-ops/devkeys" "github.com/ethereum-optimism/optimism/op-e2e/actions/helpers" + "github.com/ethereum-optimism/optimism/op-e2e/actions/interop/dsl" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/interop/contracts/bindings/emit" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/interop/contracts/bindings/inbox" "github.com/ethereum-optimism/optimism/op-node/rollup/derive" "github.com/ethereum-optimism/optimism/op-node/rollup/event" + "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/predeploys" stypes "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" ) @@ -71,7 +73,14 @@ func TestEmitterContract(gt *testing.T) { id := idForTx(t, emitTx, actors.ChainA) contract, err := inbox.NewInbox(predeploys.CrossL2InboxAddr, actors.ChainB.SequencerEngine.EthClient()) require.NoError(t, err) - tx, err := contract.ValidateMessage(auth, id, crypto.Keccak256Hash(fakeMessage)) + msgHash := crypto.Keccak256Hash(fakeMessage) + access := id.ChecksumArgs(msgHash).Access() + inboxAccessList := stypes.EncodeAccessList([]stypes.Access{access}) + auth.AccessList = types.AccessList{types.AccessTuple{ + Address: predeploys.CrossL2InboxAddr, + StorageKeys: inboxAccessList, + }} + tx, err := contract.ValidateMessage(auth, identifierForBindings(id), msgHash) require.NoError(t, err) // Process the invalid message attempt and verify that only the local unsafe head progresses @@ -113,35 +122,57 @@ func newEmitMessageTx(t helpers.Testing, chain *dsl.Chain, user *userWithKeys, e return tx } +// newExecuteMessageTx creates a new executing message tx based on the given initializing tx. func newExecuteMessageTx(t helpers.Testing, destChain *dsl.Chain, executor *userWithKeys, srcChain *dsl.Chain, srcTx *types.Transaction) *types.Transaction { // Create the id and payload id := idForTx(t, srcTx, srcChain) receipt, err := srcChain.SequencerEngine.EthClient().TransactionReceipt(t.Ctx(), srcTx.Hash()) require.NoError(t, err) payload := stypes.LogToMessagePayload(receipt.Logs[0]) + hash := crypto.Keccak256Hash(payload) // Create the tx to validate the message + return newExecuteMessageTxFromIDAndHash(t, executor, destChain, id, hash) +} + +// newExecuteMessageTxFromIDAndHash creates a new executing message tx for the given id and hash. +func newExecuteMessageTxFromIDAndHash(t helpers.Testing, executor *userWithKeys, destChain *dsl.Chain, id stypes.Identifier, msgHash common.Hash) *types.Transaction { inboxContract, err := inbox.NewInbox(predeploys.CrossL2InboxAddr, destChain.SequencerEngine.EthClient()) require.NoError(t, err) auth := newL2TxOpts(t, executor.secret, destChain) - tx, err := inboxContract.ValidateMessage(auth, id, crypto.Keccak256Hash(payload)) + access := id.ChecksumArgs(msgHash).Access() + inboxAccessList := stypes.EncodeAccessList([]stypes.Access{access}) + auth.AccessList = types.AccessList{types.AccessTuple{ + Address: predeploys.CrossL2InboxAddr, + StorageKeys: inboxAccessList, + }} + tx, err := inboxContract.ValidateMessage(auth, identifierForBindings(id), msgHash) require.NoError(t, err) - return tx } -func idForTx(t helpers.Testing, tx *types.Transaction, srcChain *dsl.Chain) inbox.Identifier { +func idForTx(t helpers.Testing, tx *types.Transaction, srcChain *dsl.Chain) stypes.Identifier { receipt, err := srcChain.SequencerEngine.EthClient().TransactionReceipt(t.Ctx(), tx.Hash()) require.NoError(t, err) block, err := srcChain.SequencerEngine.EthClient().BlockByNumber(t.Ctx(), receipt.BlockNumber) require.NoError(t, err) - return inbox.Identifier{ + return stypes.Identifier{ Origin: *tx.To(), - BlockNumber: receipt.BlockNumber, - LogIndex: common.Big0, - Timestamp: big.NewInt(int64(block.Time())), - ChainId: srcChain.RollupCfg.L2ChainID, + BlockNumber: receipt.BlockNumber.Uint64(), + LogIndex: 0, + Timestamp: block.Time(), + ChainID: eth.ChainIDFromBig(srcChain.RollupCfg.L2ChainID), + } +} + +func identifierForBindings(id stypes.Identifier) inbox.Identifier { + return inbox.Identifier{ + Origin: id.Origin, + BlockNumber: new(big.Int).SetUint64(id.BlockNumber), + LogIndex: new(big.Int).SetUint64(uint64(id.LogIndex)), + Timestamp: new(big.Int).SetUint64(id.Timestamp), + ChainId: id.ChainID.ToBig(), } } diff --git a/op-e2e/actions/interop/interop_test.go b/op-e2e/actions/interop/interop_test.go index 9f4d5dd103..c466dbe5e0 100644 --- a/op-e2e/actions/interop/interop_test.go +++ b/op-e2e/actions/interop/interop_test.go @@ -1,7 +1,6 @@ package interop import ( - "math/big" "testing" "github.com/stretchr/testify/require" @@ -12,12 +11,11 @@ import ( "github.com/ethereum-optimism/optimism/op-e2e/actions/helpers" "github.com/ethereum-optimism/optimism/op-e2e/actions/interop/dsl" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/interop/contracts/bindings/emit" - "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/interop/contracts/bindings/inbox" "github.com/ethereum-optimism/optimism/op-node/rollup/derive" "github.com/ethereum-optimism/optimism/op-node/rollup/event" "github.com/ethereum-optimism/optimism/op-node/rollup/interop/managed" "github.com/ethereum-optimism/optimism/op-service/eth" - "github.com/ethereum-optimism/optimism/op-service/predeploys" + stypes "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" ) func TestFullInterop(gt *testing.T) { @@ -251,21 +249,18 @@ func TestInteropLocalSafeInvalidation(gt *testing.T) { // build L2 block on chain B with invalid executing message pointing to A. fakeMessage := []byte("this message was never emitted") aliceB := setupUser(t, is, actors.ChainB, 0) - auth := newL2TxOpts(t, aliceB.secret, actors.ChainB) - id := inbox.Identifier{ + id := stypes.Identifier{ Origin: common.Address{0x42}, - BlockNumber: new(big.Int).SetUint64(genesisB.UnsafeL2.Number), - LogIndex: common.Big0, - Timestamp: new(big.Int).SetUint64(genesisB.UnsafeL2.Time), - ChainId: actors.ChainA.RollupCfg.L2ChainID, + BlockNumber: genesisB.UnsafeL2.Number, + LogIndex: 0, + Timestamp: genesisB.UnsafeL2.Time, + ChainID: eth.ChainIDFromBig(actors.ChainA.RollupCfg.L2ChainID), } - contract, err := inbox.NewInbox(predeploys.CrossL2InboxAddr, actors.ChainB.SequencerEngine.EthClient()) - require.NoError(t, err) - tx, err := contract.ValidateMessage(auth, id, crypto.Keccak256Hash(fakeMessage)) - require.NoError(t, err) + msgHash := crypto.Keccak256Hash(fakeMessage) + tx := newExecuteMessageTxFromIDAndHash(t, aliceB, actors.ChainB, id, msgHash) actors.ChainB.Sequencer.ActL2StartBlock(t) - _, err = actors.ChainB.SequencerEngine.EngineApi.IncludeTx(tx, aliceB.address) + _, err := actors.ChainB.SequencerEngine.EngineApi.IncludeTx(tx, aliceB.address) require.NoError(t, err) actors.ChainB.Sequencer.ActL2EndBlock(t) actors.ChainB.Sequencer.ActL2PipelineFull(t) @@ -439,3 +434,36 @@ func TestInteropCrossSafeDependencyDelay(gt *testing.T) { require.NoError(t, err) require.Equal(t, chainBSubmittedIn.NumberU64(), source.Number) } + +func TestInteropExecutingMessageOutOfRangeLogIndex(gt *testing.T) { + t := helpers.NewDefaultTesting(gt) + is := dsl.SetupInterop(t) + actors := is.CreateActors() + actors.PrepareChainState(t) + aliceA := setupUser(t, is, actors.ChainA, 0) + + // Execute a fake log on chain A + chainBHead := actors.ChainB.Sequencer.SyncStatus().UnsafeL2 + nonExistentID := stypes.Identifier{ + Origin: aliceA.address, + BlockNumber: chainBHead.Number, + LogIndex: 0, + Timestamp: chainBHead.Time, + ChainID: eth.ChainIDFromBig(actors.ChainB.RollupCfg.L2ChainID), + } + nonExistentHash := crypto.Keccak256Hash([]byte("fake message")) + tx := newExecuteMessageTxFromIDAndHash(t, aliceA, actors.ChainA, nonExistentID, nonExistentHash) + includeTxOnChainBasic(t, actors.ChainA, tx, aliceA.address) + actors.ChainB.Sequencer.ActL2EmptyBlock(t) + + // Sync the system + actors.ChainA.Sequencer.SyncSupervisor(t) + actors.ChainB.Sequencer.SyncSupervisor(t) + actors.Supervisor.ProcessFull(t) + actors.ChainA.Sequencer.ActL2PipelineFull(t) + actors.ChainB.Sequencer.ActL2PipelineFull(t) + + // Assert that chainA's block is not cross-safe but chainB's is. + assertHeads(t, actors.ChainA, 1, 0, 0, 0) + assertHeads(t, actors.ChainB, 1, 0, 1, 0) +} diff --git a/op-e2e/actions/interop/proofs_test.go b/op-e2e/actions/interop/proofs_test.go index dc700c8972..f60656b682 100644 --- a/op-e2e/actions/interop/proofs_test.go +++ b/op-e2e/actions/interop/proofs_test.go @@ -1,9 +1,11 @@ package interop import ( + "bytes" "fmt" "log/slog" "math/big" + "reflect" "testing" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/super" @@ -90,9 +92,7 @@ func TestInteropFaultProofs_ConsolidateValidCrossChainMessage(gt *testing.T) { actors := system.Actors alice := system.CreateUser() - emitter := dsl.NewEmitterContract(t) - system.AddL2Block(actors.ChainA, dsl.WithL2BlockTransactions(emitter.Deploy(alice))) - system.AddL2Block(actors.ChainB, dsl.WithL2BlockTransactions(emitter.Deploy(alice))) + emitter := system.DeployEmitterContracts() system.AddL2Block(system.Actors.ChainA, dsl.WithL2BlockTransactions(emitter.EmitMessage(alice, "hello"))) initMsg := emitter.LastEmittedMessage() @@ -365,174 +365,83 @@ func TestInteropFaultProofs(gt *testing.T) { runFppAndChallengerTests(gt, system, tests) } -func TestInteropFaultProofs_Cycle(gt *testing.T) { - t := helpers.NewDefaultTesting(gt) - - system := dsl.NewInteropDSL(t) - actors := system.Actors - - alice := system.CreateUser() - emitter := dsl.NewEmitterContract(t) - system.AddL2Block(actors.ChainA, dsl.WithL2BlockTransactions(emitter.Deploy(alice))) - system.AddL2Block(actors.ChainB, dsl.WithL2BlockTransactions(emitter.Deploy(alice))) - - assertHeads(t, actors.ChainA, 1, 0, 1, 0) - assertHeads(t, actors.ChainB, 1, 0, 1, 0) - - actEmitA := emitter.EmitMessage(alice, "hello") - actEmitB := emitter.EmitMessage(alice, "world") - - actors.ChainA.Sequencer.ActL2StartBlock(t) - actors.ChainB.Sequencer.ActL2StartBlock(t) - - // create init messages - emitTxA := actEmitA(actors.ChainA) - emitTxA.Include() - emitTxB := actEmitB(actors.ChainB) - emitTxB.Include() - - // execute them within the same block - actExecA := system.InboxContract.Execute(alice, emitTxB) // Exec msg on chain A referencing chain B - actExecB := system.InboxContract.Execute(alice, emitTxA) // Exec msg on chain B referencing chain A - actExecA(actors.ChainA).Include() - actExecB(actors.ChainB).Include() - - actors.ChainA.Sequencer.ActL2EndBlock(t) - actors.ChainB.Sequencer.ActL2EndBlock(t) - actors.ChainA.Sequencer.SyncSupervisor(t) - actors.ChainB.Sequencer.SyncSupervisor(t) - actors.Supervisor.ProcessFull(t) - actors.ChainA.Sequencer.ActL2PipelineFull(t) - actors.ChainB.Sequencer.ActL2PipelineFull(t) - - assertHeads(t, actors.ChainA, 2, 0, 2, 0) - assertHeads(t, actors.ChainB, 2, 0, 2, 0) - - system.SubmitBatchData() - assertHeads(t, actors.ChainA, 2, 2, 2, 2) - assertHeads(t, actors.ChainB, 2, 2, 2, 2) - - endTimestamp := system.Actors.ChainA.Sequencer.L2Safe().Time - startTimestamp := endTimestamp - 1 - end := system.Outputs.SuperRoot(endTimestamp) - - paddingStep := func(step uint64) []byte { - return system.Outputs.TransitionState(startTimestamp, step, - system.Outputs.OptimisticBlockAtTimestamp(actors.ChainA, endTimestamp), - system.Outputs.OptimisticBlockAtTimestamp(actors.ChainB, endTimestamp), - ).Marshal() - } - - tests := []*transitionTest{ - { - name: "Consolidate-AllValid", - agreedClaim: paddingStep(consolidateStep), - disputedClaim: end.Marshal(), - disputedTraceIndex: consolidateStep, - expectValid: true, - }, - { - name: "Consolidate-AllValid-InvalidNoChange", - agreedClaim: paddingStep(consolidateStep), - disputedClaim: paddingStep(consolidateStep), - disputedTraceIndex: consolidateStep, - expectValid: false, - }, - } - runFppAndChallengerTests(gt, system, tests) -} - -func TestInteropFaultProofs_CascadeInvalidBlock(gt *testing.T) { - // TODO(#14307): Support cascading block invalidations - gt.Skip("TODO(#14307): Support cascading block invalidations") - t := helpers.NewDefaultTesting(gt) - - system := dsl.NewInteropDSL(t) - - actors := system.Actors - alice := system.CreateUser() - emitterContract := dsl.NewEmitterContract(t) - // Deploy emitter contract to both chains - system.AddL2Block(actors.ChainA, dsl.WithL2BlockTransactions( - emitterContract.Deploy(alice), - )) - system.AddL2Block(actors.ChainB, dsl.WithL2BlockTransactions( - emitterContract.Deploy(alice), - )) - - assertHeads(t, actors.ChainA, 1, 0, 1, 0) - assertHeads(t, actors.ChainB, 1, 0, 1, 0) - - // Create initiating and executing messages within the same block - var ( - chainAExecTx *dsl.GeneratedTransaction - chainBExecTx *dsl.GeneratedTransaction - chainBInitTx *dsl.GeneratedTransaction - ) - { - actors.ChainA.Sequencer.ActL2StartBlock(t) - actors.ChainB.Sequencer.ActL2StartBlock(t) - - chainAInitTx := emitterContract.EmitMessage(alice, "chainA message")(actors.ChainA) - chainAInitTx.Include() - - // Create messages with a conflicting payload on chain B, while also emitting an initiating message - chainBExecTx := system.InboxContract.Execute(alice, chainAInitTx, - dsl.WithPayload([]byte("this message was never emitted")))(actors.ChainB) - chainBExecTx.Include() - chainBInitTx = emitterContract.EmitMessage(alice, "chainB message")(actors.ChainB) - chainBInitTx.Include() - - // Create a message with a valid message on chain A, pointing to the initiating message on B from the same block - // as an invalid message. - chainAExecTx = system.InboxContract.Execute(alice, chainBInitTx)(actors.ChainA) - chainAExecTx.Include() - - actors.ChainA.Sequencer.ActL2EndBlock(t) - actors.ChainB.Sequencer.ActL2EndBlock(t) +func TestInteropFaultProofs_IntraBlock(gt *testing.T) { + cases := []intraBlockTestCase{ + new(cascadeInvalidBlockCase), + new(swapCascadeInvalidBlockCase), + new(cyclicDependencyInvalidCase), + new(cyclicDependencyValidCase), + new(longDependencyChainValidCase), + new(sameChainMessageValidCase), + new(sameChainMessageInvalidCase), } - assertHeads(t, actors.ChainA, 2, 0, 1, 0) - assertHeads(t, actors.ChainB, 2, 0, 1, 0) - - system.SubmitBatchData(func(opts *dsl.SubmitBatchDataOpts) { - opts.SkipCrossSafeUpdate = true - }) - - endTimestamp := actors.ChainB.Sequencer.L2Unsafe().Time - startTimestamp := endTimestamp - 1 - optimisticEnd := system.Outputs.SuperRoot(endTimestamp) - - preConsolidation := system.Outputs.TransitionState(startTimestamp, consolidateStep, - system.Outputs.OptimisticBlockAtTimestamp(actors.ChainA, endTimestamp), - system.Outputs.OptimisticBlockAtTimestamp(actors.ChainB, endTimestamp), - ).Marshal() - - // Induce block replacement - system.ProcessCrossSafe() - // assert that the invalid message txs were reorged out - chainBExecTx.CheckNotIncluded() - chainBInitTx.CheckNotIncluded() // Should have been reorged out with chainBExecTx - chainAExecTx.CheckNotIncluded() // Reorged out because chainBInitTx was reorged out - - crossSafeEnd := system.Outputs.SuperRoot(endTimestamp) + for _, c := range cases { + c := c + name := reflect.TypeOf(c).Elem().Name() + gt.Run(name, func(gt *testing.T) { + if name == "cascadeInvalidBlockCase" || name == "swapCascadeInvalidBlockCase" || name == "cyclicDependencyInvalidCase" { + // TODO(#14307): Support cascading block invalidations in the supervisor + gt.Skip("Skipping cascade invalid block case") + } + t := helpers.NewDefaultTesting(gt) + system := dsl.NewInteropDSL(t) + + actors := system.Actors + emitterContract := system.DeployEmitterContracts() + + actors.ChainA.Sequencer.ActL2StartBlock(t) + actors.ChainB.Sequencer.ActL2StartBlock(t) + c.Setup(t, system, emitterContract, actors) + actors.ChainA.Sequencer.ActL2EndBlock(t) + actors.ChainB.Sequencer.ActL2EndBlock(t) + + system.SubmitBatchData(func(opts *dsl.SubmitBatchDataOpts) { + opts.SkipCrossSafeUpdate = true + }) - tests := []*transitionTest{ - { - name: "Consolidate-ExpectInvalidPendingBlock", - agreedClaim: preConsolidation, - disputedClaim: optimisticEnd.Marshal(), - disputedTraceIndex: consolidateStep, - expectValid: false, - }, - { - name: "Consolidate-ReplaceInvalidBlocks", - agreedClaim: preConsolidation, - disputedClaim: crossSafeEnd.Marshal(), - disputedTraceIndex: consolidateStep, - expectValid: true, - }, + endTimestamp := actors.ChainB.Sequencer.L2Unsafe().Time + startTimestamp := endTimestamp - 1 + optimisticEnd := system.Outputs.SuperRoot(endTimestamp) + + preConsolidation := system.Outputs.TransitionState(startTimestamp, consolidateStep, + system.Outputs.OptimisticBlockAtTimestamp(actors.ChainA, endTimestamp), + system.Outputs.OptimisticBlockAtTimestamp(actors.ChainB, endTimestamp), + ).Marshal() + + // Induce block replacement + system.ProcessCrossSafe() + c.RunCrossSafeChecks(t, system, actors) + crossSafeEnd := system.Outputs.SuperRoot(endTimestamp) + optimisticIsCrossSafe := bytes.Equal(optimisticEnd.Marshal(), crossSafeEnd.Marshal()) + + tests := []*transitionTest{ + { + name: "Consolidate", + agreedClaim: preConsolidation, + disputedClaim: crossSafeEnd.Marshal(), + disputedTraceIndex: consolidateStep, + expectValid: true, + }, + { + name: "Consolidate-InvalidNoChange", + agreedClaim: preConsolidation, + disputedClaim: preConsolidation, + disputedTraceIndex: consolidateStep, + expectValid: false, + }, + } + if !optimisticIsCrossSafe { + tests = append(tests, &transitionTest{ + name: "Consolidate-ExpectInvalidPendingBlock", + agreedClaim: preConsolidation, + disputedClaim: optimisticEnd.Marshal(), + disputedTraceIndex: consolidateStep, + expectValid: false, + }) + } + runFppAndChallengerTests(gt, system, tests) + }) } - runFppAndChallengerTests(gt, system, tests) } func TestInteropFaultProofs_MessageExpiry(gt *testing.T) { @@ -541,10 +450,7 @@ func TestInteropFaultProofs_MessageExpiry(gt *testing.T) { actors := system.Actors alice := system.CreateUser() - emitterContract := dsl.NewEmitterContract(t) - system.AddL2Block(actors.ChainA, dsl.WithL2BlockTransactions( - emitterContract.Deploy(alice), - )) + emitterContract := system.DeployEmitterContracts() system.AddL2Block(actors.ChainA, dsl.WithL2BlockTransactions( emitterContract.EmitMessage(alice, "test message"), )) @@ -614,10 +520,8 @@ func TestInteropFaultProofsInvalidBlock(gt *testing.T) { actors := system.Actors alice := system.CreateUser() - emitterContract := dsl.NewEmitterContract(t) - system.AddL2Block(actors.ChainA, dsl.WithL2BlockTransactions( - emitterContract.Deploy(alice), - )) + emitterContract := system.DeployEmitterContracts() + system.AddL2Block(actors.ChainA, dsl.WithL2BlockTransactions( emitterContract.EmitMessage(alice, "test message"), )) @@ -625,7 +529,6 @@ func TestInteropFaultProofsInvalidBlock(gt *testing.T) { // Bring ChainB to the same height and timestamp system.AddL2Block(actors.ChainB) - system.AddL2Block(actors.ChainB) system.SubmitBatchData() // Create a message with a conflicting payload @@ -774,135 +677,893 @@ func TestInteropFaultProofs_VariedBlockTimes(gt *testing.T) { system := dsl.NewInteropDSL(t, dsl.SetBlockTimeForChainA(1), dsl.SetBlockTimeForChainB(2)) actors := system.Actors - system.AddL2Block(system.Actors.ChainA) - system.AddL2Block(system.Actors.ChainB) - - assertTime(t, actors.ChainA, 1, 1, 0, 0) - assertTime(t, actors.ChainB, 2, 2, 0, 0) - // TODO(#14479): Complete test case -} - -func runFppAndChallengerTests(gt *testing.T, system *dsl.InteropDSL, tests []*transitionTest) { - for _, test := range tests { - test := test - gt.Run(fmt.Sprintf("%s-fpp", test.name), func(gt *testing.T) { - runFppTest(gt, test, system.Actors, system.DepSet()) - }) - - gt.Run(fmt.Sprintf("%s-challenger", test.name), func(gt *testing.T) { - runChallengerTest(gt, test, system.Actors) - }) - } -} + system.AdvanceSafeHeads() + assertTime(t, actors.ChainA, 1, 1, 1, 1) + assertTime(t, actors.ChainB, 2, 2, 2, 2) -func runFppTest(gt *testing.T, test *transitionTest, actors *dsl.InteropActors, depSet *depset.StaticConfigDependencySet) { - t := helpers.NewDefaultTesting(gt) - if test.skipProgram { - t.Skip("Not yet implemented") - return - } - logger := testlog.Logger(t, slog.LevelInfo) - checkResult := fpHelpers.ExpectNoError() - if !test.expectValid { - checkResult = fpHelpers.ExpectError(claim.ErrClaimNotValid) - } - l1Head := test.l1Head - if l1Head == (common.Hash{}) { - l1Head = actors.L1Miner.L1Chain().CurrentBlock().Hash() - } - proposalTimestamp := test.proposalTimestamp - if proposalTimestamp == 0 { - proposalTimestamp = actors.ChainA.Sequencer.L2Unsafe().Time - } - fpHelpers.RunFaultProofProgram( - t, - logger, - actors.L1Miner, - checkResult, - WithInteropEnabled(t, actors, depSet, test.agreedClaim, crypto.Keccak256Hash(test.disputedClaim), proposalTimestamp), - fpHelpers.WithL1Head(l1Head), - ) -} + endTimestamp := actors.ChainA.Sequencer.L2Safe().Time + startTimestamp := endTimestamp - 1 -func runChallengerTest(gt *testing.T, test *transitionTest, actors *dsl.InteropActors) { - t := helpers.NewDefaultTesting(gt) - if test.skipChallenger { - t.Skip("Not yet implemented") - return - } - logger := testlog.Logger(t, slog.LevelInfo) - endTimestamp := test.proposalTimestamp - if endTimestamp == 0 { - endTimestamp = actors.ChainA.Sequencer.L2Unsafe().Time - } - startTimestamp := actors.ChainA.Sequencer.L2Unsafe().Time - 1 - prestateProvider := super.NewSuperRootPrestateProvider(actors.Supervisor, startTimestamp) - var l1Head eth.BlockID - if test.l1Head == (common.Hash{}) { - l1Head = eth.ToBlockID(eth.HeaderBlockInfo(actors.L1Miner.L1Chain().CurrentBlock())) - } else { - l1Head = eth.ToBlockID(actors.L1Miner.L1Chain().GetBlockByHash(test.l1Head)) - } - gameDepth := challengerTypes.Depth(30) - rollupCfgs, err := super.NewRollupConfigsFromParsed(actors.ChainA.RollupCfg, actors.ChainB.RollupCfg) - require.NoError(t, err) - provider := super.NewSuperTraceProvider(logger, rollupCfgs, prestateProvider, actors.Supervisor, l1Head, gameDepth, startTimestamp, endTimestamp) - var agreedPrestate []byte - if test.disputedTraceIndex > 0 { - agreedPrestate, err = provider.GetPreimageBytes(t.Ctx(), challengerTypes.NewPosition(gameDepth, big.NewInt(test.disputedTraceIndex-1))) - require.NoError(t, err) - } else { - superRoot, err := provider.AbsolutePreState(t.Ctx()) - require.NoError(t, err) - agreedPrestate = superRoot.Marshal() - } - require.Equal(t, test.agreedClaim, agreedPrestate, "agreed prestate mismatch") + start := system.Outputs.SuperRoot(startTimestamp) + end := system.Outputs.SuperRoot(endTimestamp) + l1Head := actors.L1Miner.L1Chain().CurrentBlock().Hash() - disputedClaim, err := provider.GetPreimageBytes(t.Ctx(), challengerTypes.NewPosition(gameDepth, big.NewInt(test.disputedTraceIndex))) - require.NoError(t, err) - if test.expectValid { - require.Equal(t, test.disputedClaim, disputedClaim, "Claim is correct so should match challenger's opinion") - } else { - require.NotEqual(t, test.disputedClaim, disputedClaim, "Claim is incorrect so should not match challenger's opinion") - } -} + step1Expected := system.Outputs.TransitionState(startTimestamp, 1, + system.Outputs.OptimisticBlockAtTimestamp(actors.ChainA, endTimestamp), + ).Marshal() -func WithInteropEnabled(t helpers.StatefulTesting, actors *dsl.InteropActors, depSet *depset.StaticConfigDependencySet, agreedPrestate []byte, disputedClaim common.Hash, claimTimestamp uint64) fpHelpers.FixtureInputParam { - return func(f *fpHelpers.FixtureInputs) { - f.InteropEnabled = true - f.AgreedPrestate = agreedPrestate - f.L2OutputRoot = crypto.Keccak256Hash(agreedPrestate) - f.L2Claim = disputedClaim - f.L2BlockNumber = claimTimestamp - f.DependencySet = depSet + step2Expected := system.Outputs.TransitionState(startTimestamp, 2, + system.Outputs.OptimisticBlockAtTimestamp(actors.ChainA, endTimestamp), + system.Outputs.OptimisticBlockAtTimestamp(actors.ChainB, endTimestamp), + ).Marshal() - for _, chain := range []*dsl.Chain{actors.ChainA, actors.ChainB} { - f.L2Sources = append(f.L2Sources, &fpHelpers.FaultProofProgramL2Source{ - Node: chain.Sequencer.L2Verifier, - Engine: chain.SequencerEngine, - ChainConfig: chain.L2Genesis.Config, - }) - } + paddingStep := func(step uint64) []byte { + return system.Outputs.TransitionState(startTimestamp, step, + system.Outputs.OptimisticBlockAtTimestamp(actors.ChainA, endTimestamp), + system.Outputs.OptimisticBlockAtTimestamp(actors.ChainB, endTimestamp), + ).Marshal() } -} -func assertTime(t helpers.Testing, chain *dsl.Chain, unsafe, crossUnsafe, localSafe, safe uint64) { - start := chain.L2Genesis.Timestamp - status := chain.Sequencer.SyncStatus() - require.Equal(t, start+unsafe, status.UnsafeL2.Time, "Unsafe") - require.Equal(t, start+crossUnsafe, status.CrossUnsafeL2.Time, "Cross Unsafe") - require.Equal(t, start+localSafe, status.LocalSafeL2.Time, "Local safe") - require.Equal(t, start+safe, status.SafeL2.Time, "Safe") -} + // Add one more block on each chain to setup challenger test cases that fetch a super root that's past the end timestamp + // This is necessary because on a 1-second block time, a new super root is created immediately after the end timestamp. + system.AdvanceSafeHeads() -type transitionTest struct { - name string - agreedClaim []byte - disputedClaim []byte - disputedTraceIndex int64 - l1Head common.Hash // Defaults to current L1 head if not set - proposalTimestamp uint64 // Defaults to latest L2 block timestamp if 0 - expectValid bool - skipProgram bool - skipChallenger bool + tests := []*transitionTest{ + { + name: "ClaimDirectToNextTimestamp", + agreedClaim: start.Marshal(), + disputedClaim: end.Marshal(), + startTimestamp: startTimestamp, + disputedTraceIndex: 0, + expectValid: false, + }, + { + name: "FirstChainOptimisticBlock", + agreedClaim: start.Marshal(), + disputedClaim: step1Expected, + startTimestamp: startTimestamp, + disputedTraceIndex: 0, + expectValid: true, + }, + { + name: "FirstChainOptimisticBlock-InvalidNoChange", + agreedClaim: start.Marshal(), + disputedClaim: start.Marshal(), + startTimestamp: startTimestamp, + disputedTraceIndex: 0, + expectValid: false, + }, + { + name: "SecondChainOptimisticBlock", + agreedClaim: step1Expected, + disputedClaim: step2Expected, + startTimestamp: startTimestamp, + disputedTraceIndex: 1, + expectValid: true, + }, + { + name: "SecondChainOptimisticBlock-InvalidNoChange", + agreedClaim: step1Expected, + disputedClaim: step1Expected, + startTimestamp: startTimestamp, + disputedTraceIndex: 1, + expectValid: false, + }, + { + name: "FirstPaddingStep", + agreedClaim: step2Expected, + disputedClaim: paddingStep(3), + startTimestamp: startTimestamp, + disputedTraceIndex: 2, + expectValid: true, + }, + { + name: "FirstPaddingStep-InvalidNoChange", + agreedClaim: step2Expected, + disputedClaim: step2Expected, + startTimestamp: startTimestamp, + disputedTraceIndex: 2, + expectValid: false, + }, + { + name: "SecondPaddingStep", + agreedClaim: paddingStep(3), + disputedClaim: paddingStep(4), + startTimestamp: startTimestamp, + disputedTraceIndex: 3, + expectValid: true, + }, + { + name: "SecondPaddingStep-InvalidNoChange", + agreedClaim: paddingStep(3), + disputedClaim: paddingStep(3), + startTimestamp: startTimestamp, + disputedTraceIndex: 3, + expectValid: false, + }, + { + name: "LastPaddingStep", + agreedClaim: paddingStep(consolidateStep - 1), + disputedClaim: paddingStep(consolidateStep), + startTimestamp: startTimestamp, + disputedTraceIndex: consolidateStep - 1, + expectValid: true, + }, + { + name: "Consolidate", + agreedClaim: paddingStep(consolidateStep), + disputedClaim: end.Marshal(), + startTimestamp: startTimestamp, + disputedTraceIndex: consolidateStep, + expectValid: true, + }, + { + // The proposed block timestamp is after the unsafe head block timestamp. + // With 1 second block time, we have reached the next block on chain A. + // But the next pending block is past the chain A's safe head, so we expect the transition to be invalid + name: "DisputeTimestampAfterChainHeadChainA", + agreedClaim: end.Marshal(), + l1Head: l1Head, + disputedClaim: interop.InvalidTransition, + startTimestamp: startTimestamp, + proposalTimestamp: endTimestamp + 100, + disputedTraceIndex: consolidateStep + 1, + expectValid: true, + }, + { + name: "DisputeTimestampAfterChainHeadConsolidate", + agreedClaim: system.Outputs.TransitionState(endTimestamp, consolidateStep, + system.Outputs.OptimisticBlockAtTimestamp(actors.ChainA, endTimestamp+1), + system.Outputs.OptimisticBlockAtTimestamp(actors.ChainB, endTimestamp+1), + ).Marshal(), + disputedClaim: system.Outputs.SuperRoot(endTimestamp + 1).Marshal(), + startTimestamp: startTimestamp, + proposalTimestamp: endTimestamp + 100, + disputedTraceIndex: 2*stepsPerTimestamp - 1, + expectValid: true, + }, + { + // With a 1 second block time on chain A, the implied agreed trace index references data past the l1 head. + // So the prestate transition is invalid. + name: "DisputeBlockAfterChainHead-FirstChain", + agreedClaim: interop.InvalidTransition, + l1Head: l1Head, + // Timestamp has advanced enough to expect the next block now, but it doesn't exit so transition to invalid + disputedClaim: interop.InvalidTransition, + startTimestamp: startTimestamp, + proposalTimestamp: endTimestamp + 100, + disputedTraceIndex: 2 * stepsPerTimestamp, + expectValid: true, + }, + { + // The agreed and disputed claim are both after the current chain head + name: "AgreedBlockAfterChainHead-Consolidate", + agreedClaim: interop.InvalidTransition, + disputedClaim: interop.InvalidTransition, + startTimestamp: startTimestamp, + l1Head: l1Head, + proposalTimestamp: endTimestamp + 100, + disputedTraceIndex: 4*stepsPerTimestamp - 1, + expectValid: true, + }, + { + // The agreed and disputed claim are both after the current chain head and disputing an optimistic block + name: "AgreedBlockAfterChainHead-Optimistic", + agreedClaim: interop.InvalidTransition, + disputedClaim: interop.InvalidTransition, + proposalTimestamp: endTimestamp + 100, + disputedTraceIndex: 4*stepsPerTimestamp + 1, + expectValid: true, + }, + + { + name: "FirstChainReachesL1Head", + agreedClaim: start.Marshal(), + disputedClaim: interop.InvalidTransition, + startTimestamp: startTimestamp, + proposalTimestamp: endTimestamp, + disputedTraceIndex: 0, + // The derivation reaches the L1 head before the next block can be created + l1Head: actors.L1Miner.L1Chain().Genesis().Hash(), + expectValid: true, + }, + { + // The transition from start to end timestamp only changes chain A, since it has a 1-second block time. + // So although the L1 head doesn't contain any chain B data, the next state is still valid because the proposed timestamp is still covered by chain B's head + name: "SecondChainReachesL1Head", + agreedClaim: step1Expected, + disputedClaim: step2Expected, + startTimestamp: startTimestamp, + proposalTimestamp: endTimestamp, + disputedTraceIndex: 1, + // The derivation reaches the L1 head before the next block can be created + l1Head: actors.L1Miner.L1Chain().GetCanonicalHash(1), + expectValid: true, + }, + { + name: "SuperRootInvalidIfUnsupportedByL1Data", + agreedClaim: start.Marshal(), + disputedClaim: step1Expected, + startTimestamp: startTimestamp, + proposalTimestamp: endTimestamp, + disputedTraceIndex: 0, + // The derivation reaches the L1 head before the next block can be created + l1Head: actors.L1Miner.L1Chain().Genesis().Hash(), + expectValid: false, + }, + { + name: "FromInvalidTransitionHash", + agreedClaim: interop.InvalidTransition, + disputedClaim: interop.InvalidTransition, + startTimestamp: startTimestamp, + proposalTimestamp: endTimestamp, + disputedTraceIndex: 2, + // The derivation reaches the L1 head before the next block can be created + l1Head: actors.L1Miner.L1Chain().Genesis().Hash(), + expectValid: true, + }, + } + + runFppAndChallengerTests(gt, system, tests) +} + +func TestInteropFaultProofs_VariedBlockTimes_FasterChainB(gt *testing.T) { + t := helpers.NewDefaultTesting(gt) + system := dsl.NewInteropDSL(t, dsl.SetBlockTimeForChainA(2), dsl.SetBlockTimeForChainB(1)) + actors := system.Actors + + system.AdvanceSafeHeads() + assertTime(t, actors.ChainA, 2, 2, 2, 2) + assertTime(t, actors.ChainB, 1, 1, 1, 1) + + endTimestamp := actors.ChainB.Sequencer.L2Safe().Time + startTimestamp := endTimestamp - 1 + + start := system.Outputs.SuperRoot(startTimestamp) + end := system.Outputs.SuperRoot(endTimestamp) + l1Head := actors.L1Miner.L1Chain().CurrentBlock().Hash() + + step1Expected := system.Outputs.TransitionState(startTimestamp, 1, + system.Outputs.OptimisticBlockAtTimestamp(actors.ChainA, endTimestamp), + ).Marshal() + + step2Expected := system.Outputs.TransitionState(startTimestamp, 2, + system.Outputs.OptimisticBlockAtTimestamp(actors.ChainA, endTimestamp), + system.Outputs.OptimisticBlockAtTimestamp(actors.ChainB, endTimestamp), + ).Marshal() + + paddingStep := func(step uint64) []byte { + return system.Outputs.TransitionState(startTimestamp, step, + system.Outputs.OptimisticBlockAtTimestamp(actors.ChainA, endTimestamp), + system.Outputs.OptimisticBlockAtTimestamp(actors.ChainB, endTimestamp), + ).Marshal() + } + + // Add one more block on each chain to setup challenger test cases that fetch a super root that's past the end timestamp + // This is necessary because on a 1-second block time, a new super root is created immediately after the end timestamp. + system.AdvanceSafeHeads() + + tests := []*transitionTest{ + { + name: "ClaimDirectToNextTimestamp", + agreedClaim: start.Marshal(), + disputedClaim: end.Marshal(), + startTimestamp: startTimestamp, + disputedTraceIndex: 0, + expectValid: false, + }, + { + name: "FirstChainOptimisticBlock", + agreedClaim: start.Marshal(), + disputedClaim: step1Expected, + startTimestamp: startTimestamp, + disputedTraceIndex: 0, + expectValid: true, + }, + { + name: "FirstChainOptimisticBlock-InvalidNoChange", + agreedClaim: start.Marshal(), + disputedClaim: start.Marshal(), + startTimestamp: startTimestamp, + disputedTraceIndex: 0, + expectValid: false, + }, + { + name: "SecondChainOptimisticBlock", + agreedClaim: step1Expected, + disputedClaim: step2Expected, + startTimestamp: startTimestamp, + disputedTraceIndex: 1, + expectValid: true, + }, + { + name: "SecondChainOptimisticBlock-InvalidNoChange", + agreedClaim: step1Expected, + disputedClaim: step1Expected, + startTimestamp: startTimestamp, + disputedTraceIndex: 1, + expectValid: false, + }, + { + name: "FirstPaddingStep", + agreedClaim: step2Expected, + disputedClaim: paddingStep(3), + startTimestamp: startTimestamp, + disputedTraceIndex: 2, + expectValid: true, + }, + { + name: "FirstPaddingStep-InvalidNoChange", + agreedClaim: step2Expected, + disputedClaim: step2Expected, + startTimestamp: startTimestamp, + disputedTraceIndex: 2, + expectValid: false, + }, + { + name: "SecondPaddingStep", + agreedClaim: paddingStep(3), + disputedClaim: paddingStep(4), + startTimestamp: startTimestamp, + disputedTraceIndex: 3, + expectValid: true, + }, + { + name: "SecondPaddingStep-InvalidNoChange", + agreedClaim: paddingStep(3), + disputedClaim: paddingStep(3), + startTimestamp: startTimestamp, + disputedTraceIndex: 3, + expectValid: false, + }, + { + name: "LastPaddingStep", + agreedClaim: paddingStep(consolidateStep - 1), + disputedClaim: paddingStep(consolidateStep), + startTimestamp: startTimestamp, + disputedTraceIndex: consolidateStep - 1, + expectValid: true, + }, + { + name: "Consolidate", + agreedClaim: paddingStep(consolidateStep), + disputedClaim: end.Marshal(), + startTimestamp: startTimestamp, + disputedTraceIndex: consolidateStep, + expectValid: true, + }, + { + // The proposed block timestamp is after the unsafe head block timestamp. + name: "DisputeTimestampAfterChainHeadChainA", + agreedClaim: end.Marshal(), + l1Head: l1Head, + // With 2 second block times, we haven't yet reached the next block on the first chain so it's still valid + disputedClaim: system.Outputs.TransitionState(endTimestamp, 1, + system.Outputs.OptimisticBlockAtTimestamp(actors.ChainA, endTimestamp+1), + ).Marshal(), + startTimestamp: startTimestamp, + proposalTimestamp: endTimestamp + 100, + disputedTraceIndex: consolidateStep + 1, + expectValid: true, + }, + { + name: "DisputeTimestampAfterChainHeadConsolidate", + agreedClaim: system.Outputs.TransitionState(endTimestamp, consolidateStep, + system.Outputs.OptimisticBlockAtTimestamp(actors.ChainA, endTimestamp+1), + system.Outputs.OptimisticBlockAtTimestamp(actors.ChainB, endTimestamp+1), + ).Marshal(), + disputedClaim: system.Outputs.SuperRoot(endTimestamp + 1).Marshal(), + startTimestamp: startTimestamp, + proposalTimestamp: endTimestamp + 100, + disputedTraceIndex: 2*stepsPerTimestamp - 1, + expectValid: true, + }, + { + // With a 1 second block time on chain A, the implied agreed trace index references data past the l1 head. + // So the prestate transition is invalid. + name: "DisputeBlockAfterChainHead-FirstChain", + agreedClaim: interop.InvalidTransition, + l1Head: l1Head, + // Timestamp has advanced enough to expect the next block now, but it doesn't exit so transition to invalid + disputedClaim: interop.InvalidTransition, + startTimestamp: startTimestamp, + proposalTimestamp: endTimestamp + 100, + disputedTraceIndex: 2 * stepsPerTimestamp, + expectValid: true, + }, + { + // The agreed and disputed claim are both after the current chain head + name: "AgreedBlockAfterChainHead-Consolidate", + agreedClaim: interop.InvalidTransition, + disputedClaim: interop.InvalidTransition, + startTimestamp: startTimestamp, + l1Head: l1Head, + proposalTimestamp: endTimestamp + 100, + disputedTraceIndex: 4*stepsPerTimestamp - 1, + expectValid: true, + }, + { + // The agreed and disputed claim are both after the current chain head and disputing an optimistic block + name: "AgreedBlockAfterChainHead-Optimistic", + agreedClaim: interop.InvalidTransition, + disputedClaim: interop.InvalidTransition, + proposalTimestamp: endTimestamp + 100, + disputedTraceIndex: 4*stepsPerTimestamp + 1, + expectValid: true, + }, + + { + // The transition from start to end timestamp only changes chain A, since it has a 1-second block time. + // So although the L1 head doesn't contain any chain B data, the next state is still valid because the proposed timestamp is still covered by chain B's head + name: "FirstChainReachesL1Head", + agreedClaim: start.Marshal(), + disputedClaim: step1Expected, + startTimestamp: startTimestamp, + proposalTimestamp: endTimestamp, + disputedTraceIndex: 0, + // The derivation reaches the L1 head before the next block can be created + l1Head: actors.L1Miner.L1Chain().Genesis().Hash(), + expectValid: true, + }, + { + name: "SecondChainReachesL1Head", + agreedClaim: step1Expected, + disputedClaim: interop.InvalidTransition, + startTimestamp: startTimestamp, + proposalTimestamp: endTimestamp, + disputedTraceIndex: 1, + // The derivation reaches the L1 head before the next block can be created + l1Head: actors.L1Miner.L1Chain().GetCanonicalHash(1), + expectValid: true, + }, + { + name: "FromInvalidTransitionHash", + agreedClaim: interop.InvalidTransition, + disputedClaim: interop.InvalidTransition, + startTimestamp: startTimestamp, + proposalTimestamp: endTimestamp, + disputedTraceIndex: 2, + // The derivation reaches the L1 head before the next block can be created + l1Head: actors.L1Miner.L1Chain().Genesis().Hash(), + expectValid: true, + }, + } + + runFppAndChallengerTests(gt, system, tests) +} + +func TestInteropFaultProofs_DepositMessage(gt *testing.T) { + t := helpers.NewDefaultTesting(gt) + + system := dsl.NewInteropDSL(t) + actors := system.Actors + emitter := system.DeployEmitterContracts() + + // Advance L1 a couple times to avoid deposit gas metering issues near genesis + system.AdvanceL1() + system.AdvanceL1() + + l1User := system.CreateUser() + depositMessage := dsl.NewMessage(system, actors.ChainA, emitter, "hello") + system.AdvanceL1( + dsl.WithActIncludeTx( + depositMessage.ActEmitDeposit(l1User))) + + // As such, the next block timestamp across both chains will contain a user-deposit message and an executing message + system.AdvanceL2ToLastBlockOfOrigin(actors.ChainA, 2) + system.AdvanceL2ToLastBlockOfOrigin(actors.ChainB, 2) + + actors.ChainA.Sequencer.ActL2StartBlock(t) + actors.ChainB.Sequencer.ActL2StartBlock(t) + // The pending block on chain A will contain the user deposit + depositMessage.ExecutePendingOn(actors.ChainB, actors.ChainA.Sequencer.L2Unsafe().Number+1) + actors.ChainA.Sequencer.ActL2EndBlock(t) + actors.ChainB.Sequencer.ActL2EndBlock(t) + system.SubmitBatchData(dsl.WithSkipCrossSafeUpdate()) + + endTimestamp := actors.ChainB.Sequencer.L2Unsafe().Time + startTimestamp := endTimestamp - 1 + preConsolidation := system.Outputs.TransitionState(startTimestamp, consolidateStep, + system.Outputs.OptimisticBlockAtTimestamp(actors.ChainA, endTimestamp), + system.Outputs.OptimisticBlockAtTimestamp(actors.ChainB, endTimestamp), + ).Marshal() + + system.ProcessCrossSafe() + depositMessage.CheckExecuted() + assertUserDepositEmitted(t, system.Actors.ChainA, nil, emitter) + crossSafeEnd := system.Outputs.SuperRoot(endTimestamp) + + tests := []*transitionTest{ + { + name: "Consolidate", + agreedClaim: preConsolidation, + disputedClaim: crossSafeEnd.Marshal(), + disputedTraceIndex: consolidateStep, + expectValid: true, + }, + { + name: "Consolidate-InvalidNoChange", + agreedClaim: preConsolidation, + disputedClaim: preConsolidation, + disputedTraceIndex: consolidateStep, + expectValid: false, + }, + } + runFppAndChallengerTests(gt, system, tests) +} + +func TestInteropFaultProofs_DepositMessage_InvalidExecution(gt *testing.T) { + t := helpers.NewDefaultTesting(gt) + + system := dsl.NewInteropDSL(t) + actors := system.Actors + emitter := system.DeployEmitterContracts() + + // Advance L1 a couple times to avoid deposit gas metering issues near genesis + system.AdvanceL1() + system.AdvanceL1() + + l1User := system.CreateUser() + depositMessage := dsl.NewMessage(system, actors.ChainA, emitter, "hello") + system.AdvanceL1( + dsl.WithActIncludeTx( + depositMessage.ActEmitDeposit(l1User))) + + // As such, the next block timestamp across both chains will contain a user-deposit message and an executing message + system.AdvanceL2ToLastBlockOfOrigin(actors.ChainA, 2) + system.AdvanceL2ToLastBlockOfOrigin(actors.ChainB, 2) + + actors.ChainA.Sequencer.ActL2StartBlock(t) + actors.ChainB.Sequencer.ActL2StartBlock(t) + // The pending block on chain A will contain the user deposit + depositMessage.ExecutePendingOn(actors.ChainB, + actors.ChainA.Sequencer.L2Unsafe().Number+1, + dsl.WithPayload([]byte("this message was never emitted")), + ) + actors.ChainA.Sequencer.ActL2EndBlock(t) + actors.ChainB.Sequencer.ActL2EndBlock(t) + system.SubmitBatchData(dsl.WithSkipCrossSafeUpdate()) + + endTimestamp := actors.ChainB.Sequencer.L2Unsafe().Time + startTimestamp := endTimestamp - 1 + optimisticEnd := system.Outputs.SuperRoot(endTimestamp) + + preConsolidation := system.Outputs.TransitionState(startTimestamp, consolidateStep, + system.Outputs.OptimisticBlockAtTimestamp(actors.ChainA, endTimestamp), + system.Outputs.OptimisticBlockAtTimestamp(actors.ChainB, endTimestamp), + ).Marshal() + + system.ProcessCrossSafe() + depositMessage.CheckNotExecuted() + assertUserDepositEmitted(t, system.Actors.ChainA, nil, emitter) + crossSafeEnd := system.Outputs.SuperRoot(endTimestamp) + + tests := []*transitionTest{ + { + name: "Consolidate", + agreedClaim: preConsolidation, + disputedClaim: crossSafeEnd.Marshal(), + disputedTraceIndex: consolidateStep, + expectValid: true, + }, + { + name: "Consolidate-InvalidNoChange", + agreedClaim: preConsolidation, + disputedClaim: preConsolidation, + disputedTraceIndex: consolidateStep, + expectValid: false, + }, + } + tests = append(tests, &transitionTest{ + name: "Consolidate-ExpectInvalidPendingBlock", + agreedClaim: preConsolidation, + disputedClaim: optimisticEnd.Marshal(), + disputedTraceIndex: consolidateStep, + expectValid: false, + }) + runFppAndChallengerTests(gt, system, tests) +} + +func runFppAndChallengerTests(gt *testing.T, system *dsl.InteropDSL, tests []*transitionTest) { + for _, test := range tests { + test := test + gt.Run(fmt.Sprintf("%s-fpp", test.name), func(gt *testing.T) { + runFppTest(gt, test, system.Actors, system.DepSet()) + }) + + gt.Run(fmt.Sprintf("%s-challenger", test.name), func(gt *testing.T) { + runChallengerTest(gt, test, system.Actors) + }) + } +} + +func runFppTest(gt *testing.T, test *transitionTest, actors *dsl.InteropActors, depSet *depset.StaticConfigDependencySet) { + t := helpers.NewDefaultTesting(gt) + if test.skipProgram { + t.Skip("Not yet implemented") + return + } + logger := testlog.Logger(t, slog.LevelInfo) + checkResult := fpHelpers.ExpectNoError() + if !test.expectValid { + checkResult = fpHelpers.ExpectError(claim.ErrClaimNotValid) + } + l1Head := test.l1Head + if l1Head == (common.Hash{}) { + l1Head = actors.L1Miner.L1Chain().CurrentBlock().Hash() + } + proposalTimestamp := test.proposalTimestamp + if proposalTimestamp == 0 { + proposalTimestamp = actors.ChainA.Sequencer.L2Unsafe().Time + } + fpHelpers.RunFaultProofProgram( + t, + logger, + actors.L1Miner, + checkResult, + WithInteropEnabled(t, actors, depSet, test.agreedClaim, crypto.Keccak256Hash(test.disputedClaim), proposalTimestamp), + fpHelpers.WithL1Head(l1Head), + ) +} + +func runChallengerTest(gt *testing.T, test *transitionTest, actors *dsl.InteropActors) { + t := helpers.NewDefaultTesting(gt) + if test.skipChallenger { + t.Skip("Not yet implemented") + return + } + logger := testlog.Logger(t, slog.LevelInfo) + endTimestamp := test.proposalTimestamp + if endTimestamp == 0 { + endTimestamp = actors.ChainA.Sequencer.L2Unsafe().Time + } + startTimestamp := test.startTimestamp + if startTimestamp == 0 { + startTimestamp = actors.ChainA.Sequencer.L2Unsafe().Time - 1 + } + prestateProvider := super.NewSuperRootPrestateProvider(actors.Supervisor, startTimestamp) + var l1Head eth.BlockID + if test.l1Head == (common.Hash{}) { + l1Head = eth.ToBlockID(eth.HeaderBlockInfo(actors.L1Miner.L1Chain().CurrentBlock())) + } else { + l1Head = eth.ToBlockID(actors.L1Miner.L1Chain().GetBlockByHash(test.l1Head)) + } + gameDepth := challengerTypes.Depth(30) + rollupCfgs, err := super.NewRollupConfigsFromParsed(actors.ChainA.RollupCfg, actors.ChainB.RollupCfg) + require.NoError(t, err) + provider := super.NewSuperTraceProvider(logger, rollupCfgs, prestateProvider, actors.Supervisor, l1Head, gameDepth, startTimestamp, endTimestamp) + var agreedPrestate []byte + if test.disputedTraceIndex > 0 { + agreedPrestate, err = provider.GetPreimageBytes(t.Ctx(), challengerTypes.NewPosition(gameDepth, big.NewInt(test.disputedTraceIndex-1))) + require.NoError(t, err) + } else { + superRoot, err := provider.AbsolutePreState(t.Ctx()) + require.NoError(t, err) + agreedPrestate = superRoot.Marshal() + } + require.Equal(t, test.agreedClaim, agreedPrestate, "agreed prestate mismatch") + + disputedClaim, err := provider.GetPreimageBytes(t.Ctx(), challengerTypes.NewPosition(gameDepth, big.NewInt(test.disputedTraceIndex))) + require.NoError(t, err) + if test.expectValid { + require.Equal(t, test.disputedClaim, disputedClaim, "Claim is correct so should match challenger's opinion") + } else { + require.NotEqual(t, test.disputedClaim, disputedClaim, "Claim is incorrect so should not match challenger's opinion") + } +} + +func WithInteropEnabled(t helpers.StatefulTesting, actors *dsl.InteropActors, depSet *depset.StaticConfigDependencySet, agreedPrestate []byte, disputedClaim common.Hash, claimTimestamp uint64) fpHelpers.FixtureInputParam { + return func(f *fpHelpers.FixtureInputs) { + f.InteropEnabled = true + f.AgreedPrestate = agreedPrestate + f.L2OutputRoot = crypto.Keccak256Hash(agreedPrestate) + f.L2Claim = disputedClaim + f.L2BlockNumber = claimTimestamp + f.DependencySet = depSet + + for _, chain := range []*dsl.Chain{actors.ChainA, actors.ChainB} { + f.L2Sources = append(f.L2Sources, &fpHelpers.FaultProofProgramL2Source{ + Node: chain.Sequencer.L2Verifier, + Engine: chain.SequencerEngine, + ChainConfig: chain.L2Genesis.Config, + }) + } + } +} + +func assertTime(t helpers.Testing, chain *dsl.Chain, unsafe, crossUnsafe, localSafe, safe uint64) { + start := chain.L2Genesis.Timestamp + status := chain.Sequencer.SyncStatus() + require.Equal(t, start+unsafe, status.UnsafeL2.Time, "Unsafe") + require.Equal(t, start+crossUnsafe, status.CrossUnsafeL2.Time, "Cross Unsafe") + require.Equal(t, start+localSafe, status.LocalSafeL2.Time, "Local safe") + require.Equal(t, start+safe, status.SafeL2.Time, "Safe") +} + +func assertUserDepositEmitted(t helpers.Testing, chain *dsl.Chain, number *big.Int, emitter *dsl.EmitterContract) { + block, err := chain.SequencerEngine.EthClient().BlockByNumber(t.Ctx(), number) + require.NoError(t, err) + require.GreaterOrEqual(t, len(block.Transactions()), 2) // l1-attrs + user-deposit + [txs] + userDepositTx := block.Transactions()[1] + require.NotNil(t, userDepositTx.To()) + require.Equal(t, emitter.Address(chain), *userDepositTx.To()) +} + +type transitionTest struct { + name string + agreedClaim []byte + disputedClaim []byte + disputedTraceIndex int64 + l1Head common.Hash // Defaults to current L1 head if not set + startTimestamp uint64 // Defaults to latest L2 block timestamp - 1 if 0 + proposalTimestamp uint64 // Defaults to latest L2 block timestamp if 0 + expectValid bool + skipProgram bool + skipChallenger bool +} + +type intraBlockTestCase interface { + // Setup is called to create a single-block test scenario + Setup(t helpers.StatefulTesting, system *dsl.InteropDSL, emitterContract *dsl.EmitterContract, actors *dsl.InteropActors) + // RunCrossSafeChecks is called after cross-safe updates are applied to the system + RunCrossSafeChecks(t helpers.StatefulTesting, system *dsl.InteropDSL, actors *dsl.InteropActors) +} + +type cascadeInvalidBlockCase struct { + msgA *dsl.Message + msgB *dsl.Message +} + +func (c *cascadeInvalidBlockCase) Setup(t helpers.StatefulTesting, system *dsl.InteropDSL, emitter *dsl.EmitterContract, actors *dsl.InteropActors) { + c.msgA = dsl.NewMessage(system, actors.ChainA, emitter, "chainA message"). + Emit(). + ExecuteOn(actors.ChainB, dsl.WithPayload([]byte("this message was never emitted"))) + // valid executing message on chain A, but is included in a cross-invalid block + c.msgB = dsl.NewMessage(system, actors.ChainB, emitter, "chainB message"). + Emit(). + ExecuteOn(actors.ChainA) +} + +func (c *cascadeInvalidBlockCase) RunCrossSafeChecks(t helpers.StatefulTesting, system *dsl.InteropDSL, actors *dsl.InteropActors) { + c.msgA.CheckNotEmitted() + c.msgA.CheckNotExecuted() + c.msgB.CheckEmitted() + c.msgB.CheckExecuted() +} + +type swapCascadeInvalidBlockCase struct { + cascadeInvalidBlockCase +} + +func (c *swapCascadeInvalidBlockCase) Setup(t helpers.StatefulTesting, system *dsl.InteropDSL, emitter *dsl.EmitterContract, actors *dsl.InteropActors) { + swap := *actors + chainA := swap.ChainA + swap.ChainA = swap.ChainB + swap.ChainB = chainA + c.cascadeInvalidBlockCase.Setup(t, system, emitter, &swap) +} + +type cyclicDependencyValidCase struct { + msgA *dsl.Message + msgB *dsl.Message +} + +func (c *cyclicDependencyValidCase) Setup(t helpers.StatefulTesting, system *dsl.InteropDSL, emitter *dsl.EmitterContract, actors *dsl.InteropActors) { + msgA := dsl.NewMessage(system, actors.ChainA, emitter, "hello") + msgA.Emit() + msgB := dsl.NewMessage(system, actors.ChainB, emitter, "world") + msgB.Emit() + + msgB.ExecuteOn(actors.ChainA) + msgA.ExecuteOn(actors.ChainB) + c.msgA = msgA + c.msgB = msgB +} + +func (c *cyclicDependencyValidCase) RunCrossSafeChecks(t helpers.StatefulTesting, system *dsl.InteropDSL, actors *dsl.InteropActors) { + assertHeads(t, actors.ChainA, 2, 2, 2, 2) + assertHeads(t, actors.ChainB, 2, 2, 2, 2) + c.msgA.CheckEmitted() + c.msgB.CheckEmitted() + c.msgA.CheckExecuted() + c.msgB.CheckExecuted() +} + +type cyclicDependencyInvalidCase struct { + execATx *dsl.GeneratedTransaction + execBTx *dsl.GeneratedTransaction +} + +func (c *cyclicDependencyInvalidCase) Setup(t helpers.StatefulTesting, system *dsl.InteropDSL, emitter *dsl.EmitterContract, actors *dsl.InteropActors) { + alice := system.CreateUser() + + // Create an exec message for chain B without including it + pendingBlockNumber := actors.ChainB.Sequencer.L2Unsafe().Number + 1 + pendingExecBOpts := dsl.WithPendingMessage(emitter, actors.ChainB, pendingBlockNumber, 0, "message from B") + + // Exec(A) -> Exec(B) -> Exec(A) + actExecA := system.InboxContract.Execute(alice, nil, pendingExecBOpts) + c.execATx = actExecA(actors.ChainA) + c.execATx.IncludeOK() + actExecB := system.InboxContract.Execute(alice, c.execATx) + c.execBTx = actExecB(actors.ChainB) + c.execBTx.IncludeOK() +} + +func (c *cyclicDependencyInvalidCase) RunCrossSafeChecks(t helpers.StatefulTesting, system *dsl.InteropDSL, actors *dsl.InteropActors) { + c.execATx.CheckNotIncluded() + c.execBTx.CheckNotIncluded() +} + +type longDependencyChainValidCase struct { + initTxA *dsl.GeneratedTransaction + execs []*dsl.GeneratedTransaction +} + +func (c *longDependencyChainValidCase) Setup(t helpers.StatefulTesting, system *dsl.InteropDSL, emitter *dsl.EmitterContract, actors *dsl.InteropActors) { + alice := system.CreateUser() + const depth = 10 + + // Exec(B_0) -> Exec(A_0) -> Exec(B_1) -> Exec(A_1) -> Exec(B_2) -> Exec(A_2) -> ... -> Init(A) + initTxA := emitter.EmitMessage(alice, "chain A")(actors.ChainA) + initTxA.IncludeOK() + + var execs []*dsl.GeneratedTransaction + + exec := system.InboxContract.Execute(alice, initTxA)(actors.ChainB) + exec.IncludeOK() + execs = append(execs, exec) + lastExecChain := actors.ChainB + for i := 1; i < depth; i++ { + if lastExecChain == actors.ChainA { + lastExecChain = actors.ChainB + } else { + lastExecChain = actors.ChainA + } + exec := system.InboxContract.Execute(alice, execs[i-1])(lastExecChain) + exec.IncludeOK() + execs = append(execs, exec) + } + + c.execs = execs + c.initTxA = initTxA +} + +func (c *longDependencyChainValidCase) RunCrossSafeChecks(t helpers.StatefulTesting, system *dsl.InteropDSL, actors *dsl.InteropActors) { + for _, exec := range c.execs { + exec.CheckIncluded() + } + c.initTxA.CheckIncluded() +} + +type sameChainMessageValidCase struct { + msg *dsl.Message +} + +func (c *sameChainMessageValidCase) Setup(t helpers.StatefulTesting, system *dsl.InteropDSL, emitter *dsl.EmitterContract, actors *dsl.InteropActors) { + msg := dsl.NewMessage(system, actors.ChainA, emitter, "hello") + msg.Emit() + msg.ExecuteOn(actors.ChainA) + c.msg = msg +} + +func (c *sameChainMessageValidCase) RunCrossSafeChecks(t helpers.StatefulTesting, system *dsl.InteropDSL, actors *dsl.InteropActors) { + c.msg.CheckEmitted() + c.msg.CheckExecuted() +} + +type sameChainMessageInvalidCase struct { + msg *dsl.Message +} + +func (c *sameChainMessageInvalidCase) Setup(t helpers.StatefulTesting, system *dsl.InteropDSL, emitter *dsl.EmitterContract, actors *dsl.InteropActors) { + msg := dsl.NewMessage(system, actors.ChainA, emitter, "hello") + msg.Emit() + msg.ExecuteOn(actors.ChainA, dsl.WithPayload([]byte("this message was never emitted"))) + c.msg = msg +} + +func (c *sameChainMessageInvalidCase) RunCrossSafeChecks(t helpers.StatefulTesting, system *dsl.InteropDSL, actors *dsl.InteropActors) { + c.msg.CheckNotEmitted() + c.msg.CheckNotExecuted() } diff --git a/op-e2e/actions/proofs/block_data_hint_test.go b/op-e2e/actions/proofs/block_data_hint_test.go index 3a08a54d19..8b82723626 100644 --- a/op-e2e/actions/proofs/block_data_hint_test.go +++ b/op-e2e/actions/proofs/block_data_hint_test.go @@ -23,7 +23,7 @@ import ( "github.com/stretchr/testify/require" ) -func Test_OPProgramAction_BlockDataHint(gt *testing.T) { +func Test_ProgramAction_BlockDataHint(gt *testing.T) { testCfg := &helpers.TestCfg[any]{ Hardfork: helpers.LatestFork, } diff --git a/op-e2e/actions/proofs/helpers/env.go b/op-e2e/actions/proofs/helpers/env.go index e347d6b124..d9490f2fd4 100644 --- a/op-e2e/actions/proofs/helpers/env.go +++ b/op-e2e/actions/proofs/helpers/env.go @@ -231,3 +231,13 @@ func NewOpProgramCfg( dfault.DependencySet = fi.DependencySet return dfault } + +// BatchAndMine batches the current unsafe chain to L1 and mines the L1 block containing the +// batcher transaction. +func (env *L2FaultProofEnv) BatchAndMine(t helpers.Testing) { + t.Helper() + env.Batcher.ActSubmitAll(t) + env.Miner.ActL1StartBlock(12)(t) + env.Miner.ActL1IncludeTxByHash(env.Batcher.LastSubmitted.Hash())(t) + env.Miner.ActL1EndBlock(t) +} diff --git a/op-e2e/actions/proofs/helpers/kona.go b/op-e2e/actions/proofs/helpers/kona.go index e826bf75c5..203c10ede5 100644 --- a/op-e2e/actions/proofs/helpers/kona.go +++ b/op-e2e/actions/proofs/helpers/kona.go @@ -56,11 +56,11 @@ func RunKonaNative( Server: konaHostPath, } inputs := utils.LocalGameInputs{ - L1Head: fixtureInputs.L1Head, - L2Head: fixtureInputs.L2Head, - L2OutputRoot: fixtureInputs.L2OutputRoot, - L2Claim: fixtureInputs.L2Claim, - L2BlockNumber: big.NewInt(int64(fixtureInputs.L2BlockNumber)), + L1Head: fixtureInputs.L1Head, + L2Head: fixtureInputs.L2Head, + L2OutputRoot: fixtureInputs.L2OutputRoot, + L2Claim: fixtureInputs.L2Claim, + L2SequenceNumber: big.NewInt(int64(fixtureInputs.L2BlockNumber)), } var hostCmd []string diff --git a/op-e2e/actions/proofs/helpers/matrix.go b/op-e2e/actions/proofs/helpers/matrix.go index 4f161531ce..44612f7c70 100644 --- a/op-e2e/actions/proofs/helpers/matrix.go +++ b/op-e2e/actions/proofs/helpers/matrix.go @@ -5,6 +5,8 @@ import ( "testing" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils" + "github.com/ethereum-optimism/optimism/op-program/client/claim" + "github.com/ethereum/go-ethereum/common" ) type RunTest[cfg any] func(t *testing.T, testCfg *TestCfg[cfg]) @@ -75,6 +77,36 @@ func (ts *TestMatrix[cfg]) AddTestCase( return ts } +func (ts *TestMatrix[cfg]) AddDefaultTestCases( + testCfg cfg, + forkMatrix ForkMatrix, + runTest RunTest[cfg], +) *TestMatrix[cfg] { + return ts.AddDefaultTestCasesWithName("", testCfg, forkMatrix, runTest) +} + +func (ts *TestMatrix[cfg]) AddDefaultTestCasesWithName( + name string, + testCfg cfg, + forkMatrix ForkMatrix, + runTest RunTest[cfg], +) *TestMatrix[cfg] { + return ts.AddTestCase( + "HonestClaim-"+name, + testCfg, + forkMatrix, + runTest, + ExpectNoError(), + ).AddTestCase( + "JunkClaim-"+name, + testCfg, + forkMatrix, + runTest, + ExpectError(claim.ErrClaimNotValid), + WithL2Claim(common.HexToHash("0xdeadbeef")), + ) +} + type Hardfork struct { Name string Precedence int diff --git a/op-e2e/actions/proofs/holocene_helpers_test.go b/op-e2e/actions/proofs/holocene_helpers_test.go index 4b19185fe3..d3c21616e3 100644 --- a/op-e2e/actions/proofs/holocene_helpers_test.go +++ b/op-e2e/actions/proofs/holocene_helpers_test.go @@ -21,6 +21,8 @@ type holoceneExpectations struct { } func (h holoceneExpectations) RequireExpectedProgressAndLogs(t actionsHelpers.StatefulTesting, actualSafeHead eth.L2BlockRef, isHolocene bool, engine *actionsHelpers.L2Engine, logs *testlog.CapturingHandler) { + t.Helper() + var exp expectations if isHolocene { exp = h.holocene @@ -28,16 +30,15 @@ func (h holoceneExpectations) RequireExpectedProgressAndLogs(t actionsHelpers.St exp = h.preHolocene } - require.Equal(t, exp.safeHead, actualSafeHead.Number) - expectedHash := engine.L2Chain().GetBlockByNumber(exp.safeHead).Hash() - require.Equal(t, expectedHash, actualSafeHead.Hash) + require.Equal(t, exp.safeHead, actualSafeHead.Number, "safe head: wrong number") + expectedHash := engine.L2Chain().GetHeaderByNumber(exp.safeHead).Hash() + require.Equal(t, expectedHash, actualSafeHead.Hash, "safe head: wrong hash") for _, l := range exp.logs { t.Helper() recs := logs.FindLogs(testlog.NewMessageContainsFilter(l.filter), testlog.NewAttributesFilter("role", l.role)) - require.Len(t, recs, l.num, "searching for %d instances of '%s' in logs from role %s", l.num, l.filter, l.role) + require.Len(t, recs, l.num, "searching for %d instances of %q in logs from role %s", l.num, l.filter, l.role) } - } func sequencerOnce(filter string) []logExpectations { diff --git a/op-e2e/actions/proofs/holocene_invalid_batch_test.go b/op-e2e/actions/proofs/holocene_invalid_batch_test.go index fc73a252e8..16fea61518 100644 --- a/op-e2e/actions/proofs/holocene_invalid_batch_test.go +++ b/op-e2e/actions/proofs/holocene_invalid_batch_test.go @@ -70,10 +70,12 @@ func Test_ProgramAction_HoloceneInvalidBatch(gt *testing.T) { name: "invalid-payload", blocks: []uint{1, 2, 3}, blockModifiers: []actionsHelpers.BlockModifier{nil, invalidPayload, nil}, useSpanBatch: false, holoceneExpectations: holoceneExpectations{ - preHolocene: expectations{safeHead: 1, // Invalid signature in block 2 causes an invalid _payload_ in the engine queue. Entire span batch is invalidated. - logs: sequencerOnce("could not process payload attributes"), + preHolocene: expectations{ + safeHead: 1, // Invalid signature in block 2 causes an invalid _payload_ in the engine queue. Entire span batch is invalidated. + logs: sequencerOnce("could not process payload attributes"), }, - holocene: expectations{safeHead: 2, // We expect the safe head to move to 2 due to creation of a deposit-only block. + holocene: expectations{ + safeHead: 2, // We expect the safe head to move to 2 due to creation of a deposit-only block. logs: append( sequencerOnce("Holocene active, requesting deposits-only attributes"), sequencerOnce("could not process payload attributes")..., @@ -85,12 +87,14 @@ func Test_ProgramAction_HoloceneInvalidBatch(gt *testing.T) { name: "invalid-payload-span", blocks: []uint{1, 2, 3}, blockModifiers: []actionsHelpers.BlockModifier{nil, invalidPayload, nil}, useSpanBatch: true, holoceneExpectations: holoceneExpectations{ - preHolocene: expectations{safeHead: 0, // Invalid signature in block 2 causes an invalid _payload_ in the engine queue. Entire span batch is invalidated. - logs: sequencerOnce("could not process payload attributes"), + preHolocene: expectations{ + safeHead: 0, // Invalid signature in block 2 causes an invalid _payload_ in the engine queue. Entire span batch is invalidated. + logs: sequencerOnce("could not process payload attributes"), }, - holocene: expectations{safeHead: 2, // We expect the safe head to move to 2 due to creation of an deposit-only block. - logs: sequencerOnce("could not process payload attributes"), + holocene: expectations{ + safeHead: 2, // We expect the safe head to move to 2 due to creation of an deposit-only block. + logs: sequencerOnce("could not process payload attributes"), }, }, }, @@ -109,11 +113,13 @@ func Test_ProgramAction_HoloceneInvalidBatch(gt *testing.T) { useSpanBatch: true, breachMaxSequencerDrift: true, holoceneExpectations: holoceneExpectations{ - preHolocene: expectations{safeHead: 0, // Entire span batch invalidated. - logs: sequencerOnce("batch exceeded sequencer time drift without adopting next origin, and next L1 origin would have been valid"), + preHolocene: expectations{ + safeHead: 0, // Entire span batch invalidated. + logs: sequencerOnce("batch exceeded sequencer time drift, sequencer must adopt new L1 origin to include transactions again"), }, - holocene: expectations{safeHead: 1800, // We expect partial validity until we hit sequencer drift. - logs: sequencerOnce("batch exceeded sequencer time drift without adopting next origin, and next L1 origin would have been valid"), + holocene: expectations{ + safeHead: 1800, // We expect partial validity until we hit sequencer drift. + logs: sequencerOnce("batch exceeded sequencer time drift, sequencer must adopt new L1 origin to include transactions again"), }, }, }, @@ -123,11 +129,13 @@ func Test_ProgramAction_HoloceneInvalidBatch(gt *testing.T) { useSpanBatch: true, overAdvanceL1Origin: 3, // this will over-advance the L1 origin of block 3 holoceneExpectations: holoceneExpectations{ - preHolocene: expectations{safeHead: 0, // Entire span batch invalidated. - logs: sequencerOnce("block timestamp is less than L1 origin timestamp"), + preHolocene: expectations{ + safeHead: 0, // Entire span batch invalidated. + logs: sequencerOnce("block timestamp is less than L1 origin timestamp"), }, - holocene: expectations{safeHead: 2, // We expect partial validity, safe head should move to block 2, dropping invalid block 3 and remaining channel. - logs: sequencerOnce("batch timestamp is less than L1 origin timestamp"), + holocene: expectations{ + safeHead: 2, // We expect partial validity, safe head should move to block 2, dropping invalid block 3 and remaining channel. + logs: sequencerOnce("batch timestamp is less than L1 origin timestamp"), }, }, }, diff --git a/op-e2e/actions/proofs/isthmus_requests_test.go b/op-e2e/actions/proofs/isthmus_requests_test.go index 847685519e..26efe63dba 100644 --- a/op-e2e/actions/proofs/isthmus_requests_test.go +++ b/op-e2e/actions/proofs/isthmus_requests_test.go @@ -16,7 +16,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestIsthmusExcludedPredeploys(gt *testing.T) { +func Test_ProgramAction_IsthmusExcludedPredeploys(gt *testing.T) { // Ensures that if EIP-7251, or EIP-7002 predeploys are deployed manually after the fork, // Isthmus block processing still works correctly. Also ensures that if requests are sent to these // contracts, they are not processed and do not show up in the block body or requests hash. diff --git a/op-e2e/actions/proofs/isthmus_setcode_tx_test.go b/op-e2e/actions/proofs/isthmus_setcode_tx_test.go new file mode 100644 index 0000000000..b78a8754ee --- /dev/null +++ b/op-e2e/actions/proofs/isthmus_setcode_tx_test.go @@ -0,0 +1,191 @@ +package proofs_test + +import ( + "bytes" + "math/rand" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm/program" + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" + + actionsHelpers "github.com/ethereum-optimism/optimism/op-e2e/actions/helpers" + "github.com/ethereum-optimism/optimism/op-e2e/actions/proofs/helpers" + "github.com/ethereum-optimism/optimism/op-service/testlog" + "github.com/ethereum-optimism/optimism/op-service/testutils" +) + +func Test_ProgramAction_SetCodeTx(gt *testing.T) { + matrix := helpers.NewMatrix[any]() + defer matrix.Run(gt) + + matrix.AddDefaultTestCases( + nil, + helpers.LatestForkOnly, + runSetCodeTxTypeTest, + ) +} + +func runSetCodeTxTypeTest(gt *testing.T, testCfg *helpers.TestCfg[any]) { + var ( + aa = common.HexToAddress("0x000000000000000000000000000000000000aaaa") + bb = common.HexToAddress("0x000000000000000000000000000000000000bbbb") + ) + + t := actionsHelpers.NewDefaultTesting(gt) + + // hardcoded because it's not available until after we need it + bobAddr := common.HexToAddress("0x14dC79964da2C08b23698B3D3cc7Ca32193d9955") + + // Create 2 contracts, (1) writes 42 to slot 42, (2) calls (1) + store42Program := program.New().Sstore(0x42, 0x42) + callBobProgram := program.New().Call(nil, bobAddr, 1, 0, 0, 0, 0) + + alloc := *actionsHelpers.DefaultAlloc + alloc.L2Alloc = make(map[common.Address]types.Account) + alloc.L2Alloc[aa] = types.Account{ + Code: store42Program.Bytes(), + } + alloc.L2Alloc[bb] = types.Account{ + Code: callBobProgram.Bytes(), + } + + testCfg.Allocs = &alloc + + tp := helpers.NewTestParams() + env := helpers.NewL2FaultProofEnv(t, testCfg, tp, helpers.NewBatcherCfg()) + require.Equal(gt, env.Bob.Address(), bobAddr) + + cl := env.Engine.EthClient() + + env.Sequencer.ActL2PipelineFull(t) + env.Miner.ActEmptyBlock(t) + env.Sequencer.ActL2StartBlock(t) + + aliceSecret := env.Alice.L2.Secret() + bobSecret := env.Bob.L2.Secret() + + chainID := env.Sequencer.RollupCfg.L2ChainID + + // Sign authorization tuples. + // The way the auths are combined, it becomes + // 1. tx -> addr1 which is delegated to 0xaaaa + // 2. addr1:0xaaaa calls into addr2:0xbbbb + // 3. addr2:0xbbbb writes to storage + auth1, err := types.SignSetCode(aliceSecret, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(chainID), + Address: bb, + Nonce: 1, + }) + require.NoError(gt, err, "failed to sign auth1") + auth2, err := types.SignSetCode(bobSecret, types.SetCodeAuthorization{ + Address: aa, + Nonce: 0, + }) + require.NoError(gt, err, "failed to sign auth2") + + txdata := &types.SetCodeTx{ + ChainID: uint256.MustFromBig(chainID), + Nonce: 0, + To: env.Alice.Address(), + Gas: 500000, + GasFeeCap: uint256.NewInt(5000000000), + GasTipCap: uint256.NewInt(2), + AuthList: []types.SetCodeAuthorization{auth1, auth2}, + } + signer := types.NewIsthmusSigner(chainID) + tx := types.MustSignNewTx(aliceSecret, signer, txdata) + + err = cl.SendTransaction(t.Ctx(), tx) + require.NoError(gt, err, "failed to send set code tx") + + _, err = env.Engine.EngineApi.IncludeTx(tx, env.Alice.Address()) + require.NoError(t, err, "failed to include set code tx") + + env.Sequencer.ActL2EndBlock(t) + + // Verify delegation designations were deployed. + bobCode, err := cl.PendingCodeAt(t.Ctx(), env.Bob.Address()) + require.NoError(gt, err, "failed to get bob code") + want := types.AddressToDelegation(auth2.Address) + if !bytes.Equal(bobCode, want) { + t.Fatalf("addr1 code incorrect: got %s, want %s", common.Bytes2Hex(bobCode), common.Bytes2Hex(want)) + } + aliceCode, err := cl.PendingCodeAt(t.Ctx(), env.Alice.Address()) + require.NoError(gt, err, "failed to get alice code") + want = types.AddressToDelegation(auth1.Address) + if !bytes.Equal(aliceCode, want) { + t.Fatalf("addr2 code incorrect: got %s, want %s", common.Bytes2Hex(aliceCode), common.Bytes2Hex(want)) + } + + // Verify delegation executed the correct code. + fortyTwo := common.BytesToHash([]byte{0x42}) + actual, err := cl.PendingStorageAt(t.Ctx(), env.Bob.Address(), fortyTwo) + require.NoError(gt, err, "failed to get addr1 storage") + + if !bytes.Equal(actual, fortyTwo[:]) { + t.Fatalf("addr2 storage wrong: expected %d, got %d", fortyTwo, actual) + } + + // batch submit to L1. batcher should submit span batches. + env.BatchAndMine(t) + + env.Sequencer.ActL1HeadSignal(t) + env.Sequencer.ActL2PipelineFull(t) + + latestBlock, err := cl.BlockByNumber(t.Ctx(), nil) + require.NoError(t, err, "error fetching latest block") + + env.RunFaultProofProgram(t, latestBlock.NumberU64(), testCfg.CheckResult, testCfg.InputParams...) +} + +// TestInvalidSetCodeTxBatch tests that batches that include SetCodeTxs are dropped before Isthmus +func Test_ProgramAction_InvalidSetCodeTxBatch(gt *testing.T) { + matrix := helpers.NewMatrix[any]() + matrix.AddDefaultTestCases( + nil, + helpers.NewForkMatrix(helpers.Holocene), + testInvalidSetCodeTxBatch, + ) + matrix.Run(gt) +} + +func testInvalidSetCodeTxBatch(gt *testing.T, testCfg *helpers.TestCfg[any]) { + t := actionsHelpers.NewDefaultTesting(gt) + env := helpers.NewL2FaultProofEnv(t, testCfg, helpers.NewTestParams(), helpers.NewBatcherCfg()) + sequencer := env.Sequencer + miner := env.Miner + batcher := env.Batcher + + sequencer.ActL2EmptyBlock(t) + u1 := sequencer.L2Unsafe() + sequencer.ActL2EmptyBlock(t) // we'll inject the setcode tx in this block's batch + + rng := rand.New(rand.NewSource(0)) + setcodetx := testutils.RandomSetCodeTx(rng, types.NewPragueSigner(env.Sd.RollupCfg.L2ChainID)) + batcher.ActL2BatchBuffer(t) + batcher.ActL2BatchBuffer(t, func(block *types.Block) *types.Block { + // inject user tx into upgrade batch + return block.WithBody(types.Body{Transactions: append(block.Transactions(), setcodetx)}) + }) + batcher.ActL2ChannelClose(t) + batcher.ActL2BatchSubmit(t) + miner.ActL1StartBlock(12)(t) + miner.ActL1IncludeTxByHash(env.Batcher.LastSubmitted.Hash())(t) + miner.ActL1EndBlock(t) + + sequencer.ActL1HeadSignal(t) + sequencer.ActL2PipelineFull(t) + + l2safe := sequencer.L2Safe() + s2block := env.Engine.L2Chain().GetBlockByHash(l2safe.Hash) + require.Len(t, s2block.Transactions(), 1, "safe head should only contain l1 info deposit") + require.Equal(t, u1, l2safe, "expected last block to be reorgd out due to setcode tx") + + recs := env.Logs.FindLogs(testlog.NewMessageFilter("sequencers may not embed any SetCode transactions before Isthmus")) + require.Len(t, recs, 1) + + env.RunFaultProofProgram(t, l2safe.Number, testCfg.CheckResult, testCfg.InputParams...) +} diff --git a/op-e2e/actions/proofs/l1_prague_fork_test.go b/op-e2e/actions/proofs/l1_prague_fork_test.go new file mode 100644 index 0000000000..7b00a5a037 --- /dev/null +++ b/op-e2e/actions/proofs/l1_prague_fork_test.go @@ -0,0 +1,186 @@ +package proofs_test + +import ( + "testing" + + batcherFlags "github.com/ethereum-optimism/optimism/op-batcher/flags" + "github.com/ethereum-optimism/optimism/op-chain-ops/genesis" + actionsHelpers "github.com/ethereum-optimism/optimism/op-e2e/actions/helpers" + "github.com/ethereum-optimism/optimism/op-e2e/actions/proofs/helpers" + legacybindings "github.com/ethereum-optimism/optimism/op-e2e/bindings" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/predeploys" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/consensus/misc/eip4844" + "github.com/ethereum/go-ethereum/core/types" + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" +) + +func Test_ProgramAction_PragueForkAfterGenesis(gt *testing.T) { + type testCase struct { + name string + useSetCodeTx bool + } + + dynamiceFeeCase := testCase{ + name: "dynamicFeeTx", useSetCodeTx: false, + } + setCodeCase := testCase{ + name: "setCodeTx", useSetCodeTx: true, + } + + runL1PragueTest := func(gt *testing.T, testCfg *helpers.TestCfg[testCase]) { + t := actionsHelpers.NewDefaultTesting(gt) + env := helpers.NewL2FaultProofEnv(t, testCfg, helpers.NewTestParams(), + helpers.NewBatcherCfg( + func(c *actionsHelpers.BatcherCfg) { + c.DataAvailabilityType = batcherFlags.CalldataType + }, + ), + func(dp *genesis.DeployConfig) { + dp.L1PragueTimeOffset = ptr(hexutil.Uint64(24)) // Activate at second l1 block + }, + ) + + miner, batcher, verifier, sequencer, engine := env.Miner, env.Batcher, env.Sequencer, env.Sequencer, env.Engine + + l1Block, err := legacybindings.NewL1Block(predeploys.L1BlockAddr, engine.EthClient()) + require.NoError(t, err) + + // utils + checkVerifierDerivedToL1Head := func(t actionsHelpers.StatefulTesting) { + l1Head := miner.L1Chain().CurrentBlock() + currentL1 := verifier.SyncStatus().CurrentL1 + require.Equal(t, l1Head.Number.Int64(), int64(currentL1.Number), "verifier should derive up to and including the L1 head") + require.Equal(t, l1Head.Hash(), currentL1.Hash, "verifier should derive up to and including the L1 head") + } + + buildUnsafeL2AndSubmit := func(useSetCode bool) { + sequencer.ActL1HeadSignal(t) + sequencer.ActBuildToL1Head(t) + + miner.ActL1StartBlock(12)(t) + if useSetCode { + batcher.ActBufferAll(t) + batcher.ActL2ChannelClose(t) + batcher.ActSubmitSetCodeTx(t) + } else { + batcher.ActSubmitAll(t) + } + miner.ActL1IncludeTx(batcher.BatcherAddr)(t) + miner.ActL1EndBlock(t) + } + + requirePragueStatusOnL1 := func(active bool, block *types.Header) { + if active { + require.True(t, env.Sd.L1Cfg.Config.IsPrague(block.Number, block.Time), "Prague should be active at block", block.Number.Uint64()) + require.NotNil(t, block.RequestsHash, "Prague header requests hash should be non-nil") + } else { + require.False(t, env.Sd.L1Cfg.Config.IsPrague(block.Number, block.Time), "Prague should not be active yet at block", block.Number.Uint64()) + require.Nil(t, block.RequestsHash, "Prague header requests hash should be nil") + } + } + + syncVerifierAndCheck := func(t actionsHelpers.StatefulTesting) { + verifier.ActL1HeadSignal(t) + verifier.ActL2PipelineFull(t) + checkVerifierDerivedToL1Head(t) + } + + checkL1BlockBlobBaseFee := func(t actionsHelpers.StatefulTesting, l2Block eth.L2BlockRef) { + l1BlockID := l2Block.L1Origin + l1BlockHeader := miner.L1Chain().GetHeaderByHash(l1BlockID.Hash) + expectedBbf := eth.CalcBlobFeeDefault(l1BlockHeader) + upstreamExpectedBbf := eip4844.CalcBlobFee(env.Sd.L1Cfg.Config, l1BlockHeader) + require.Equal(t, expectedBbf.Uint64(), upstreamExpectedBbf.Uint64(), "expected blob base fee should match upstream calculation") + bbf, err := l1Block.BlobBaseFee(&bind.CallOpts{BlockHash: l2Block.Hash}) + require.NoError(t, err, "failed to get blob base fee") + require.Equal(t, expectedBbf.Uint64(), bbf.Uint64(), "l1Block blob base fee does not match expectation, l1BlockNum %d, l2BlockNum %d", l1BlockID.Number, l2Block.Number) + } + + requireSafeHeadProgression := func(t actionsHelpers.StatefulTesting, safeL2Before, safeL2After eth.L2BlockRef, batchedWithSetCodeTx bool) { + if batchedWithSetCodeTx { + require.Equal(t, safeL2Before, safeL2After, "safe head should not have changed (SetCode / type 4 batcher tx ignored)") + require.Equal(t, safeL2Before.L1Origin.Number, safeL2After.Number, "l1 origin of l2 safe should not have changed (SetCode / type 4 batcher tx ignored)") + } else { + require.Greater(t, safeL2After.Number, safeL2Before.Number, "safe head should have progressed (DynamicFee / type 2 batcher tx derived from)") + require.Equal(t, verifier.SyncStatus().UnsafeL2.Number, safeL2After.Number, "safe head should equal unsafe head (DynamicFee / type 2 batcher tx derived from)") + require.Greater(t, safeL2After.L1Origin.Number, safeL2Before.L1Origin.Number, "l1 origin of l2 safe should have progressed (DynamicFee / type 2 batcher tx tx derived from)") + } + } + + // Check initially Prague is not activated + requirePragueStatusOnL1(false, miner.L1Chain().CurrentBlock()) + + // Start op-nodes + sequencer.ActL2PipelineFull(t) + verifier.ActL2PipelineFull(t) + + // Build L1 blocks, crossing the fork boundary + miner.ActEmptyBlock(t) // block 1 + miner.ActEmptyBlock(t) // Prague activates here (block 2) + + // Here's a block with a type 4 deposit transaction, sent to the OptimismPortal + miner.ActL1StartBlock(12)(t) // block 3 + tx, err := actionsHelpers.PrepareSignedSetCodeTx( + *uint256.MustFromBig(env.Sd.L1Cfg.Config.ChainID), + env.Dp.Secrets.Alice, + env.Alice.L1.Signer(), + env.Alice.L1.PendingNonce(t), // nonce + env.Sd.DeploymentsL1.OptimismPortalProxy, + []byte{}) + require.NoError(t, err, "failed to prepare set code tx") + err = miner.EthClient().SendTransaction(t.Ctx(), tx) + require.NoError(t, err, "failed to send set code tx") + miner.ActL1IncludeTx(env.Alice.Address())(t) + miner.ActL1EndBlock(t) + + // Check that Prague is active on L1 + requirePragueStatusOnL1(true, miner.L1Chain().CurrentBlock()) + + // Cache safe head before verifier sync + safeL2Initial := verifier.SyncStatus().SafeL2 + + // Build an empty L2 block which has a pre-prague L1 origin, and check the blob fee is correct + sequencer.ActL2EmptyBlock(t) + l1OriginHeader := miner.L1Chain().GetHeaderByHash(verifier.SyncStatus().UnsafeL2.L1Origin.Hash) + requirePragueStatusOnL1(false, l1OriginHeader) + checkL1BlockBlobBaseFee(t, verifier.SyncStatus().UnsafeL2) + + // Build L2 unsafe chain and batch it to L1 using either DynamicFee or + // EIP-7702 SetCode txs + // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7702.md + buildUnsafeL2AndSubmit(testCfg.Custom.useSetCodeTx) + + // Check verifier derived from Prague L1 blocks + syncVerifierAndCheck(t) + + // Check safe head did or did not change, + // depending on tx type used by batcher: + safeL2AfterFirstBatch := verifier.SyncStatus().SafeL2 + requireSafeHeadProgression(t, safeL2Initial, safeL2AfterFirstBatch, testCfg.Custom.useSetCodeTx) + + sequencer.ActBuildToL1Head(t) // Advance L2 chain until L1 origin has Prague active + + // Check that the l1 origin is now a Prague block, and that the blob fee is correct + l1Origin := miner.L1Chain().GetHeaderByNumber(verifier.SyncStatus().UnsafeL2.L1Origin.Number) + requirePragueStatusOnL1(true, l1Origin) + checkL1BlockBlobBaseFee(t, verifier.SyncStatus().UnsafeL2) + + // Batch and sync again + buildUnsafeL2AndSubmit(testCfg.Custom.useSetCodeTx) + syncVerifierAndCheck(t) + safeL2AfterSecondBatch := verifier.SyncStatus().SafeL2 + requireSafeHeadProgression(t, safeL2AfterFirstBatch, safeL2AfterSecondBatch, testCfg.Custom.useSetCodeTx) + + env.RunFaultProofProgram(t, safeL2AfterSecondBatch.Number, testCfg.CheckResult, testCfg.InputParams...) + } + + matrix := helpers.NewMatrix[testCase]() + defer matrix.Run(gt) + matrix. + AddDefaultTestCasesWithName(dynamiceFeeCase.name, dynamiceFeeCase, helpers.NewForkMatrix(helpers.Holocene, helpers.LatestFork), runL1PragueTest). + AddDefaultTestCasesWithName(setCodeCase.name, setCodeCase, helpers.NewForkMatrix(helpers.Holocene, helpers.LatestFork), runL1PragueTest) +} diff --git a/op-e2e/actions/proofs/operator_fee_test.go b/op-e2e/actions/proofs/operator_fee_test.go index 476d50b085..cfc4fa63db 100644 --- a/op-e2e/actions/proofs/operator_fee_test.go +++ b/op-e2e/actions/proofs/operator_fee_test.go @@ -14,7 +14,7 @@ import ( "github.com/stretchr/testify/require" ) -func Test_Operator_Fee_Constistency(gt *testing.T) { +func Test_ProgramAction_OperatorFeeConstistency(gt *testing.T) { const testOperatorFeeScalar = uint32(20000) const testOperatorFeeConstant = uint64(500) diff --git a/op-e2e/actions/proofs/pectra_blob_schedule_test.go b/op-e2e/actions/proofs/pectra_blob_schedule_test.go new file mode 100644 index 0000000000..a6b1750630 --- /dev/null +++ b/op-e2e/actions/proofs/pectra_blob_schedule_test.go @@ -0,0 +1,136 @@ +package proofs_test + +import ( + "testing" + + "github.com/ethereum-optimism/optimism/op-chain-ops/genesis" + actionsHelpers "github.com/ethereum-optimism/optimism/op-e2e/actions/helpers" + "github.com/ethereum-optimism/optimism/op-e2e/actions/proofs/helpers" + legacybindings "github.com/ethereum-optimism/optimism/op-e2e/bindings" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/predeploys" + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +type pectraBlobScheduleTestCfg struct { + offset *uint64 + expectCancunBBF bool +} + +func Test_ProgramAction_PectraBlobSchedule(gt *testing.T) { + matrix := helpers.NewMatrix[any]() + defer matrix.Run(gt) + + matrix.AddDefaultTestCases( + // aligned with an L1 timestamp + pectraBlobScheduleTestCfg{ptr(uint64(24)), true}, + helpers.NewForkMatrix(helpers.Holocene), + testPectraBlobSchedule, + ).AddDefaultTestCases( + // in the middle between two L1 timestamps + pectraBlobScheduleTestCfg{ptr(uint64(18)), true}, + helpers.NewForkMatrix(helpers.Holocene), + testPectraBlobSchedule, + ).AddDefaultTestCases( + pectraBlobScheduleTestCfg{nil, false}, + helpers.NewForkMatrix(helpers.Holocene), + testPectraBlobSchedule, + ) +} + +func testPectraBlobSchedule(gt *testing.T, testCfg *helpers.TestCfg[any]) { + tcfg := testCfg.Custom.(pectraBlobScheduleTestCfg) // two flavors of this test + t := actionsHelpers.NewDefaultTesting(gt) + testSetup := func(dc *genesis.DeployConfig) { + dc.L1PragueTimeOffset = ptr(hexutil.Uint64(0)) + dc.L2GenesisPectraBlobScheduleTimeOffset = (*hexutil.Uint64)(tcfg.offset) + // set genesis excess blob gas so there are >0 blob base fees for some blocks + dc.L1GenesisBlockExcessBlobGas = ptr(hexutil.Uint64(1e8)) + } + + env := helpers.NewL2FaultProofEnv(t, testCfg, helpers.NewTestParams(), helpers.NewBatcherCfg(), testSetup) + + // sanity check + if tcfg.offset != nil { + require.Equal(t, *env.Sd.RollupCfg.PectraBlobScheduleTime, env.Sd.L2Cfg.Timestamp+*tcfg.offset) + } + + engine := env.Engine + sequencer := env.Sequencer + miner := env.Miner + + l1_0 := miner.L1Chain().CurrentHeader() + t.Logf("Header0: Number: %v, Time: %v, ExcessBlobGas: %v", l1_0.Number, l1_0.Time, *l1_0.ExcessBlobGas) + require.NotZero(t, *l1_0.ExcessBlobGas) + require.Equal(t, env.Sd.L2Cfg.Timestamp, l1_0.Time, "we assume L1 and L2 genesis have the same time") + + ethCl := engine.EthClient() + l1Block, err := legacybindings.NewL1Block(predeploys.L1BlockAddr, ethCl) + require.NoError(t, err) + + miner.ActEmptyBlock(t) + l1_1 := miner.L1Chain().CurrentHeader() + t.Logf("Header1: Number: %v, Time: %v, ExcessBlobGas: %v", l1_1.Number, l1_1.Time, *l1_1.ExcessBlobGas) + if tcfg.offset != nil { + require.Less(t, l1_1.Time, *env.Sd.RollupCfg.PectraBlobScheduleTime) + } + + sequencer.ActL1HeadSignal(t) + sequencer.ActBuildToL1HeadUnsafe(t) + + cancunBBF1 := eth.CalcBlobFeeCancun(*l1_1.ExcessBlobGas) + pragueBBF1 := eth.CalcBlobFeeDefault(l1_1) + // Make sure they differ. + require.Less(t, pragueBBF1.Uint64(), cancunBBF1.Uint64()) + opts := &bind.CallOpts{} + bbf1, err := l1Block.BlobBaseFee(opts) + require.NoError(t, err) + t.Logf("BlobBaseFee1: %v", bbf1) + // This is the critical assertion of this test. With the PectraBlobSchedule set, the blob + // base fee is still calculated using the Cancun schedule, without it with the same as the + // Prague schedule of L1. + if tcfg.expectCancunBBF { + require.Equal(t, cancunBBF1, bbf1) + } else { + require.Equal(t, pragueBBF1, bbf1) + } + + miner.ActEmptyBlock(t) + l1_2 := miner.L1Chain().CurrentHeader() + t.Logf("Header2: Number: %v, Time: %v, ExcessBlobGas: %v", l1_2.Number, l1_2.Time, *l1_2.ExcessBlobGas) + if tcfg.offset != nil { + if *tcfg.offset%12 == 0 { + require.Equal(t, l1_2.Time, *env.Sd.RollupCfg.PectraBlobScheduleTime) + } else { + require.Greater(t, l1_2.Time, *env.Sd.RollupCfg.PectraBlobScheduleTime) + } + } + + sequencer.ActL1HeadSignal(t) + sequencer.ActBuildToL1HeadUnsafe(t) + + cancunBBF2 := eth.CalcBlobFeeCancun(*l1_2.ExcessBlobGas) + pragueBBF2 := eth.CalcBlobFeeDefault(l1_2) + require.Less(t, pragueBBF2.Uint64(), cancunBBF2.Uint64()) + bbf2, err := l1Block.BlobBaseFee(opts) + require.NoError(t, err) + t.Logf("BlobBaseFee2: %v", bbf2) + require.Equal(t, pragueBBF2, bbf2) + l2UnsafeHead := env.Engine.L2Chain().CurrentHeader() + + env.BatchAndMine(t) + env.Sequencer.ActL1HeadSignal(t) + env.Sequencer.ActL2PipelineFull(t) + + l2SafeHead := env.Engine.L2Chain().CurrentSafeBlock() + require.Equal(t, eth.HeaderBlockID(l2SafeHead), eth.HeaderBlockID(l2UnsafeHead), "derivation leads to the same block") + + env.RunFaultProofProgram(t, l2SafeHead.Number.Uint64(), testCfg.CheckResult, testCfg.InputParams...) +} + +func ptr[T any](v T) *T { + return &v +} diff --git a/op-e2e/actions/proofs/simple_program_test.go b/op-e2e/actions/proofs/simple_program_test.go index ab3218caf1..91815bd97f 100644 --- a/op-e2e/actions/proofs/simple_program_test.go +++ b/op-e2e/actions/proofs/simple_program_test.go @@ -1,18 +1,28 @@ -package proofs +package proofs_test import ( "testing" + "github.com/ethereum-optimism/optimism/op-batcher/flags" + "github.com/ethereum-optimism/optimism/op-chain-ops/genesis" actionsHelpers "github.com/ethereum-optimism/optimism/op-e2e/actions/helpers" "github.com/ethereum-optimism/optimism/op-e2e/actions/proofs/helpers" - "github.com/ethereum-optimism/optimism/op-program/client/claim" - "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/stretchr/testify/require" ) func runSimpleProgramTest(gt *testing.T, testCfg *helpers.TestCfg[any]) { t := actionsHelpers.NewDefaultTesting(gt) - env := helpers.NewL2FaultProofEnv(t, testCfg, helpers.NewTestParams(), helpers.NewBatcherCfg()) + testSetup := func(dc *genesis.DeployConfig) { + dc.L1PragueTimeOffset = ptr(hexutil.Uint64(0)) + // Set non-trivial excess blob gas so that the L1 miner's blob logic is + // properly tested. + dc.L1GenesisBlockExcessBlobGas = ptr(hexutil.Uint64(1e8)) + } + bcfg := helpers.NewBatcherCfg(func(c *actionsHelpers.BatcherCfg) { + c.DataAvailabilityType = flags.BlobsType + }) + env := helpers.NewL2FaultProofEnv(t, testCfg, helpers.NewTestParams(), bcfg, testSetup) // Build an empty block on L2 env.Sequencer.ActL2StartBlock(t) @@ -47,19 +57,9 @@ func Test_ProgramAction_SimpleEmptyChain(gt *testing.T) { matrix := helpers.NewMatrix[any]() defer matrix.Run(gt) - matrix.AddTestCase( - "HonestClaim", + matrix.AddDefaultTestCases( nil, helpers.LatestForkOnly, runSimpleProgramTest, - helpers.ExpectNoError(), - ) - matrix.AddTestCase( - "JunkClaim", - nil, - helpers.LatestForkOnly, - runSimpleProgramTest, - helpers.ExpectError(claim.ErrClaimNotValid), - helpers.WithL2Claim(common.HexToHash("0xdeadbeef")), ) } diff --git a/op-e2e/actions/proposer/l2_proposer_test.go b/op-e2e/actions/proposer/l2_proposer_test.go index 917fff2baf..da5000d855 100644 --- a/op-e2e/actions/proposer/l2_proposer_test.go +++ b/op-e2e/actions/proposer/l2_proposer_test.go @@ -28,17 +28,10 @@ func TestProposerBatchType(t *testing.T) { t.Run("SingularBatch/Standard", func(t *testing.T) { runProposerTest(t, nil, config.AllocTypeStandard) }) - t.Run("SingularBatch/L2OO", func(t *testing.T) { - runProposerTest(t, nil, config.AllocTypeL2OO) - }) t.Run("SpanBatch/Standard", func(t *testing.T) { deltaTimeOffset := hexutil.Uint64(0) runProposerTest(t, &deltaTimeOffset, config.AllocTypeStandard) }) - t.Run("SpanBatch/L2OO", func(t *testing.T) { - deltaTimeOffset := hexutil.Uint64(0) - runProposerTest(t, &deltaTimeOffset, config.AllocTypeL2OO) - }) } func runProposerTest(gt *testing.T, deltaTimeOffset *hexutil.Uint64, allocType config.AllocType) { diff --git a/op-e2e/actions/upgrades/isthmus_fork_test.go b/op-e2e/actions/upgrades/isthmus_fork_test.go index 2175970921..640df7bb0b 100644 --- a/op-e2e/actions/upgrades/isthmus_fork_test.go +++ b/op-e2e/actions/upgrades/isthmus_fork_test.go @@ -23,17 +23,20 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var ( - isthmusL1BlockCodeHash = common.HexToHash("0xe59074b8d4c08924ce463087b05485b835650652528383a32ef009fb2b6d4050") - isthmusGasPriceOracleCodeHash = common.HexToHash("0x9279e9e0535a7b63939d670e7faec536256b63d4ff353eb521a3342c51ce26e5") - isthmusOperatorFeeVaultCodeHash = common.HexToHash("0x9ee0fa5ab86f13f58fcb8f798f3a74401a8493d99d1c5b3bad19a8dff4b3194f") + isthmusL1BlockCodeHash = common.HexToHash("0x8e3fe7a416d3e5f3b7be74ddd4e7e58e516fa3f80b67c6d930e3cd7297da4a4b") + isthmusGasPriceOracleCodeHash = common.HexToHash("0x4d195a9d7caf9fb6d4beaf80de252c626c853afd5868c4f4f8d19c9d301c2679") + isthmusOperatorFeeVaultCodeHash = common.HexToHash("0x57dc55c9c09ca456fa728f253fe7b895d3e6aae0706104935fe87c7721001971") ) +var zeroHex64 = hexutil.Uint64(0) + func TestIsthmusActivationAtGenesis(gt *testing.T) { t := helpers.NewDefaultTesting(gt) env := helpers.SetupEnv(t, helpers.WithActiveGenesisFork(rollup.Isthmus)) @@ -77,6 +80,7 @@ func TestIsthmusActivationAtGenesis(gt *testing.T) { require.NoError(t, err) genesisPayload, err := l2Cl.PayloadByNumber(t.Ctx(), 0) require.NoError(t, err) + require.NotNil(t, genesisPayload.ExecutionPayload.WithdrawalsRoot) require.Equal(t, genesisPayload.ExecutionPayload.WithdrawalsRoot, genesisBlock.WithdrawalsRoot()) require.Equal(t, types.EmptyRequestsHash, *genesisPayload.RequestsHash) got, ok := genesisPayload.CheckBlockHash() @@ -90,7 +94,6 @@ func TestIsthmusActivationAtGenesis(gt *testing.T) { func TestWithdrawlsRootPreCanyonAndIsthmus(gt *testing.T) { t := helpers.NewDefaultTesting(gt) dp := e2eutils.MakeDeployParams(t, helpers.DefaultRollupTestParams()) - genesisBlock := hexutil.Uint64(0) canyonOffset := hexutil.Uint64(2) log := testlog.Logger(t, log.LvlDebug) @@ -98,14 +101,7 @@ func TestWithdrawlsRootPreCanyonAndIsthmus(gt *testing.T) { dp.DeployConfig.L1CancunTimeOffset = &canyonOffset // Activate pre-canyon forks at genesis, and schedule Canyon the block after - dp.DeployConfig.L2GenesisRegolithTimeOffset = &genesisBlock - dp.DeployConfig.L2GenesisCanyonTimeOffset = &canyonOffset - dp.DeployConfig.L2GenesisDeltaTimeOffset = nil - dp.DeployConfig.L2GenesisEcotoneTimeOffset = nil - dp.DeployConfig.L2GenesisFjordTimeOffset = nil - dp.DeployConfig.L2GenesisGraniteTimeOffset = nil - dp.DeployConfig.L2GenesisHoloceneTimeOffset = nil - dp.DeployConfig.L2GenesisIsthmusTimeOffset = nil + dp.DeployConfig.ActivateForkAtOffset(rollup.Canyon, uint64(canyonOffset)) require.NoError(t, dp.DeployConfig.Check(log), "must have valid config") sd := e2eutils.Setup(t, dp, helpers.DefaultAlloc) @@ -148,42 +144,32 @@ func TestWithdrawlsRootPreCanyonAndIsthmus(gt *testing.T) { func TestWithdrawalsRootBeforeAtAndAfterIsthmus(t *testing.T) { tests := []struct { name string - f func(gt *testing.T, withdrawalTx bool, withdrawalTxBlock, totalBlocks int) withdrawalTx bool withdrawalTxBlock int totalBlocks int }{ - {"BeforeIsthmusWithoutWithdrawalTx", testWithdrawlsRootAtIsthmus, false, 0, 1}, - {"BeforeIsthmusWithWithdrawalTx", testWithdrawlsRootAtIsthmus, true, 1, 1}, - {"AtIsthmusWithoutWithdrawalTx", testWithdrawlsRootAtIsthmus, false, 0, 2}, - {"AtIsthmusWithWithdrawalTx", testWithdrawlsRootAtIsthmus, true, 2, 2}, - {"AfterIsthmusWithoutWithdrawalTx", testWithdrawlsRootAtIsthmus, false, 0, 3}, - {"AfterIsthmusWithWithdrawalTx", testWithdrawlsRootAtIsthmus, true, 3, 3}, + {"BeforeIsthmusWithoutWithdrawalTx", false, 0, 1}, + {"BeforeIsthmusWithWithdrawalTx", true, 1, 1}, + {"AtIsthmusWithoutWithdrawalTx", false, 0, 2}, + {"AtIsthmusWithWithdrawalTx", true, 2, 2}, + {"AfterIsthmusWithoutWithdrawalTx", false, 0, 3}, + {"AfterIsthmusWithWithdrawalTx", true, 3, 3}, } for _, test := range tests { - test := test t.Run(test.name, func(t *testing.T) { - test.f(t, test.withdrawalTx, test.withdrawalTxBlock, test.totalBlocks) + testWithdrawlsRootIsthmus(t, test.withdrawalTx, test.withdrawalTxBlock, test.totalBlocks) }) } } -func testWithdrawlsRootAtIsthmus(gt *testing.T, withdrawalTx bool, withdrawalTxBlock, totalBlocks int) { +func testWithdrawlsRootIsthmus(gt *testing.T, withdrawalTx bool, withdrawalTxBlock, totalBlocks int) { t := helpers.NewDefaultTesting(gt) dp := e2eutils.MakeDeployParams(t, helpers.DefaultRollupTestParams()) - genesisBlock := hexutil.Uint64(0) - isthmusOffset := hexutil.Uint64(2) + const isthmusOffset = 2 log := testlog.Logger(t, log.LvlDebug) - dp.DeployConfig.L2GenesisRegolithTimeOffset = &genesisBlock - dp.DeployConfig.L2GenesisCanyonTimeOffset = &genesisBlock - dp.DeployConfig.L2GenesisIsthmusTimeOffset = &isthmusOffset - dp.DeployConfig.L2GenesisDeltaTimeOffset = &genesisBlock - dp.DeployConfig.L2GenesisEcotoneTimeOffset = &genesisBlock - dp.DeployConfig.L2GenesisFjordTimeOffset = &genesisBlock - dp.DeployConfig.L2GenesisGraniteTimeOffset = &genesisBlock - dp.DeployConfig.L2GenesisHoloceneTimeOffset = &genesisBlock + dp.DeployConfig.ActivateForkAtOffset(rollup.Isthmus, isthmusOffset) require.NoError(t, dp.DeployConfig.Check(log), "must have valid config") sd := e2eutils.Setup(t, dp, helpers.DefaultAlloc) @@ -203,32 +189,32 @@ func testWithdrawlsRootAtIsthmus(gt *testing.T, withdrawalTx bool, withdrawalTxB if withdrawalTx && withdrawalTxBlock == i { l2withdrawer, err := bindings.NewL2ToL1MessagePasser(predeploys.L2ToL1MessagePasserAddr, ethCl) - require.Nil(t, err, "binding withdrawer on L2") + require.NoError(t, err, "binding withdrawer on L2") // Initiate Withdrawal // Bind L2 Withdrawer Contract and invoke the Receive function l2opts, err := bind.NewKeyedTransactorWithChainID(dp.Secrets.Alice, new(big.Int).SetUint64(dp.DeployConfig.L2ChainID)) - require.Nil(t, err) + require.NoError(t, err) l2opts.Value = big.NewInt(500) tx, err = l2withdrawer.Receive(l2opts) - require.Nil(t, err) + require.NoError(t, err) - // include the transaction - engine.ActL2IncludeTx(dp.Addresses.Alice)(t) + // force-include the transaction, also in upgrade blocks + engine.ActL2IncludeTxIgnoreForcedEmpty(dp.Addresses.Alice)(t) } sequencer.ActL2EndBlock(t) if withdrawalTx && withdrawalTxBlock == i { // wait for withdrawal to be included in a block receipt, err := geth.WaitForTransaction(tx.Hash(), ethCl, 10*time.Duration(dp.DeployConfig.L2BlockTime)*time.Second) - require.Nil(t, err, "withdrawal initiated on L2 sequencer") + require.NoError(t, err, "withdrawal initiated on L2 sequencer") require.Equal(t, types.ReceiptStatusSuccessful, receipt.Status, "transaction had incorrect status") } } rpcCl := engine.RPCClient() // we set withdrawals root only at or after isthmus - if totalBlocks >= 2 { + if totalBlocks >= isthmusOffset { verifyIsthmusHeaderWithdrawalsRoot(gt, rpcCl, engine.L2Chain().CurrentBlock(), true) } } @@ -236,19 +222,12 @@ func testWithdrawlsRootAtIsthmus(gt *testing.T, withdrawalTx bool, withdrawalTxB func TestWithdrawlsRootPostIsthmus(gt *testing.T) { t := helpers.NewDefaultTesting(gt) dp := e2eutils.MakeDeployParams(t, helpers.DefaultRollupTestParams()) - genesisBlock := hexutil.Uint64(0) - isthmusOffset := hexutil.Uint64(2) + const isthmusOffset = 2 log := testlog.Logger(t, log.LvlDebug) - dp.DeployConfig.L2GenesisRegolithTimeOffset = &genesisBlock - dp.DeployConfig.L2GenesisCanyonTimeOffset = &genesisBlock - dp.DeployConfig.L2GenesisIsthmusTimeOffset = &isthmusOffset - dp.DeployConfig.L2GenesisDeltaTimeOffset = &genesisBlock - dp.DeployConfig.L2GenesisEcotoneTimeOffset = &genesisBlock - dp.DeployConfig.L2GenesisFjordTimeOffset = &genesisBlock - dp.DeployConfig.L2GenesisGraniteTimeOffset = &genesisBlock - dp.DeployConfig.L2GenesisHoloceneTimeOffset = &genesisBlock + dp.DeployConfig.ActivateForkAtOffset(rollup.Isthmus, isthmusOffset) + dp.DeployConfig.L1PragueTimeOffset = &zeroHex64 require.NoError(t, dp.DeployConfig.Check(log), "must have valid config") sd := e2eutils.Setup(t, dp, helpers.DefaultAlloc) @@ -267,15 +246,15 @@ func TestWithdrawlsRootPostIsthmus(gt *testing.T) { // Bind L2 Withdrawer Contract ethCl := engine.EthClient() l2withdrawer, err := bindings.NewL2ToL1MessagePasser(predeploys.L2ToL1MessagePasserAddr, ethCl) - require.Nil(t, err, "binding withdrawer on L2") + require.NoError(t, err, "binding withdrawer on L2") // Initiate Withdrawal l2opts, err := bind.NewKeyedTransactorWithChainID(dp.Secrets.Alice, new(big.Int).SetUint64(dp.DeployConfig.L2ChainID)) - require.Nil(t, err) + require.NoError(t, err) l2opts.Value = big.NewInt(500) tx, err := l2withdrawer.Receive(l2opts) - require.Nil(t, err) + require.NoError(t, err) // build blocks until Isthmus activates sequencer.ActL2StartBlock(t) @@ -288,7 +267,7 @@ func TestWithdrawlsRootPostIsthmus(gt *testing.T) { // wait for withdrawal to be included in a block receipt, err := geth.WaitForTransaction(tx.Hash(), ethCl, 10*time.Duration(dp.DeployConfig.L2BlockTime)*time.Second) - require.Nil(t, err, "withdrawal initiated on L2 sequencer") + require.NoError(t, err, "withdrawal initiated on L2 sequencer") require.Equal(t, types.ReceiptStatusSuccessful, receipt.Status, "transaction had incorrect status") verifyIsthmusHeaderWithdrawalsRoot(gt, rpcCl, engine.L2Chain().CurrentBlock(), true) @@ -308,7 +287,7 @@ func verifyIsthmusHeaderWithdrawalsRoot(gt *testing.T, rpcCl client.RPC, header getStorageRoot := func(rpcCl client.RPC, ctx context.Context, address common.Address, blockTag string) common.Hash { var getProofResponse *eth.AccountResult err := rpcCl.CallContext(ctx, &getProofResponse, "eth_getProof", address, []common.Hash{}, blockTag) - assert.Nil(gt, err) + assert.NoError(gt, err) assert.NotNil(gt, getProofResponse) return getProofResponse.StorageHash } @@ -321,20 +300,25 @@ func verifyIsthmusHeaderWithdrawalsRoot(gt *testing.T, rpcCl client.RPC, header } } +func checkContractVersion(gt *testing.T, client *ethclient.Client, addr common.Address, expectedVersion string) { + isemver, err := bindings.NewISemver(addr, client) + require.NoError(gt, err) + + version, err := isemver.Version(nil) + require.NoError(gt, err) + + require.Equal(gt, expectedVersion, version) +} + func TestIsthmusNetworkUpgradeTransactions(gt *testing.T) { t := helpers.NewDefaultTesting(gt) dp := e2eutils.MakeDeployParams(t, helpers.DefaultRollupTestParams()) - isthmusOffset := hexutil.Uint64(4) + const isthmusOffset = 2 - log := testlog.Logger(t, log.LevelDebug) - - zero := hexutil.Uint64(0) + log := testlog.Logger(t, log.LvlDebug) - // Activate all forks at genesis, and schedule Isthmus the block after - dp.DeployConfig.L2GenesisHoloceneTimeOffset = &zero - dp.DeployConfig.L2GenesisIsthmusTimeOffset = &isthmusOffset - dp.DeployConfig.L1PragueTimeOffset = &zero - // New forks have to be added here... + dp.DeployConfig.ActivateForkAtOffset(rollup.Isthmus, isthmusOffset) + dp.DeployConfig.L1PragueTimeOffset = &zeroHex64 require.NoError(t, dp.DeployConfig.Check(log), "must have valid config") sd := e2eutils.Setup(t, dp, helpers.DefaultAlloc) @@ -429,6 +413,11 @@ func TestIsthmusNetworkUpgradeTransactions(gt *testing.T) { require.Equal(t, expectedHash, common.BytesToHash(rootValue), msg) } + // Check contract versions + checkContractVersion(gt, ethCl, common.BytesToAddress(updatedL1BlockAddress), "1.6.0") + checkContractVersion(gt, ethCl, common.BytesToAddress(updatedGasPriceOracleAddress), "1.4.0") + checkContractVersion(gt, ethCl, common.BytesToAddress(updatedOperatorFeeVaultAddress), "1.0.0") + // Legacy check: // > The first block is an exception in upgrade-networks, // > since the recent-block-hash contract isn't there at Isthmus activation, diff --git a/op-e2e/e2eutils/disputegame/cannon_helper.go b/op-e2e/e2eutils/disputegame/cannon_helper.go index b49c70c5be..8284f6f35c 100644 --- a/op-e2e/e2eutils/disputegame/cannon_helper.go +++ b/op-e2e/e2eutils/disputegame/cannon_helper.go @@ -337,7 +337,7 @@ func (g *CannonHelper) createCannonTraceProvider(ctx context.Context, l2Node str l2Client := g.system.NodeClient(l2Node) - prestateBlock, poststateBlock, err := g.splitGame.Game.GetBlockRange(ctx) + prestateBlock, poststateBlock, err := g.splitGame.Game.GetGameRange(ctx) g.require.NoError(err, "Failed to load block range") rollupClient := g.system.RollupClient(l2Node) prestateProvider := outputs.NewPrestateProvider(rollupClient, prestateBlock) diff --git a/op-e2e/e2eutils/disputegame/helper.go b/op-e2e/e2eutils/disputegame/helper.go index 50d9cd9c4f..9d07586d9a 100644 --- a/op-e2e/e2eutils/disputegame/helper.go +++ b/op-e2e/e2eutils/disputegame/helper.go @@ -202,7 +202,7 @@ func (h *FactoryHelper) startOutputCannonGameOfType(ctx context.Context, l2Node game, err := contracts.NewFaultDisputeGameContract(ctx, metrics.NoopContractMetrics, createdEvent.DisputeProxy, batching.NewMultiCaller(h.Client.Client(), batching.DefaultBatchSize)) h.Require.NoError(err) - prestateBlock, poststateBlock, err := game.GetBlockRange(ctx) + prestateBlock, poststateBlock, err := game.GetGameRange(ctx) h.Require.NoError(err, "Failed to load starting block number") splitDepth, err := game.GetSplitDepth(ctx) h.Require.NoError(err, "Failed to load split depth") @@ -240,7 +240,7 @@ func (h *FactoryHelper) startSuperCannonGameOfType(ctx context.Context, timestam game, err := contracts.NewFaultDisputeGameContract(ctx, metrics.NoopContractMetrics, createdEvent.DisputeProxy, batching.NewMultiCaller(h.Client.Client(), batching.DefaultBatchSize)) h.Require.NoError(err) - prestateTimestamp, poststateTimestamp, err := game.GetBlockRange(ctx) + prestateTimestamp, poststateTimestamp, err := game.GetGameRange(ctx) h.Require.NoError(err, "Failed to load starting block number") splitDepth, err := game.GetSplitDepth(ctx) h.Require.NoError(err, "Failed to load split depth") @@ -294,7 +294,7 @@ func (h *FactoryHelper) StartOutputAlphabetGame(ctx context.Context, l2Node stri game, err := contracts.NewFaultDisputeGameContract(ctx, metrics.NoopContractMetrics, createdEvent.DisputeProxy, batching.NewMultiCaller(h.Client.Client(), batching.DefaultBatchSize)) h.Require.NoError(err) - prestateBlock, poststateBlock, err := game.GetBlockRange(ctx) + prestateBlock, poststateBlock, err := game.GetGameRange(ctx) h.Require.NoError(err, "Failed to load starting block number") splitDepth, err := game.GetSplitDepth(ctx) h.Require.NoError(err, "Failed to load split depth") diff --git a/op-e2e/e2eutils/disputegame/output_alphabet_helper.go b/op-e2e/e2eutils/disputegame/output_alphabet_helper.go index b9acf24f2f..a6ad0cabe3 100644 --- a/op-e2e/e2eutils/disputegame/output_alphabet_helper.go +++ b/op-e2e/e2eutils/disputegame/output_alphabet_helper.go @@ -35,7 +35,7 @@ func (g *OutputAlphabetGameHelper) StartChallenger( func (g *OutputAlphabetGameHelper) CreateHonestActor(ctx context.Context, l2Node string) *OutputHonestHelper { logger := testlog.Logger(g.T, log.LevelInfo).New("role", "HonestHelper", "game", g.Addr) - prestateBlock, poststateBlock, err := g.Game.GetBlockRange(ctx) + prestateBlock, poststateBlock, err := g.Game.GetGameRange(ctx) g.Require.NoError(err, "Get block range") splitDepth := g.SplitDepth(ctx) l1Head := g.GetL1Head(ctx) diff --git a/op-e2e/e2eutils/disputegame/output_cannon_helper.go b/op-e2e/e2eutils/disputegame/output_cannon_helper.go index 533e4f779f..a8686c95cb 100644 --- a/op-e2e/e2eutils/disputegame/output_cannon_helper.go +++ b/op-e2e/e2eutils/disputegame/output_cannon_helper.go @@ -64,7 +64,7 @@ func (g *OutputCannonGameHelper) CreateHonestActor(ctx context.Context, l2Node s logger := testlog.Logger(g.T, log.LevelInfo).New("role", "HonestHelper", "game", g.Addr) l2Client := g.System.NodeClient(l2Node) - realPrestateBlock, realPostStateBlock, err := g.Game.GetBlockRange(ctx) + realPrestateBlock, realPostStateBlock, err := g.Game.GetGameRange(ctx) g.Require.NoError(err, "Failed to load block range") splitDepth := g.SplitDepth(ctx) rollupClient := g.System.RollupClient(l2Node) diff --git a/op-e2e/e2eutils/disputegame/output_game_helper.go b/op-e2e/e2eutils/disputegame/output_game_helper.go index a969c59309..055459def9 100644 --- a/op-e2e/e2eutils/disputegame/output_game_helper.go +++ b/op-e2e/e2eutils/disputegame/output_game_helper.go @@ -54,7 +54,7 @@ func NewOutputGameHelper(t *testing.T, require *require.Assertions, client *ethc } func (g *OutputGameHelper) StartingBlockNum(ctx context.Context) uint64 { - blockNum, _, err := g.Game.GetBlockRange(ctx) + blockNum, _, err := g.Game.GetGameRange(ctx) g.Require.NoError(err, "failed to load starting block number") return blockNum } diff --git a/op-e2e/e2eutils/disputegame/split_game_helper.go b/op-e2e/e2eutils/disputegame/split_game_helper.go index bb9dbc1560..05fcec2a7b 100644 --- a/op-e2e/e2eutils/disputegame/split_game_helper.go +++ b/op-e2e/e2eutils/disputegame/split_game_helper.go @@ -68,7 +68,7 @@ func WithIgnoreDuplicates() MoveOpt { } func (g *SplitGameHelper) L2BlockNum(ctx context.Context) uint64 { - _, blockNum, err := g.Game.GetBlockRange(ctx) + _, blockNum, err := g.Game.GetGameRange(ctx) g.Require.NoError(err, "failed to load l2 block number") return blockNum } diff --git a/op-e2e/e2eutils/interop/contracts/bindings/emit/emit.go b/op-e2e/e2eutils/interop/contracts/bindings/emit/emit.go index 7755ef5fd1..ceae241265 100644 --- a/op-e2e/e2eutils/interop/contracts/bindings/emit/emit.go +++ b/op-e2e/e2eutils/interop/contracts/bindings/emit/emit.go @@ -26,6 +26,7 @@ var ( _ = common.Big1 _ = types.BloomLookup _ = event.NewSubscription + _ = abi.ConvertType ) // EmitMetaData contains all meta data concerning the Emit contract. @@ -156,11 +157,11 @@ func NewEmitFilterer(address common.Address, filterer bind.ContractFilterer) (*E // bindEmit binds a generic wrapper to an already deployed contract. func bindEmit(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { - parsed, err := abi.JSON(strings.NewReader(EmitABI)) + parsed, err := EmitMetaData.GetAbi() if err != nil { return nil, err } - return bind.NewBoundContract(address, parsed, caller, transactor, filterer), nil + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil } // Call invokes the (constant) contract method with params as input values and diff --git a/op-e2e/e2eutils/interop/contracts/bindings/inbox/inbox.go b/op-e2e/e2eutils/interop/contracts/bindings/inbox/inbox.go index e9381f631d..e41399ef18 100644 --- a/op-e2e/e2eutils/interop/contracts/bindings/inbox/inbox.go +++ b/op-e2e/e2eutils/interop/contracts/bindings/inbox/inbox.go @@ -26,6 +26,7 @@ var ( _ = common.Big1 _ = types.BloomLookup _ = event.NewSubscription + _ = abi.ConvertType ) // Identifier is an auto generated low-level Go binding around an user-defined struct. @@ -39,7 +40,7 @@ type Identifier struct { // InboxMetaData contains all meta data concerning the Inbox contract. var InboxMetaData = &bind.MetaData{ - ABI: "[{\"type\":\"function\",\"name\":\"executeMessage\",\"inputs\":[{\"name\":\"_id\",\"type\":\"tuple\",\"internalType\":\"structIdentifier\",\"components\":[{\"name\":\"origin\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"blockNumber\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"logIndex\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"timestamp\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"chainId\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"name\":\"_target\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"_message\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[],\"stateMutability\":\"payable\"},{\"type\":\"function\",\"name\":\"validateMessage\",\"inputs\":[{\"name\":\"_id\",\"type\":\"tuple\",\"internalType\":\"structIdentifier\",\"components\":[{\"name\":\"origin\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"blockNumber\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"logIndex\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"timestamp\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"chainId\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"name\":\"_msgHash\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"}]", + ABI: "[{\"type\":\"function\",\"name\":\"validateMessage\",\"inputs\":[{\"name\":\"_id\",\"type\":\"tuple\",\"internalType\":\"structIdentifier\",\"components\":[{\"name\":\"origin\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"blockNumber\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"logIndex\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"timestamp\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"chainId\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"name\":\"_msgHash\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"version\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"string\",\"internalType\":\"string\"}],\"stateMutability\":\"view\"},{\"type\":\"event\",\"name\":\"ExecutingMessage\",\"inputs\":[{\"name\":\"msgHash\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"bytes32\"},{\"name\":\"id\",\"type\":\"tuple\",\"indexed\":false,\"internalType\":\"structIdentifier\",\"components\":[{\"name\":\"origin\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"blockNumber\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"logIndex\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"timestamp\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"chainId\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]}],\"anonymous\":false},{\"type\":\"error\",\"name\":\"BlockNumberTooHigh\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"LogIndexTooHigh\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"NoExecutingDeposits\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"NotInAccessList\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"TimestampTooHigh\",\"inputs\":[]}]", } // InboxABI is the input ABI used to generate the binding from. @@ -143,11 +144,11 @@ func NewInboxFilterer(address common.Address, filterer bind.ContractFilterer) (* // bindInbox binds a generic wrapper to an already deployed contract. func bindInbox(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { - parsed, err := abi.JSON(strings.NewReader(InboxABI)) + parsed, err := InboxMetaData.GetAbi() if err != nil { return nil, err } - return bind.NewBoundContract(address, parsed, caller, transactor, filterer), nil + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil } // Call invokes the (constant) contract method with params as input values and @@ -188,6 +189,37 @@ func (_Inbox *InboxTransactorRaw) Transact(opts *bind.TransactOpts, method strin return _Inbox.Contract.contract.Transact(opts, method, params...) } +// Version is a free data retrieval call binding the contract method 0x54fd4d50. +// +// Solidity: function version() view returns(string) +func (_Inbox *InboxCaller) Version(opts *bind.CallOpts) (string, error) { + var out []interface{} + err := _Inbox.contract.Call(opts, &out, "version") + + if err != nil { + return *new(string), err + } + + out0 := *abi.ConvertType(out[0], new(string)).(*string) + + return out0, err + +} + +// Version is a free data retrieval call binding the contract method 0x54fd4d50. +// +// Solidity: function version() view returns(string) +func (_Inbox *InboxSession) Version() (string, error) { + return _Inbox.Contract.Version(&_Inbox.CallOpts) +} + +// Version is a free data retrieval call binding the contract method 0x54fd4d50. +// +// Solidity: function version() view returns(string) +func (_Inbox *InboxCallerSession) Version() (string, error) { + return _Inbox.Contract.Version(&_Inbox.CallOpts) +} + // ValidateMessage is a paid mutator transaction binding the contract method 0xab4d6f75. // // Solidity: function validateMessage((address,uint256,uint256,uint256,uint256) _id, bytes32 _msgHash) returns() @@ -208,3 +240,148 @@ func (_Inbox *InboxSession) ValidateMessage(_id Identifier, _msgHash [32]byte) ( func (_Inbox *InboxTransactorSession) ValidateMessage(_id Identifier, _msgHash [32]byte) (*types.Transaction, error) { return _Inbox.Contract.ValidateMessage(&_Inbox.TransactOpts, _id, _msgHash) } + +// InboxExecutingMessageIterator is returned from FilterExecutingMessage and is used to iterate over the raw logs and unpacked data for ExecutingMessage events raised by the Inbox contract. +type InboxExecutingMessageIterator struct { + Event *InboxExecutingMessage // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *InboxExecutingMessageIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(InboxExecutingMessage) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(InboxExecutingMessage) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *InboxExecutingMessageIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *InboxExecutingMessageIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// InboxExecutingMessage represents a ExecutingMessage event raised by the Inbox contract. +type InboxExecutingMessage struct { + MsgHash [32]byte + Id Identifier + Raw types.Log // Blockchain specific contextual infos +} + +// FilterExecutingMessage is a free log retrieval operation binding the contract event 0x5c37832d2e8d10e346e55ad62071a6a2f9fa5130614ef2ec6617555c6f467ba7. +// +// Solidity: event ExecutingMessage(bytes32 indexed msgHash, (address,uint256,uint256,uint256,uint256) id) +func (_Inbox *InboxFilterer) FilterExecutingMessage(opts *bind.FilterOpts, msgHash [][32]byte) (*InboxExecutingMessageIterator, error) { + + var msgHashRule []interface{} + for _, msgHashItem := range msgHash { + msgHashRule = append(msgHashRule, msgHashItem) + } + + logs, sub, err := _Inbox.contract.FilterLogs(opts, "ExecutingMessage", msgHashRule) + if err != nil { + return nil, err + } + return &InboxExecutingMessageIterator{contract: _Inbox.contract, event: "ExecutingMessage", logs: logs, sub: sub}, nil +} + +// WatchExecutingMessage is a free log subscription operation binding the contract event 0x5c37832d2e8d10e346e55ad62071a6a2f9fa5130614ef2ec6617555c6f467ba7. +// +// Solidity: event ExecutingMessage(bytes32 indexed msgHash, (address,uint256,uint256,uint256,uint256) id) +func (_Inbox *InboxFilterer) WatchExecutingMessage(opts *bind.WatchOpts, sink chan<- *InboxExecutingMessage, msgHash [][32]byte) (event.Subscription, error) { + + var msgHashRule []interface{} + for _, msgHashItem := range msgHash { + msgHashRule = append(msgHashRule, msgHashItem) + } + + logs, sub, err := _Inbox.contract.WatchLogs(opts, "ExecutingMessage", msgHashRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(InboxExecutingMessage) + if err := _Inbox.contract.UnpackLog(event, "ExecutingMessage", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseExecutingMessage is a log parse operation binding the contract event 0x5c37832d2e8d10e346e55ad62071a6a2f9fa5130614ef2ec6617555c6f467ba7. +// +// Solidity: event ExecutingMessage(bytes32 indexed msgHash, (address,uint256,uint256,uint256,uint256) id) +func (_Inbox *InboxFilterer) ParseExecutingMessage(log types.Log) (*InboxExecutingMessage, error) { + event := new(InboxExecutingMessage) + if err := _Inbox.contract.UnpackLog(event, "ExecutingMessage", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/op-e2e/e2eutils/interop/contracts/bindings/systemconfig/systemconfig.go b/op-e2e/e2eutils/interop/contracts/bindings/systemconfig/systemconfig.go deleted file mode 100644 index 9e29cc4700..0000000000 --- a/op-e2e/e2eutils/interop/contracts/bindings/systemconfig/systemconfig.go +++ /dev/null @@ -1,201 +0,0 @@ -// Code generated - DO NOT EDIT. -// This file is a generated binding and any manual changes will be lost. - -package systemconfig - -import ( - "errors" - "math/big" - "strings" - - ethereum "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/accounts/abi" - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/event" -) - -// Reference imports to suppress errors if they are not otherwise used. -var ( - _ = errors.New - _ = big.NewInt - _ = strings.NewReader - _ = ethereum.NotFound - _ = bind.Bind - _ = common.Big1 - _ = types.BloomLookup - _ = event.NewSubscription -) - -// SystemconfigMetaData contains all meta data concerning the Systemconfig contract. -var SystemconfigMetaData = &bind.MetaData{ - ABI: "[{\"type\":\"function\",\"name\":\"addDependency\",\"inputs\":[{\"name\":\"_chainId\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"}]", -} - -// SystemconfigABI is the input ABI used to generate the binding from. -// Deprecated: Use SystemconfigMetaData.ABI instead. -var SystemconfigABI = SystemconfigMetaData.ABI - -// Systemconfig is an auto generated Go binding around an Ethereum contract. -type Systemconfig struct { - SystemconfigCaller // Read-only binding to the contract - SystemconfigTransactor // Write-only binding to the contract - SystemconfigFilterer // Log filterer for contract events -} - -// SystemconfigCaller is an auto generated read-only Go binding around an Ethereum contract. -type SystemconfigCaller struct { - contract *bind.BoundContract // Generic contract wrapper for the low level calls -} - -// SystemconfigTransactor is an auto generated write-only Go binding around an Ethereum contract. -type SystemconfigTransactor struct { - contract *bind.BoundContract // Generic contract wrapper for the low level calls -} - -// SystemconfigFilterer is an auto generated log filtering Go binding around an Ethereum contract events. -type SystemconfigFilterer struct { - contract *bind.BoundContract // Generic contract wrapper for the low level calls -} - -// SystemconfigSession is an auto generated Go binding around an Ethereum contract, -// with pre-set call and transact options. -type SystemconfigSession struct { - Contract *Systemconfig // Generic contract binding to set the session for - CallOpts bind.CallOpts // Call options to use throughout this session - TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session -} - -// SystemconfigCallerSession is an auto generated read-only Go binding around an Ethereum contract, -// with pre-set call options. -type SystemconfigCallerSession struct { - Contract *SystemconfigCaller // Generic contract caller binding to set the session for - CallOpts bind.CallOpts // Call options to use throughout this session -} - -// SystemconfigTransactorSession is an auto generated write-only Go binding around an Ethereum contract, -// with pre-set transact options. -type SystemconfigTransactorSession struct { - Contract *SystemconfigTransactor // Generic contract transactor binding to set the session for - TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session -} - -// SystemconfigRaw is an auto generated low-level Go binding around an Ethereum contract. -type SystemconfigRaw struct { - Contract *Systemconfig // Generic contract binding to access the raw methods on -} - -// SystemconfigCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. -type SystemconfigCallerRaw struct { - Contract *SystemconfigCaller // Generic read-only contract binding to access the raw methods on -} - -// SystemconfigTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. -type SystemconfigTransactorRaw struct { - Contract *SystemconfigTransactor // Generic write-only contract binding to access the raw methods on -} - -// NewSystemconfig creates a new instance of Systemconfig, bound to a specific deployed contract. -func NewSystemconfig(address common.Address, backend bind.ContractBackend) (*Systemconfig, error) { - contract, err := bindSystemconfig(address, backend, backend, backend) - if err != nil { - return nil, err - } - return &Systemconfig{SystemconfigCaller: SystemconfigCaller{contract: contract}, SystemconfigTransactor: SystemconfigTransactor{contract: contract}, SystemconfigFilterer: SystemconfigFilterer{contract: contract}}, nil -} - -// NewSystemconfigCaller creates a new read-only instance of Systemconfig, bound to a specific deployed contract. -func NewSystemconfigCaller(address common.Address, caller bind.ContractCaller) (*SystemconfigCaller, error) { - contract, err := bindSystemconfig(address, caller, nil, nil) - if err != nil { - return nil, err - } - return &SystemconfigCaller{contract: contract}, nil -} - -// NewSystemconfigTransactor creates a new write-only instance of Systemconfig, bound to a specific deployed contract. -func NewSystemconfigTransactor(address common.Address, transactor bind.ContractTransactor) (*SystemconfigTransactor, error) { - contract, err := bindSystemconfig(address, nil, transactor, nil) - if err != nil { - return nil, err - } - return &SystemconfigTransactor{contract: contract}, nil -} - -// NewSystemconfigFilterer creates a new log filterer instance of Systemconfig, bound to a specific deployed contract. -func NewSystemconfigFilterer(address common.Address, filterer bind.ContractFilterer) (*SystemconfigFilterer, error) { - contract, err := bindSystemconfig(address, nil, nil, filterer) - if err != nil { - return nil, err - } - return &SystemconfigFilterer{contract: contract}, nil -} - -// bindSystemconfig binds a generic wrapper to an already deployed contract. -func bindSystemconfig(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { - parsed, err := abi.JSON(strings.NewReader(SystemconfigABI)) - if err != nil { - return nil, err - } - return bind.NewBoundContract(address, parsed, caller, transactor, filterer), nil -} - -// Call invokes the (constant) contract method with params as input values and -// sets the output to result. The result type might be a single field for simple -// returns, a slice of interfaces for anonymous returns and a struct for named -// returns. -func (_Systemconfig *SystemconfigRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { - return _Systemconfig.Contract.SystemconfigCaller.contract.Call(opts, result, method, params...) -} - -// Transfer initiates a plain transaction to move funds to the contract, calling -// its default method if one is available. -func (_Systemconfig *SystemconfigRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { - return _Systemconfig.Contract.SystemconfigTransactor.contract.Transfer(opts) -} - -// Transact invokes the (paid) contract method with params as input values. -func (_Systemconfig *SystemconfigRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { - return _Systemconfig.Contract.SystemconfigTransactor.contract.Transact(opts, method, params...) -} - -// Call invokes the (constant) contract method with params as input values and -// sets the output to result. The result type might be a single field for simple -// returns, a slice of interfaces for anonymous returns and a struct for named -// returns. -func (_Systemconfig *SystemconfigCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { - return _Systemconfig.Contract.contract.Call(opts, result, method, params...) -} - -// Transfer initiates a plain transaction to move funds to the contract, calling -// its default method if one is available. -func (_Systemconfig *SystemconfigTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { - return _Systemconfig.Contract.contract.Transfer(opts) -} - -// Transact invokes the (paid) contract method with params as input values. -func (_Systemconfig *SystemconfigTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { - return _Systemconfig.Contract.contract.Transact(opts, method, params...) -} - -// AddDependency is a paid mutator transaction binding the contract method 0xa89c793c. -// -// Solidity: function addDependency(uint256 _chainId) returns() -func (_Systemconfig *SystemconfigTransactor) AddDependency(opts *bind.TransactOpts, _chainId *big.Int) (*types.Transaction, error) { - return _Systemconfig.contract.Transact(opts, "addDependency", _chainId) -} - -// AddDependency is a paid mutator transaction binding the contract method 0xa89c793c. -// -// Solidity: function addDependency(uint256 _chainId) returns() -func (_Systemconfig *SystemconfigSession) AddDependency(_chainId *big.Int) (*types.Transaction, error) { - return _Systemconfig.Contract.AddDependency(&_Systemconfig.TransactOpts, _chainId) -} - -// AddDependency is a paid mutator transaction binding the contract method 0xa89c793c. -// -// Solidity: function addDependency(uint256 _chainId) returns() -func (_Systemconfig *SystemconfigTransactorSession) AddDependency(_chainId *big.Int) (*types.Transaction, error) { - return _Systemconfig.Contract.AddDependency(&_Systemconfig.TransactOpts, _chainId) -} diff --git a/op-e2e/e2eutils/interop/contracts/build/ICrossL2Inbox.sol/ICrossL2Inbox.abi b/op-e2e/e2eutils/interop/contracts/build/ICrossL2Inbox.sol/ICrossL2Inbox.abi index b724169ec6..e007a8ed48 100644 --- a/op-e2e/e2eutils/interop/contracts/build/ICrossL2Inbox.sol/ICrossL2Inbox.abi +++ b/op-e2e/e2eutils/interop/contracts/build/ICrossL2Inbox.sol/ICrossL2Inbox.abi @@ -1,7 +1,7 @@ [ { "type": "function", - "name": "executeMessage", + "name": "validateMessage", "inputs": [ { "name": "_id", @@ -36,26 +36,41 @@ ] }, { - "name": "_target", - "type": "address", - "internalType": "address" - }, - { - "name": "_message", - "type": "bytes", - "internalType": "bytes" + "name": "_msgHash", + "type": "bytes32", + "internalType": "bytes32" } ], "outputs": [], - "stateMutability": "payable" + "stateMutability": "nonpayable" }, { "type": "function", - "name": "validateMessage", + "name": "version", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "event", + "name": "ExecutingMessage", "inputs": [ { - "name": "_id", + "name": "msgHash", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "id", "type": "tuple", + "indexed": false, "internalType": "struct Identifier", "components": [ { @@ -84,14 +99,33 @@ "internalType": "uint256" } ] - }, - { - "name": "_msgHash", - "type": "bytes32", - "internalType": "bytes32" } ], - "outputs": [], - "stateMutability": "nonpayable" + "anonymous": false + }, + { + "type": "error", + "name": "BlockNumberTooHigh", + "inputs": [] + }, + { + "type": "error", + "name": "LogIndexTooHigh", + "inputs": [] + }, + { + "type": "error", + "name": "NoExecutingDeposits", + "inputs": [] + }, + { + "type": "error", + "name": "NotInAccessList", + "inputs": [] + }, + { + "type": "error", + "name": "TimestampTooHigh", + "inputs": [] } ] diff --git a/op-e2e/e2eutils/interop/contracts/build/ICrossL2Inbox.sol/ICrossL2Inbox.json b/op-e2e/e2eutils/interop/contracts/build/ICrossL2Inbox.sol/ICrossL2Inbox.json index 96c8d6ddb0..00d0c90cbd 100644 --- a/op-e2e/e2eutils/interop/contracts/build/ICrossL2Inbox.sol/ICrossL2Inbox.json +++ b/op-e2e/e2eutils/interop/contracts/build/ICrossL2Inbox.sol/ICrossL2Inbox.json @@ -1 +1 @@ -{"abi":[{"type":"function","name":"executeMessage","inputs":[{"name":"_id","type":"tuple","internalType":"struct Identifier","components":[{"name":"origin","type":"address","internalType":"address"},{"name":"blockNumber","type":"uint256","internalType":"uint256"},{"name":"logIndex","type":"uint256","internalType":"uint256"},{"name":"timestamp","type":"uint256","internalType":"uint256"},{"name":"chainId","type":"uint256","internalType":"uint256"}]},{"name":"_target","type":"address","internalType":"address"},{"name":"_message","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"validateMessage","inputs":[{"name":"_id","type":"tuple","internalType":"struct Identifier","components":[{"name":"origin","type":"address","internalType":"address"},{"name":"blockNumber","type":"uint256","internalType":"uint256"},{"name":"logIndex","type":"uint256","internalType":"uint256"},{"name":"timestamp","type":"uint256","internalType":"uint256"},{"name":"chainId","type":"uint256","internalType":"uint256"}]},{"name":"_msgHash","type":"bytes32","internalType":"bytes32"}],"outputs":[],"stateMutability":"nonpayable"}],"bytecode":{"object":"0x","sourceMap":"","linkReferences":{}},"deployedBytecode":{"object":"0x","sourceMap":"","linkReferences":{}},"methodIdentifiers":{"executeMessage((address,uint256,uint256,uint256,uint256),address,bytes)":"5984c53e","validateMessage((address,uint256,uint256,uint256,uint256),bytes32)":"ab4d6f75"},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.25+commit.b61c2a91\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"origin\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"logIndex\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"timestamp\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"chainId\",\"type\":\"uint256\"}],\"internalType\":\"struct Identifier\",\"name\":\"_id\",\"type\":\"tuple\"},{\"internalType\":\"address\",\"name\":\"_target\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"_message\",\"type\":\"bytes\"}],\"name\":\"executeMessage\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"origin\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"logIndex\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"timestamp\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"chainId\",\"type\":\"uint256\"}],\"internalType\":\"struct Identifier\",\"name\":\"_id\",\"type\":\"tuple\"},{\"internalType\":\"bytes32\",\"name\":\"_msgHash\",\"type\":\"bytes32\"}],\"name\":\"validateMessage\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}],\"devdoc\":{\"kind\":\"dev\",\"methods\":{\"executeMessage((address,uint256,uint256,uint256,uint256),address,bytes)\":{\"params\":{\"_id\":\"An Identifier pointing to the initiating message.\",\"_message\":\"The message payload, matching the initiating message.\",\"_target\":\"Account that is called with _msg.\"}},\"validateMessage((address,uint256,uint256,uint256,uint256),bytes32)\":{\"params\":{\"_id\":\"Identifier of the message.\",\"_msgHash\":\"Hash of the message payload to call target with.\"}}},\"title\":\"ICrossL2Inbox\",\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{\"executeMessage((address,uint256,uint256,uint256,uint256),address,bytes)\":{\"notice\":\"Executes a cross chain message on the destination chain.\"},\"validateMessage((address,uint256,uint256,uint256,uint256),bytes32)\":{\"notice\":\"Validates a cross chain message on the destination chain and emits an ExecutingMessage event. This function is useful for applications that understand the schema of the _message payload and want to process it in a custom way.\"}},\"notice\":\"Interface for the CrossL2Inbox contract.\",\"version\":1}},\"settings\":{\"compilationTarget\":{\"src/ICrossL2Inbox.sol\":\"ICrossL2Inbox\"},\"evmVersion\":\"cancun\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"none\"},\"optimizer\":{\"enabled\":true,\"runs\":999999},\"remappings\":[]},\"sources\":{\"src/ICrossL2Inbox.sol\":{\"keccak256\":\"0x97daf76e4a10b96d8062e71df352cbfa7577593fa96676fd43b40f4a0aca9b7f\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://74518c974318070da4e118cd28aa86d7f0702ed8cccc39d60906b93197687248\",\"dweb:/ipfs/QmWimBXYsDWDtNgzhjaLUx4xEiTMvbx5635E2TfNBKoV1E\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.25+commit.b61c2a91"},"language":"Solidity","output":{"abi":[{"inputs":[{"internalType":"struct Identifier","name":"_id","type":"tuple","components":[{"internalType":"address","name":"origin","type":"address"},{"internalType":"uint256","name":"blockNumber","type":"uint256"},{"internalType":"uint256","name":"logIndex","type":"uint256"},{"internalType":"uint256","name":"timestamp","type":"uint256"},{"internalType":"uint256","name":"chainId","type":"uint256"}]},{"internalType":"address","name":"_target","type":"address"},{"internalType":"bytes","name":"_message","type":"bytes"}],"stateMutability":"payable","type":"function","name":"executeMessage"},{"inputs":[{"internalType":"struct Identifier","name":"_id","type":"tuple","components":[{"internalType":"address","name":"origin","type":"address"},{"internalType":"uint256","name":"blockNumber","type":"uint256"},{"internalType":"uint256","name":"logIndex","type":"uint256"},{"internalType":"uint256","name":"timestamp","type":"uint256"},{"internalType":"uint256","name":"chainId","type":"uint256"}]},{"internalType":"bytes32","name":"_msgHash","type":"bytes32"}],"stateMutability":"nonpayable","type":"function","name":"validateMessage"}],"devdoc":{"kind":"dev","methods":{"executeMessage((address,uint256,uint256,uint256,uint256),address,bytes)":{"params":{"_id":"An Identifier pointing to the initiating message.","_message":"The message payload, matching the initiating message.","_target":"Account that is called with _msg."}},"validateMessage((address,uint256,uint256,uint256,uint256),bytes32)":{"params":{"_id":"Identifier of the message.","_msgHash":"Hash of the message payload to call target with."}}},"version":1},"userdoc":{"kind":"user","methods":{"executeMessage((address,uint256,uint256,uint256,uint256),address,bytes)":{"notice":"Executes a cross chain message on the destination chain."},"validateMessage((address,uint256,uint256,uint256,uint256),bytes32)":{"notice":"Validates a cross chain message on the destination chain and emits an ExecutingMessage event. This function is useful for applications that understand the schema of the _message payload and want to process it in a custom way."}},"version":1}},"settings":{"remappings":[],"optimizer":{"enabled":true,"runs":999999},"metadata":{"bytecodeHash":"none"},"compilationTarget":{"src/ICrossL2Inbox.sol":"ICrossL2Inbox"},"evmVersion":"cancun","libraries":{}},"sources":{"src/ICrossL2Inbox.sol":{"keccak256":"0x97daf76e4a10b96d8062e71df352cbfa7577593fa96676fd43b40f4a0aca9b7f","urls":["bzz-raw://74518c974318070da4e118cd28aa86d7f0702ed8cccc39d60906b93197687248","dweb:/ipfs/QmWimBXYsDWDtNgzhjaLUx4xEiTMvbx5635E2TfNBKoV1E"],"license":"MIT"}},"version":1},"storageLayout":{"storage":[],"types":{}},"userdoc":{"version":1,"kind":"user","methods":{"executeMessage((address,uint256,uint256,uint256,uint256),address,bytes)":{"notice":"Executes a cross chain message on the destination chain."},"validateMessage((address,uint256,uint256,uint256,uint256),bytes32)":{"notice":"Validates a cross chain message on the destination chain and emits an ExecutingMessage event. This function is useful for applications that understand the schema of the _message payload and want to process it in a custom way."}},"notice":"Interface for the CrossL2Inbox contract."},"devdoc":{"version":1,"kind":"dev","methods":{"executeMessage((address,uint256,uint256,uint256,uint256),address,bytes)":{"params":{"_id":"An Identifier pointing to the initiating message.","_message":"The message payload, matching the initiating message.","_target":"Account that is called with _msg."}},"validateMessage((address,uint256,uint256,uint256,uint256),bytes32)":{"params":{"_id":"Identifier of the message.","_msgHash":"Hash of the message payload to call target with."}}},"title":"ICrossL2Inbox"},"ast":{"absolutePath":"src/ICrossL2Inbox.sol","id":35,"exportedSymbols":{"ICrossL2Inbox":[34],"Identifier":[12]},"nodeType":"SourceUnit","src":"32:1153:0","nodes":[{"id":1,"nodeType":"PragmaDirective","src":"32:23:0","nodes":[],"literals":["solidity","^","0.8",".0"]},{"id":12,"nodeType":"StructDefinition","src":"57:132:0","nodes":[],"canonicalName":"Identifier","members":[{"constant":false,"id":3,"mutability":"mutable","name":"origin","nameLocation":"89:6:0","nodeType":"VariableDeclaration","scope":12,"src":"81:14:0","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"},"typeName":{"id":2,"name":"address","nodeType":"ElementaryTypeName","src":"81:7:0","stateMutability":"nonpayable","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"visibility":"internal"},{"constant":false,"id":5,"mutability":"mutable","name":"blockNumber","nameLocation":"109:11:0","nodeType":"VariableDeclaration","scope":12,"src":"101:19:0","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"},"typeName":{"id":4,"name":"uint256","nodeType":"ElementaryTypeName","src":"101:7:0","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"visibility":"internal"},{"constant":false,"id":7,"mutability":"mutable","name":"logIndex","nameLocation":"134:8:0","nodeType":"VariableDeclaration","scope":12,"src":"126:16:0","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"},"typeName":{"id":6,"name":"uint256","nodeType":"ElementaryTypeName","src":"126:7:0","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"visibility":"internal"},{"constant":false,"id":9,"mutability":"mutable","name":"timestamp","nameLocation":"156:9:0","nodeType":"VariableDeclaration","scope":12,"src":"148:17:0","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"},"typeName":{"id":8,"name":"uint256","nodeType":"ElementaryTypeName","src":"148:7:0","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"visibility":"internal"},{"constant":false,"id":11,"mutability":"mutable","name":"chainId","nameLocation":"179:7:0","nodeType":"VariableDeclaration","scope":12,"src":"171:15:0","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"},"typeName":{"id":10,"name":"uint256","nodeType":"ElementaryTypeName","src":"171:7:0","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"visibility":"internal"}],"name":"Identifier","nameLocation":"64:10:0","scope":35,"visibility":"public"},{"id":34,"nodeType":"ContractDefinition","src":"269:915:0","nodes":[{"id":24,"nodeType":"FunctionDefinition","src":"577:108:0","nodes":[],"documentation":{"id":14,"nodeType":"StructuredDocumentation","src":"300:272:0","text":"@notice Executes a cross chain message on the destination chain.\n @param _id An Identifier pointing to the initiating message.\n @param _target Account that is called with _msg.\n @param _message The message payload, matching the initiating message."},"functionSelector":"5984c53e","implemented":false,"kind":"function","modifiers":[],"name":"executeMessage","nameLocation":"586:14:0","parameters":{"id":22,"nodeType":"ParameterList","parameters":[{"constant":false,"id":17,"mutability":"mutable","name":"_id","nameLocation":"621:3:0","nodeType":"VariableDeclaration","scope":24,"src":"601:23:0","stateVariable":false,"storageLocation":"calldata","typeDescriptions":{"typeIdentifier":"t_struct$_Identifier_$12_calldata_ptr","typeString":"struct Identifier"},"typeName":{"id":16,"nodeType":"UserDefinedTypeName","pathNode":{"id":15,"name":"Identifier","nameLocations":["601:10:0"],"nodeType":"IdentifierPath","referencedDeclaration":12,"src":"601:10:0"},"referencedDeclaration":12,"src":"601:10:0","typeDescriptions":{"typeIdentifier":"t_struct$_Identifier_$12_storage_ptr","typeString":"struct Identifier"}},"visibility":"internal"},{"constant":false,"id":19,"mutability":"mutable","name":"_target","nameLocation":"634:7:0","nodeType":"VariableDeclaration","scope":24,"src":"626:15:0","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"},"typeName":{"id":18,"name":"address","nodeType":"ElementaryTypeName","src":"626:7:0","stateMutability":"nonpayable","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"visibility":"internal"},{"constant":false,"id":21,"mutability":"mutable","name":"_message","nameLocation":"658:8:0","nodeType":"VariableDeclaration","scope":24,"src":"643:23:0","stateVariable":false,"storageLocation":"calldata","typeDescriptions":{"typeIdentifier":"t_bytes_calldata_ptr","typeString":"bytes"},"typeName":{"id":20,"name":"bytes","nodeType":"ElementaryTypeName","src":"643:5:0","typeDescriptions":{"typeIdentifier":"t_bytes_storage_ptr","typeString":"bytes"}},"visibility":"internal"}],"src":"600:67:0"},"returnParameters":{"id":23,"nodeType":"ParameterList","parameters":[],"src":"684:0:0"},"scope":34,"stateMutability":"payable","virtual":false,"visibility":"external"},{"id":33,"nodeType":"FunctionDefinition","src":"1105:77:0","nodes":[],"documentation":{"id":25,"nodeType":"StructuredDocumentation","src":"691:409:0","text":"@notice Validates a cross chain message on the destination chain\n and emits an ExecutingMessage event. This function is useful\n for applications that understand the schema of the _message payload and want to\n process it in a custom way.\n @param _id Identifier of the message.\n @param _msgHash Hash of the message payload to call target with."},"functionSelector":"ab4d6f75","implemented":false,"kind":"function","modifiers":[],"name":"validateMessage","nameLocation":"1114:15:0","parameters":{"id":31,"nodeType":"ParameterList","parameters":[{"constant":false,"id":28,"mutability":"mutable","name":"_id","nameLocation":"1150:3:0","nodeType":"VariableDeclaration","scope":33,"src":"1130:23:0","stateVariable":false,"storageLocation":"calldata","typeDescriptions":{"typeIdentifier":"t_struct$_Identifier_$12_calldata_ptr","typeString":"struct Identifier"},"typeName":{"id":27,"nodeType":"UserDefinedTypeName","pathNode":{"id":26,"name":"Identifier","nameLocations":["1130:10:0"],"nodeType":"IdentifierPath","referencedDeclaration":12,"src":"1130:10:0"},"referencedDeclaration":12,"src":"1130:10:0","typeDescriptions":{"typeIdentifier":"t_struct$_Identifier_$12_storage_ptr","typeString":"struct Identifier"}},"visibility":"internal"},{"constant":false,"id":30,"mutability":"mutable","name":"_msgHash","nameLocation":"1163:8:0","nodeType":"VariableDeclaration","scope":33,"src":"1155:16:0","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"},"typeName":{"id":29,"name":"bytes32","nodeType":"ElementaryTypeName","src":"1155:7:0","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},"visibility":"internal"}],"src":"1129:43:0"},"returnParameters":{"id":32,"nodeType":"ParameterList","parameters":[],"src":"1181:0:0"},"scope":34,"stateMutability":"nonpayable","virtual":false,"visibility":"external"}],"abstract":false,"baseContracts":[],"canonicalName":"ICrossL2Inbox","contractDependencies":[],"contractKind":"interface","documentation":{"id":13,"nodeType":"StructuredDocumentation","src":"191:78:0","text":"@title ICrossL2Inbox\n @notice Interface for the CrossL2Inbox contract."},"fullyImplemented":false,"linearizedBaseContracts":[34],"name":"ICrossL2Inbox","nameLocation":"279:13:0","scope":35,"usedErrors":[],"usedEvents":[]}],"license":"MIT"},"id":0} \ No newline at end of file +{"abi":[{"type":"function","name":"validateMessage","inputs":[{"name":"_id","type":"tuple","internalType":"struct Identifier","components":[{"name":"origin","type":"address","internalType":"address"},{"name":"blockNumber","type":"uint256","internalType":"uint256"},{"name":"logIndex","type":"uint256","internalType":"uint256"},{"name":"timestamp","type":"uint256","internalType":"uint256"},{"name":"chainId","type":"uint256","internalType":"uint256"}]},{"name":"_msgHash","type":"bytes32","internalType":"bytes32"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"version","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"event","name":"ExecutingMessage","inputs":[{"name":"msgHash","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"id","type":"tuple","indexed":false,"internalType":"struct Identifier","components":[{"name":"origin","type":"address","internalType":"address"},{"name":"blockNumber","type":"uint256","internalType":"uint256"},{"name":"logIndex","type":"uint256","internalType":"uint256"},{"name":"timestamp","type":"uint256","internalType":"uint256"},{"name":"chainId","type":"uint256","internalType":"uint256"}]}],"anonymous":false},{"type":"error","name":"BlockNumberTooHigh","inputs":[]},{"type":"error","name":"LogIndexTooHigh","inputs":[]},{"type":"error","name":"NoExecutingDeposits","inputs":[]},{"type":"error","name":"NotInAccessList","inputs":[]},{"type":"error","name":"TimestampTooHigh","inputs":[]}],"bytecode":{"object":"0x","sourceMap":"","linkReferences":{}},"deployedBytecode":{"object":"0x","sourceMap":"","linkReferences":{}},"methodIdentifiers":{"validateMessage((address,uint256,uint256,uint256,uint256),bytes32)":"ab4d6f75","version()":"54fd4d50"},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.25+commit.b61c2a91\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[],\"name\":\"BlockNumberTooHigh\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"LogIndexTooHigh\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"NoExecutingDeposits\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"NotInAccessList\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"TimestampTooHigh\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"msgHash\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"origin\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"logIndex\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"timestamp\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"chainId\",\"type\":\"uint256\"}],\"indexed\":false,\"internalType\":\"struct Identifier\",\"name\":\"id\",\"type\":\"tuple\"}],\"name\":\"ExecutingMessage\",\"type\":\"event\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"origin\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"logIndex\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"timestamp\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"chainId\",\"type\":\"uint256\"}],\"internalType\":\"struct Identifier\",\"name\":\"_id\",\"type\":\"tuple\"},{\"internalType\":\"bytes32\",\"name\":\"_msgHash\",\"type\":\"bytes32\"}],\"name\":\"validateMessage\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"version\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"}],\"devdoc\":{\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"src/ICrossL2Inbox.sol\":\"ICrossL2Inbox\"},\"evmVersion\":\"cancun\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"none\"},\"optimizer\":{\"enabled\":true,\"runs\":999999},\"remappings\":[]},\"sources\":{\"src/ICrossL2Inbox.sol\":{\"keccak256\":\"0x9d31923d67c620293adcc180d6f6219f196dcca4d81425b932113537801749de\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://10456b2586608b6336f58ba78fa481c70ca0da48a459b9be42b2b52da738adfe\",\"dweb:/ipfs/QmVNkgm9YFv2HCYNEipCUbP3jdkC3Ma1XWLvWKVnb3xi8t\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.25+commit.b61c2a91"},"language":"Solidity","output":{"abi":[{"inputs":[],"type":"error","name":"BlockNumberTooHigh"},{"inputs":[],"type":"error","name":"LogIndexTooHigh"},{"inputs":[],"type":"error","name":"NoExecutingDeposits"},{"inputs":[],"type":"error","name":"NotInAccessList"},{"inputs":[],"type":"error","name":"TimestampTooHigh"},{"inputs":[{"internalType":"bytes32","name":"msgHash","type":"bytes32","indexed":true},{"internalType":"struct Identifier","name":"id","type":"tuple","components":[{"internalType":"address","name":"origin","type":"address"},{"internalType":"uint256","name":"blockNumber","type":"uint256"},{"internalType":"uint256","name":"logIndex","type":"uint256"},{"internalType":"uint256","name":"timestamp","type":"uint256"},{"internalType":"uint256","name":"chainId","type":"uint256"}],"indexed":false}],"type":"event","name":"ExecutingMessage","anonymous":false},{"inputs":[{"internalType":"struct Identifier","name":"_id","type":"tuple","components":[{"internalType":"address","name":"origin","type":"address"},{"internalType":"uint256","name":"blockNumber","type":"uint256"},{"internalType":"uint256","name":"logIndex","type":"uint256"},{"internalType":"uint256","name":"timestamp","type":"uint256"},{"internalType":"uint256","name":"chainId","type":"uint256"}]},{"internalType":"bytes32","name":"_msgHash","type":"bytes32"}],"stateMutability":"nonpayable","type":"function","name":"validateMessage"},{"inputs":[],"stateMutability":"view","type":"function","name":"version","outputs":[{"internalType":"string","name":"","type":"string"}]}],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":[],"optimizer":{"enabled":true,"runs":999999},"metadata":{"bytecodeHash":"none"},"compilationTarget":{"src/ICrossL2Inbox.sol":"ICrossL2Inbox"},"evmVersion":"cancun","libraries":{}},"sources":{"src/ICrossL2Inbox.sol":{"keccak256":"0x9d31923d67c620293adcc180d6f6219f196dcca4d81425b932113537801749de","urls":["bzz-raw://10456b2586608b6336f58ba78fa481c70ca0da48a459b9be42b2b52da738adfe","dweb:/ipfs/QmVNkgm9YFv2HCYNEipCUbP3jdkC3Ma1XWLvWKVnb3xi8t"],"license":"MIT"}},"version":1},"storageLayout":{"storage":[],"types":{}},"userdoc":{"version":1,"kind":"user"},"devdoc":{"version":1,"kind":"dev"},"ast":{"absolutePath":"src/ICrossL2Inbox.sol","id":45,"exportedSymbols":{"ICrossL2Inbox":[44],"Identifier":[13]},"nodeType":"SourceUnit","src":"32:604:0","nodes":[{"id":1,"nodeType":"PragmaDirective","src":"32:23:0","nodes":[],"literals":["solidity","^","0.8",".0"]},{"id":13,"nodeType":"StructDefinition","src":"106:132:0","nodes":[],"canonicalName":"Identifier","documentation":{"id":2,"nodeType":"StructuredDocumentation","src":"57:49:0","text":"@notice Identifier of a cross chain message."},"members":[{"constant":false,"id":4,"mutability":"mutable","name":"origin","nameLocation":"138:6:0","nodeType":"VariableDeclaration","scope":13,"src":"130:14:0","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"},"typeName":{"id":3,"name":"address","nodeType":"ElementaryTypeName","src":"130:7:0","stateMutability":"nonpayable","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"visibility":"internal"},{"constant":false,"id":6,"mutability":"mutable","name":"blockNumber","nameLocation":"158:11:0","nodeType":"VariableDeclaration","scope":13,"src":"150:19:0","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"},"typeName":{"id":5,"name":"uint256","nodeType":"ElementaryTypeName","src":"150:7:0","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"visibility":"internal"},{"constant":false,"id":8,"mutability":"mutable","name":"logIndex","nameLocation":"183:8:0","nodeType":"VariableDeclaration","scope":13,"src":"175:16:0","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"},"typeName":{"id":7,"name":"uint256","nodeType":"ElementaryTypeName","src":"175:7:0","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"visibility":"internal"},{"constant":false,"id":10,"mutability":"mutable","name":"timestamp","nameLocation":"205:9:0","nodeType":"VariableDeclaration","scope":13,"src":"197:17:0","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"},"typeName":{"id":9,"name":"uint256","nodeType":"ElementaryTypeName","src":"197:7:0","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"visibility":"internal"},{"constant":false,"id":12,"mutability":"mutable","name":"chainId","nameLocation":"228:7:0","nodeType":"VariableDeclaration","scope":13,"src":"220:15:0","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"},"typeName":{"id":11,"name":"uint256","nodeType":"ElementaryTypeName","src":"220:7:0","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"visibility":"internal"}],"name":"Identifier","nameLocation":"113:10:0","scope":45,"visibility":"public"},{"id":44,"nodeType":"ContractDefinition","src":"240:395:0","nodes":[{"id":15,"nodeType":"ErrorDefinition","src":"270:28:0","nodes":[],"errorSelector":"753f1072","name":"NoExecutingDeposits","nameLocation":"276:19:0","parameters":{"id":14,"nodeType":"ParameterList","parameters":[],"src":"295:2:0"}},{"id":17,"nodeType":"ErrorDefinition","src":"303:24:0","nodes":[],"errorSelector":"e3c00816","name":"NotInAccessList","nameLocation":"309:15:0","parameters":{"id":16,"nodeType":"ParameterList","parameters":[],"src":"324:2:0"}},{"id":19,"nodeType":"ErrorDefinition","src":"332:27:0","nodes":[],"errorSelector":"d1f79e82","name":"BlockNumberTooHigh","nameLocation":"338:18:0","parameters":{"id":18,"nodeType":"ParameterList","parameters":[],"src":"356:2:0"}},{"id":21,"nodeType":"ErrorDefinition","src":"364:25:0","nodes":[],"errorSelector":"596a19a9","name":"TimestampTooHigh","nameLocation":"370:16:0","parameters":{"id":20,"nodeType":"ParameterList","parameters":[],"src":"386:2:0"}},{"id":23,"nodeType":"ErrorDefinition","src":"394:24:0","nodes":[],"errorSelector":"94338eba","name":"LogIndexTooHigh","nameLocation":"400:15:0","parameters":{"id":22,"nodeType":"ParameterList","parameters":[],"src":"415:2:0"}},{"id":30,"nodeType":"EventDefinition","src":"424:63:0","nodes":[],"anonymous":false,"eventSelector":"5c37832d2e8d10e346e55ad62071a6a2f9fa5130614ef2ec6617555c6f467ba7","name":"ExecutingMessage","nameLocation":"430:16:0","parameters":{"id":29,"nodeType":"ParameterList","parameters":[{"constant":false,"id":25,"indexed":true,"mutability":"mutable","name":"msgHash","nameLocation":"463:7:0","nodeType":"VariableDeclaration","scope":30,"src":"447:23:0","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"},"typeName":{"id":24,"name":"bytes32","nodeType":"ElementaryTypeName","src":"447:7:0","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},"visibility":"internal"},{"constant":false,"id":28,"indexed":false,"mutability":"mutable","name":"id","nameLocation":"483:2:0","nodeType":"VariableDeclaration","scope":30,"src":"472:13:0","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_struct$_Identifier_$13_memory_ptr","typeString":"struct Identifier"},"typeName":{"id":27,"nodeType":"UserDefinedTypeName","pathNode":{"id":26,"name":"Identifier","nameLocations":["472:10:0"],"nodeType":"IdentifierPath","referencedDeclaration":13,"src":"472:10:0"},"referencedDeclaration":13,"src":"472:10:0","typeDescriptions":{"typeIdentifier":"t_struct$_Identifier_$13_storage_ptr","typeString":"struct Identifier"}},"visibility":"internal"}],"src":"446:40:0"}},{"id":35,"nodeType":"FunctionDefinition","src":"493:57:0","nodes":[],"functionSelector":"54fd4d50","implemented":false,"kind":"function","modifiers":[],"name":"version","nameLocation":"502:7:0","parameters":{"id":31,"nodeType":"ParameterList","parameters":[],"src":"509:2:0"},"returnParameters":{"id":34,"nodeType":"ParameterList","parameters":[{"constant":false,"id":33,"mutability":"mutable","name":"","nameLocation":"-1:-1:-1","nodeType":"VariableDeclaration","scope":35,"src":"535:13:0","stateVariable":false,"storageLocation":"memory","typeDescriptions":{"typeIdentifier":"t_string_memory_ptr","typeString":"string"},"typeName":{"id":32,"name":"string","nodeType":"ElementaryTypeName","src":"535:6:0","typeDescriptions":{"typeIdentifier":"t_string_storage_ptr","typeString":"string"}},"visibility":"internal"}],"src":"534:15:0"},"scope":44,"stateMutability":"view","virtual":false,"visibility":"external"},{"id":43,"nodeType":"FunctionDefinition","src":"556:77:0","nodes":[],"functionSelector":"ab4d6f75","implemented":false,"kind":"function","modifiers":[],"name":"validateMessage","nameLocation":"565:15:0","parameters":{"id":41,"nodeType":"ParameterList","parameters":[{"constant":false,"id":38,"mutability":"mutable","name":"_id","nameLocation":"601:3:0","nodeType":"VariableDeclaration","scope":43,"src":"581:23:0","stateVariable":false,"storageLocation":"calldata","typeDescriptions":{"typeIdentifier":"t_struct$_Identifier_$13_calldata_ptr","typeString":"struct Identifier"},"typeName":{"id":37,"nodeType":"UserDefinedTypeName","pathNode":{"id":36,"name":"Identifier","nameLocations":["581:10:0"],"nodeType":"IdentifierPath","referencedDeclaration":13,"src":"581:10:0"},"referencedDeclaration":13,"src":"581:10:0","typeDescriptions":{"typeIdentifier":"t_struct$_Identifier_$13_storage_ptr","typeString":"struct Identifier"}},"visibility":"internal"},{"constant":false,"id":40,"mutability":"mutable","name":"_msgHash","nameLocation":"614:8:0","nodeType":"VariableDeclaration","scope":43,"src":"606:16:0","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"},"typeName":{"id":39,"name":"bytes32","nodeType":"ElementaryTypeName","src":"606:7:0","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},"visibility":"internal"}],"src":"580:43:0"},"returnParameters":{"id":42,"nodeType":"ParameterList","parameters":[],"src":"632:0:0"},"scope":44,"stateMutability":"nonpayable","virtual":false,"visibility":"external"}],"abstract":false,"baseContracts":[],"canonicalName":"ICrossL2Inbox","contractDependencies":[],"contractKind":"interface","fullyImplemented":false,"linearizedBaseContracts":[44],"name":"ICrossL2Inbox","nameLocation":"250:13:0","scope":45,"usedErrors":[15,17,19,21,23],"usedEvents":[30]}],"license":"MIT"},"id":0} \ No newline at end of file diff --git a/op-e2e/e2eutils/interop/contracts/build/ISystemConfig.sol/ISystemConfig.abi b/op-e2e/e2eutils/interop/contracts/build/ISystemConfig.sol/ISystemConfig.abi deleted file mode 100644 index 7a7bffc2ca..0000000000 --- a/op-e2e/e2eutils/interop/contracts/build/ISystemConfig.sol/ISystemConfig.abi +++ /dev/null @@ -1,15 +0,0 @@ -[ - { - "type": "function", - "name": "addDependency", - "inputs": [ - { - "name": "_chainId", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - } -] diff --git a/op-e2e/e2eutils/interop/contracts/build/ISystemConfig.sol/ISystemConfig.bin b/op-e2e/e2eutils/interop/contracts/build/ISystemConfig.sol/ISystemConfig.bin deleted file mode 100644 index ec687260b8..0000000000 --- a/op-e2e/e2eutils/interop/contracts/build/ISystemConfig.sol/ISystemConfig.bin +++ /dev/null @@ -1 +0,0 @@ -0x diff --git a/op-e2e/e2eutils/interop/contracts/build/ISystemConfig.sol/ISystemConfig.json b/op-e2e/e2eutils/interop/contracts/build/ISystemConfig.sol/ISystemConfig.json deleted file mode 100644 index fef67d0a40..0000000000 --- a/op-e2e/e2eutils/interop/contracts/build/ISystemConfig.sol/ISystemConfig.json +++ /dev/null @@ -1 +0,0 @@ -{"abi":[{"type":"function","name":"addDependency","inputs":[{"name":"_chainId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"}],"bytecode":{"object":"0x","sourceMap":"","linkReferences":{}},"deployedBytecode":{"object":"0x","sourceMap":"","linkReferences":{}},"methodIdentifiers":{"addDependency(uint256)":"a89c793c"},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.25+commit.b61c2a91\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_chainId\",\"type\":\"uint256\"}],\"name\":\"addDependency\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}],\"devdoc\":{\"kind\":\"dev\",\"methods\":{\"addDependency(uint256)\":{\"params\":{\"_chainId\":\"Chain ID of chain to add.\"}}},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{\"addDependency(uint256)\":{\"notice\":\"Adds a chain to the interop dependency set. Can only be called by the dependency manager.\"}},\"version\":1}},\"settings\":{\"compilationTarget\":{\"src/ISystemConfig.sol\":\"ISystemConfig\"},\"evmVersion\":\"cancun\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"none\"},\"optimizer\":{\"enabled\":true,\"runs\":999999},\"remappings\":[]},\"sources\":{\"src/ISystemConfig.sol\":{\"keccak256\":\"0x764ea7e528e9e5ab907515b4da22738a49a3d5d8ffb948def86d0cbb9e7765be\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://9307f5fd1e826f8d059f4aa9141c360aef54612ba8562ec996e7f078ef1d1cf2\",\"dweb:/ipfs/QmbzFSWWLoQUyeuD6U7detgjPy36L3DHJp9BPpaUZqVs68\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.25+commit.b61c2a91"},"language":"Solidity","output":{"abi":[{"inputs":[{"internalType":"uint256","name":"_chainId","type":"uint256"}],"stateMutability":"nonpayable","type":"function","name":"addDependency"}],"devdoc":{"kind":"dev","methods":{"addDependency(uint256)":{"params":{"_chainId":"Chain ID of chain to add."}}},"version":1},"userdoc":{"kind":"user","methods":{"addDependency(uint256)":{"notice":"Adds a chain to the interop dependency set. Can only be called by the dependency manager."}},"version":1}},"settings":{"remappings":[],"optimizer":{"enabled":true,"runs":999999},"metadata":{"bytecodeHash":"none"},"compilationTarget":{"src/ISystemConfig.sol":"ISystemConfig"},"evmVersion":"cancun","libraries":{}},"sources":{"src/ISystemConfig.sol":{"keccak256":"0x764ea7e528e9e5ab907515b4da22738a49a3d5d8ffb948def86d0cbb9e7765be","urls":["bzz-raw://9307f5fd1e826f8d059f4aa9141c360aef54612ba8562ec996e7f078ef1d1cf2","dweb:/ipfs/QmbzFSWWLoQUyeuD6U7detgjPy36L3DHJp9BPpaUZqVs68"],"license":"MIT"}},"version":1},"storageLayout":{"storage":[],"types":{}},"userdoc":{"version":1,"kind":"user","methods":{"addDependency(uint256)":{"notice":"Adds a chain to the interop dependency set. Can only be called by the dependency manager."}}},"devdoc":{"version":1,"kind":"dev","methods":{"addDependency(uint256)":{"params":{"_chainId":"Chain ID of chain to add."}}}},"ast":{"absolutePath":"src/ISystemConfig.sol","id":9,"exportedSymbols":{"ISystemConfig":[8]},"nodeType":"SourceUnit","src":"32:264:0","nodes":[{"id":1,"nodeType":"PragmaDirective","src":"32:23:0","nodes":[],"literals":["solidity","^","0.8",".0"]},{"id":8,"nodeType":"ContractDefinition","src":"57:238:0","nodes":[{"id":7,"nodeType":"FunctionDefinition","src":"243:50:0","nodes":[],"documentation":{"id":2,"nodeType":"StructuredDocumentation","src":"87:151:0","text":"@notice Adds a chain to the interop dependency set. Can only be called by the dependency manager.\n @param _chainId Chain ID of chain to add."},"functionSelector":"a89c793c","implemented":false,"kind":"function","modifiers":[],"name":"addDependency","nameLocation":"252:13:0","parameters":{"id":5,"nodeType":"ParameterList","parameters":[{"constant":false,"id":4,"mutability":"mutable","name":"_chainId","nameLocation":"274:8:0","nodeType":"VariableDeclaration","scope":7,"src":"266:16:0","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"},"typeName":{"id":3,"name":"uint256","nodeType":"ElementaryTypeName","src":"266:7:0","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"visibility":"internal"}],"src":"265:18:0"},"returnParameters":{"id":6,"nodeType":"ParameterList","parameters":[],"src":"292:0:0"},"scope":8,"stateMutability":"nonpayable","virtual":false,"visibility":"external"}],"abstract":false,"baseContracts":[],"canonicalName":"ISystemConfig","contractDependencies":[],"contractKind":"interface","fullyImplemented":false,"linearizedBaseContracts":[8],"name":"ISystemConfig","nameLocation":"67:13:0","scope":9,"usedErrors":[],"usedEvents":[]}],"license":"MIT"},"id":0} \ No newline at end of file diff --git a/op-e2e/e2eutils/interop/contracts/build/emit.sol/EmitEvent.json b/op-e2e/e2eutils/interop/contracts/build/emit.sol/EmitEvent.json index 465823afa3..2c4e3a26da 100644 --- a/op-e2e/e2eutils/interop/contracts/build/emit.sol/EmitEvent.json +++ b/op-e2e/e2eutils/interop/contracts/build/emit.sol/EmitEvent.json @@ -1 +1 @@ -{"abi":[{"type":"function","name":"emitData","inputs":[{"name":"_data","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"DataEmitted","inputs":[{"name":"_data","type":"bytes","indexed":true,"internalType":"bytes"}],"anonymous":false}],"bytecode":{"object":"0x6080604052348015600e575f80fd5b5060ff8061001b5f395ff3fe6080604052348015600e575f80fd5b50600436106026575f3560e01c8063d836083e14602a575b5f80fd5b60396035366004607c565b603b565b005b8181604051604992919060e3565b604051908190038120907fe00bbfe6f6f8f1bbed2da38e3f5a139c6f9da594ab248a3cf8b44fc73627772c905f90a25050565b5f8060208385031215608c575f80fd5b823567ffffffffffffffff8082111560a2575f80fd5b818501915085601f83011260b4575f80fd5b81358181111560c1575f80fd5b86602082850101111560d1575f80fd5b60209290920196919550909350505050565b818382375f910190815291905056fea164736f6c6343000819000a","sourceMap":"58:278:1:-:0;;;;;;;;;;;;;;;;;;;","linkReferences":{}},"deployedBytecode":{"object":"0x6080604052348015600e575f80fd5b50600436106026575f3560e01c8063d836083e14602a575b5f80fd5b60396035366004607c565b603b565b005b8181604051604992919060e3565b604051908190038120907fe00bbfe6f6f8f1bbed2da38e3f5a139c6f9da594ab248a3cf8b44fc73627772c905f90a25050565b5f8060208385031215608c575f80fd5b823567ffffffffffffffff8082111560a2575f80fd5b818501915085601f83011260b4575f80fd5b81358181111560c1575f80fd5b86602082850101111560d1575f80fd5b60209290920196919550909350505050565b818382375f910190815291905056fea164736f6c6343000819000a","sourceMap":"58:278:1:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;245:89;;;;;;:::i;:::-;;:::i;:::-;;;321:5;;309:18;;;;;;;:::i;:::-;;;;;;;;;;;;;;;245:89;;:::o;14:591:2:-;84:6;92;145:2;133:9;124:7;120:23;116:32;113:52;;;161:1;158;151:12;113:52;201:9;188:23;230:18;271:2;263:6;260:14;257:34;;;287:1;284;277:12;257:34;325:6;314:9;310:22;300:32;;370:7;363:4;359:2;355:13;351:27;341:55;;392:1;389;382:12;341:55;432:2;419:16;458:2;450:6;447:14;444:34;;;474:1;471;464:12;444:34;519:7;514:2;505:6;501:2;497:15;493:24;490:37;487:57;;;540:1;537;530:12;487:57;571:2;563:11;;;;;593:6;;-1:-1:-1;14:591:2;;-1:-1:-1;;;;14:591:2:o;610:271::-;793:6;785;780:3;767:33;749:3;819:16;;844:13;;;819:16;610:271;-1:-1:-1;610:271:2:o","linkReferences":{}},"methodIdentifiers":{"emitData(bytes)":"d836083e"},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.25+commit.b61c2a91\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes\",\"name\":\"_data\",\"type\":\"bytes\"}],\"name\":\"DataEmitted\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"_data\",\"type\":\"bytes\"}],\"name\":\"emitData\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}],\"devdoc\":{\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"src/emit.sol\":\"EmitEvent\"},\"evmVersion\":\"cancun\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"none\"},\"optimizer\":{\"enabled\":true,\"runs\":999999},\"remappings\":[]},\"sources\":{\"src/emit.sol\":{\"keccak256\":\"0xe078378fd445ed0cbbf1087c5013110412ab6a44850af24a15bcde945467accc\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://66c218de0059688c75903c2ba6d4066661dc6f5fa17a329dd7c385d151f3993a\",\"dweb:/ipfs/Qmby4S4N44naZ2miw3Tgdpq5Qbj4DwzJbcGapgqtd7qd26\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.25+commit.b61c2a91"},"language":"Solidity","output":{"abi":[{"inputs":[{"internalType":"bytes","name":"_data","type":"bytes","indexed":true}],"type":"event","name":"DataEmitted","anonymous":false},{"inputs":[{"internalType":"bytes","name":"_data","type":"bytes"}],"stateMutability":"nonpayable","type":"function","name":"emitData"}],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":[],"optimizer":{"enabled":true,"runs":999999},"metadata":{"bytecodeHash":"none"},"compilationTarget":{"src/emit.sol":"EmitEvent"},"evmVersion":"cancun","libraries":{}},"sources":{"src/emit.sol":{"keccak256":"0xe078378fd445ed0cbbf1087c5013110412ab6a44850af24a15bcde945467accc","urls":["bzz-raw://66c218de0059688c75903c2ba6d4066661dc6f5fa17a329dd7c385d151f3993a","dweb:/ipfs/Qmby4S4N44naZ2miw3Tgdpq5Qbj4DwzJbcGapgqtd7qd26"],"license":"MIT"}},"version":1},"storageLayout":{"storage":[],"types":{}},"userdoc":{"version":1,"kind":"user"},"devdoc":{"version":1,"kind":"dev"},"ast":{"absolutePath":"src/emit.sol","id":52,"exportedSymbols":{"EmitEvent":[51]},"nodeType":"SourceUnit","src":"32:305:1","nodes":[{"id":36,"nodeType":"PragmaDirective","src":"32:24:1","nodes":[],"literals":["solidity","^","0.8",".15"]},{"id":51,"nodeType":"ContractDefinition","src":"58:278:1","nodes":[{"id":40,"nodeType":"EventDefinition","src":"133:39:1","nodes":[],"anonymous":false,"eventSelector":"e00bbfe6f6f8f1bbed2da38e3f5a139c6f9da594ab248a3cf8b44fc73627772c","name":"DataEmitted","nameLocation":"139:11:1","parameters":{"id":39,"nodeType":"ParameterList","parameters":[{"constant":false,"id":38,"indexed":true,"mutability":"mutable","name":"_data","nameLocation":"165:5:1","nodeType":"VariableDeclaration","scope":40,"src":"151:19:1","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_bytes_memory_ptr","typeString":"bytes"},"typeName":{"id":37,"name":"bytes","nodeType":"ElementaryTypeName","src":"151:5:1","typeDescriptions":{"typeIdentifier":"t_bytes_storage_ptr","typeString":"bytes"}},"visibility":"internal"}],"src":"150:21:1"}},{"id":50,"nodeType":"FunctionDefinition","src":"245:89:1","nodes":[],"body":{"id":49,"nodeType":"Block","src":"294:40:1","nodes":[],"statements":[{"eventCall":{"arguments":[{"id":46,"name":"_data","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":42,"src":"321:5:1","typeDescriptions":{"typeIdentifier":"t_bytes_calldata_ptr","typeString":"bytes calldata"}}],"expression":{"argumentTypes":[{"typeIdentifier":"t_bytes_calldata_ptr","typeString":"bytes calldata"}],"id":45,"name":"DataEmitted","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":40,"src":"309:11:1","typeDescriptions":{"typeIdentifier":"t_function_event_nonpayable$_t_bytes_memory_ptr_$returns$__$","typeString":"function (bytes memory)"}},"id":47,"isConstant":false,"isLValue":false,"isPure":false,"kind":"functionCall","lValueRequested":false,"nameLocations":[],"names":[],"nodeType":"FunctionCall","src":"309:18:1","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_tuple$__$","typeString":"tuple()"}},"id":48,"nodeType":"EmitStatement","src":"304:23:1"}]},"functionSelector":"d836083e","implemented":true,"kind":"function","modifiers":[],"name":"emitData","nameLocation":"254:8:1","parameters":{"id":43,"nodeType":"ParameterList","parameters":[{"constant":false,"id":42,"mutability":"mutable","name":"_data","nameLocation":"278:5:1","nodeType":"VariableDeclaration","scope":50,"src":"263:20:1","stateVariable":false,"storageLocation":"calldata","typeDescriptions":{"typeIdentifier":"t_bytes_calldata_ptr","typeString":"bytes"},"typeName":{"id":41,"name":"bytes","nodeType":"ElementaryTypeName","src":"263:5:1","typeDescriptions":{"typeIdentifier":"t_bytes_storage_ptr","typeString":"bytes"}},"visibility":"internal"}],"src":"262:22:1"},"returnParameters":{"id":44,"nodeType":"ParameterList","parameters":[],"src":"294:0:1"},"scope":51,"stateMutability":"nonpayable","virtual":false,"visibility":"external"}],"abstract":false,"baseContracts":[],"canonicalName":"EmitEvent","contractDependencies":[],"contractKind":"contract","fullyImplemented":true,"linearizedBaseContracts":[51],"name":"EmitEvent","nameLocation":"67:9:1","scope":52,"usedErrors":[],"usedEvents":[40]}],"license":"MIT"},"id":1} \ No newline at end of file +{"abi":[{"type":"function","name":"emitData","inputs":[{"name":"_data","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"DataEmitted","inputs":[{"name":"_data","type":"bytes","indexed":true,"internalType":"bytes"}],"anonymous":false}],"bytecode":{"object":"0x6080604052348015600e575f80fd5b5060ff8061001b5f395ff3fe6080604052348015600e575f80fd5b50600436106026575f3560e01c8063d836083e14602a575b5f80fd5b60396035366004607c565b603b565b005b8181604051604992919060e3565b604051908190038120907fe00bbfe6f6f8f1bbed2da38e3f5a139c6f9da594ab248a3cf8b44fc73627772c905f90a25050565b5f8060208385031215608c575f80fd5b823567ffffffffffffffff8082111560a2575f80fd5b818501915085601f83011260b4575f80fd5b81358181111560c1575f80fd5b86602082850101111560d1575f80fd5b60209290920196919550909350505050565b818382375f910190815291905056fea164736f6c6343000819000a","sourceMap":"58:278:1:-:0;;;;;;;;;;;;;;;;;;;","linkReferences":{}},"deployedBytecode":{"object":"0x6080604052348015600e575f80fd5b50600436106026575f3560e01c8063d836083e14602a575b5f80fd5b60396035366004607c565b603b565b005b8181604051604992919060e3565b604051908190038120907fe00bbfe6f6f8f1bbed2da38e3f5a139c6f9da594ab248a3cf8b44fc73627772c905f90a25050565b5f8060208385031215608c575f80fd5b823567ffffffffffffffff8082111560a2575f80fd5b818501915085601f83011260b4575f80fd5b81358181111560c1575f80fd5b86602082850101111560d1575f80fd5b60209290920196919550909350505050565b818382375f910190815291905056fea164736f6c6343000819000a","sourceMap":"58:278:1:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;245:89;;;;;;:::i;:::-;;:::i;:::-;;;321:5;;309:18;;;;;;;:::i;:::-;;;;;;;;;;;;;;;245:89;;:::o;14:591:2:-;84:6;92;145:2;133:9;124:7;120:23;116:32;113:52;;;161:1;158;151:12;113:52;201:9;188:23;230:18;271:2;263:6;260:14;257:34;;;287:1;284;277:12;257:34;325:6;314:9;310:22;300:32;;370:7;363:4;359:2;355:13;351:27;341:55;;392:1;389;382:12;341:55;432:2;419:16;458:2;450:6;447:14;444:34;;;474:1;471;464:12;444:34;519:7;514:2;505:6;501:2;497:15;493:24;490:37;487:57;;;540:1;537;530:12;487:57;571:2;563:11;;;;;593:6;;-1:-1:-1;14:591:2;;-1:-1:-1;;;;14:591:2:o;610:271::-;793:6;785;780:3;767:33;749:3;819:16;;844:13;;;819:16;610:271;-1:-1:-1;610:271:2:o","linkReferences":{}},"methodIdentifiers":{"emitData(bytes)":"d836083e"},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.25+commit.b61c2a91\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes\",\"name\":\"_data\",\"type\":\"bytes\"}],\"name\":\"DataEmitted\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"_data\",\"type\":\"bytes\"}],\"name\":\"emitData\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}],\"devdoc\":{\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"src/emit.sol\":\"EmitEvent\"},\"evmVersion\":\"cancun\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"none\"},\"optimizer\":{\"enabled\":true,\"runs\":999999},\"remappings\":[]},\"sources\":{\"src/emit.sol\":{\"keccak256\":\"0xe078378fd445ed0cbbf1087c5013110412ab6a44850af24a15bcde945467accc\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://66c218de0059688c75903c2ba6d4066661dc6f5fa17a329dd7c385d151f3993a\",\"dweb:/ipfs/Qmby4S4N44naZ2miw3Tgdpq5Qbj4DwzJbcGapgqtd7qd26\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.25+commit.b61c2a91"},"language":"Solidity","output":{"abi":[{"inputs":[{"internalType":"bytes","name":"_data","type":"bytes","indexed":true}],"type":"event","name":"DataEmitted","anonymous":false},{"inputs":[{"internalType":"bytes","name":"_data","type":"bytes"}],"stateMutability":"nonpayable","type":"function","name":"emitData"}],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":[],"optimizer":{"enabled":true,"runs":999999},"metadata":{"bytecodeHash":"none"},"compilationTarget":{"src/emit.sol":"EmitEvent"},"evmVersion":"cancun","libraries":{}},"sources":{"src/emit.sol":{"keccak256":"0xe078378fd445ed0cbbf1087c5013110412ab6a44850af24a15bcde945467accc","urls":["bzz-raw://66c218de0059688c75903c2ba6d4066661dc6f5fa17a329dd7c385d151f3993a","dweb:/ipfs/Qmby4S4N44naZ2miw3Tgdpq5Qbj4DwzJbcGapgqtd7qd26"],"license":"MIT"}},"version":1},"storageLayout":{"storage":[],"types":{}},"userdoc":{"version":1,"kind":"user"},"devdoc":{"version":1,"kind":"dev"},"ast":{"absolutePath":"src/emit.sol","id":62,"exportedSymbols":{"EmitEvent":[61]},"nodeType":"SourceUnit","src":"32:305:1","nodes":[{"id":46,"nodeType":"PragmaDirective","src":"32:24:1","nodes":[],"literals":["solidity","^","0.8",".15"]},{"id":61,"nodeType":"ContractDefinition","src":"58:278:1","nodes":[{"id":50,"nodeType":"EventDefinition","src":"133:39:1","nodes":[],"anonymous":false,"eventSelector":"e00bbfe6f6f8f1bbed2da38e3f5a139c6f9da594ab248a3cf8b44fc73627772c","name":"DataEmitted","nameLocation":"139:11:1","parameters":{"id":49,"nodeType":"ParameterList","parameters":[{"constant":false,"id":48,"indexed":true,"mutability":"mutable","name":"_data","nameLocation":"165:5:1","nodeType":"VariableDeclaration","scope":50,"src":"151:19:1","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_bytes_memory_ptr","typeString":"bytes"},"typeName":{"id":47,"name":"bytes","nodeType":"ElementaryTypeName","src":"151:5:1","typeDescriptions":{"typeIdentifier":"t_bytes_storage_ptr","typeString":"bytes"}},"visibility":"internal"}],"src":"150:21:1"}},{"id":60,"nodeType":"FunctionDefinition","src":"245:89:1","nodes":[],"body":{"id":59,"nodeType":"Block","src":"294:40:1","nodes":[],"statements":[{"eventCall":{"arguments":[{"id":56,"name":"_data","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":52,"src":"321:5:1","typeDescriptions":{"typeIdentifier":"t_bytes_calldata_ptr","typeString":"bytes calldata"}}],"expression":{"argumentTypes":[{"typeIdentifier":"t_bytes_calldata_ptr","typeString":"bytes calldata"}],"id":55,"name":"DataEmitted","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":50,"src":"309:11:1","typeDescriptions":{"typeIdentifier":"t_function_event_nonpayable$_t_bytes_memory_ptr_$returns$__$","typeString":"function (bytes memory)"}},"id":57,"isConstant":false,"isLValue":false,"isPure":false,"kind":"functionCall","lValueRequested":false,"nameLocations":[],"names":[],"nodeType":"FunctionCall","src":"309:18:1","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_tuple$__$","typeString":"tuple()"}},"id":58,"nodeType":"EmitStatement","src":"304:23:1"}]},"functionSelector":"d836083e","implemented":true,"kind":"function","modifiers":[],"name":"emitData","nameLocation":"254:8:1","parameters":{"id":53,"nodeType":"ParameterList","parameters":[{"constant":false,"id":52,"mutability":"mutable","name":"_data","nameLocation":"278:5:1","nodeType":"VariableDeclaration","scope":60,"src":"263:20:1","stateVariable":false,"storageLocation":"calldata","typeDescriptions":{"typeIdentifier":"t_bytes_calldata_ptr","typeString":"bytes"},"typeName":{"id":51,"name":"bytes","nodeType":"ElementaryTypeName","src":"263:5:1","typeDescriptions":{"typeIdentifier":"t_bytes_storage_ptr","typeString":"bytes"}},"visibility":"internal"}],"src":"262:22:1"},"returnParameters":{"id":54,"nodeType":"ParameterList","parameters":[],"src":"294:0:1"},"scope":61,"stateMutability":"nonpayable","virtual":false,"visibility":"external"}],"abstract":false,"baseContracts":[],"canonicalName":"EmitEvent","contractDependencies":[],"contractKind":"contract","fullyImplemented":true,"linearizedBaseContracts":[61],"name":"EmitEvent","nameLocation":"67:9:1","scope":62,"usedErrors":[],"usedEvents":[50]}],"license":"MIT"},"id":1} \ No newline at end of file diff --git a/op-e2e/e2eutils/interop/contracts/generate.sh b/op-e2e/e2eutils/interop/contracts/generate.sh index d6107df9cc..124deb1913 100755 --- a/op-e2e/e2eutils/interop/contracts/generate.sh +++ b/op-e2e/e2eutils/interop/contracts/generate.sh @@ -19,11 +19,3 @@ cd ../.. mkdir -p bindings/inbox abigen --abi ./build/ICrossL2Inbox.sol/ICrossL2Inbox.abi --bin ./build/ICrossL2Inbox.sol/ICrossL2Inbox.bin --pkg inbox --out ./bindings/inbox/inbox.go - -cd build/ISystemConfig.sol -cat ISystemConfig.json | jq -r '.bytecode.object' > ISystemConfig.bin -cat ISystemConfig.json | jq '.abi' > ISystemConfig.abi -cd ../.. - -mkdir -p bindings/systemconfig -abigen --abi ./build/ISystemConfig.sol/ISystemConfig.abi --bin ./build/ISystemConfig.sol/ISystemConfig.bin --pkg systemconfig --out ./bindings/systemconfig/systemconfig.go diff --git a/op-e2e/e2eutils/interop/contracts/src/ICrossL2Inbox.sol b/op-e2e/e2eutils/interop/contracts/src/ICrossL2Inbox.sol index e495c13f61..6fbd2bfedd 100644 --- a/op-e2e/e2eutils/interop/contracts/src/ICrossL2Inbox.sol +++ b/op-e2e/e2eutils/interop/contracts/src/ICrossL2Inbox.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +/// @notice Identifier of a cross chain message. struct Identifier { address origin; uint256 blockNumber; @@ -9,15 +10,16 @@ struct Identifier { uint256 chainId; } -/// @title ICrossL2Inbox -/// @notice Interface for the CrossL2Inbox contract. interface ICrossL2Inbox { + error NoExecutingDeposits(); + error NotInAccessList(); + error BlockNumberTooHigh(); + error TimestampTooHigh(); + error LogIndexTooHigh(); + + event ExecutingMessage(bytes32 indexed msgHash, Identifier id); + + function version() external view returns (string memory); - /// @notice Validates a cross chain message on the destination chain - /// and emits an ExecutingMessage event. This function is useful - /// for applications that understand the schema of the _message payload and want to - /// process it in a custom way. - /// @param _id Identifier of the message. - /// @param _msgHash Hash of the message payload to call target with. function validateMessage(Identifier calldata _id, bytes32 _msgHash) external; } diff --git a/op-e2e/e2eutils/interop/contracts/src/ISystemConfig.sol b/op-e2e/e2eutils/interop/contracts/src/ISystemConfig.sol deleted file mode 100644 index fd14f919b2..0000000000 --- a/op-e2e/e2eutils/interop/contracts/src/ISystemConfig.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -interface ISystemConfig { - /// @notice Adds a chain to the interop dependency set. Can only be called by the dependency manager. - /// @param _chainId Chain ID of chain to add. - function addDependency(uint256 _chainId) external; -} diff --git a/op-e2e/e2eutils/setup.go b/op-e2e/e2eutils/setup.go index 92fc08403c..53a27124f4 100644 --- a/op-e2e/e2eutils/setup.go +++ b/op-e2e/e2eutils/setup.go @@ -156,7 +156,7 @@ func Setup(t require.TestingT, deployParams *DeployParams, alloc *AllocParams) * allocsMode := GetL2AllocsMode(deployConf, l1Block.Time()) l2Allocs := config.L2Allocs(deployParams.AllocType, allocsMode) - l2Genesis, err := genesis.BuildL2Genesis(deployConf, l2Allocs, l1Block.Header()) + l2Genesis, err := genesis.BuildL2Genesis(deployConf, l2Allocs, eth.BlockRefFromHeader(l1Block.Header())) require.NoError(t, err, "failed to create l2 genesis") if alloc.PrefundTestUsers { for _, addr := range deployParams.Addresses.All() { @@ -208,6 +208,7 @@ func Setup(t require.TestingT, deployParams *DeployParams, alloc *AllocParams) * FjordTime: deployConf.FjordTime(uint64(deployConf.L1GenesisBlockTimestamp)), GraniteTime: deployConf.GraniteTime(uint64(deployConf.L1GenesisBlockTimestamp)), HoloceneTime: deployConf.HoloceneTime(uint64(deployConf.L1GenesisBlockTimestamp)), + PectraBlobScheduleTime: deployConf.PectraBlobScheduleTime(uint64(deployConf.L1GenesisBlockTimestamp)), IsthmusTime: deployConf.IsthmusTime(uint64(deployConf.L1GenesisBlockTimestamp)), InteropTime: deployConf.InteropTime(uint64(deployConf.L1GenesisBlockTimestamp)), AltDAConfig: pcfg, diff --git a/op-e2e/faultproofs/cannon_benchmark_test.go b/op-e2e/faultproofs/cannon_benchmark_test.go index 9c05b9e9e1..57b5f1d83e 100644 --- a/op-e2e/faultproofs/cannon_benchmark_test.go +++ b/op-e2e/faultproofs/cannon_benchmark_test.go @@ -94,11 +94,11 @@ func testBenchmarkCannonFPP(t *testing.T, allocType config.AllocType) { l1Head := l1HeadBlock.Hash() inputs := utils.LocalGameInputs{ - L1Head: l1Head, - L2Head: l2Head, - L2Claim: common.Hash(l2Claim), - L2OutputRoot: common.Hash(l2OutputRoot), - L2BlockNumber: l2ClaimBlockNumber, + L1Head: l1Head, + L2Head: l2Head, + L2Claim: common.Hash(l2Claim), + L2OutputRoot: common.Hash(l2OutputRoot), + L2SequenceNumber: l2ClaimBlockNumber, } debugfile := path.Join(t.TempDir(), "debug.json") runCannon(t, ctx, sys, inputs, "--debug-info", debugfile) diff --git a/op-e2e/faultproofs/precompile_test.go b/op-e2e/faultproofs/precompile_test.go index e4f3f24b48..5210776fe8 100644 --- a/op-e2e/faultproofs/precompile_test.go +++ b/op-e2e/faultproofs/precompile_test.go @@ -92,11 +92,11 @@ func testPrecompiles(t *testing.T, allocType e2e_config.AllocType) { l1Head := l1HeadBlock.Hash() inputs := utils.LocalGameInputs{ - L1Head: l1Head, - L2Head: l2Head, - L2Claim: common.Hash(l2Claim), - L2OutputRoot: common.Hash(l2OutputRoot), - L2BlockNumber: l2ClaimBlockNumber, + L1Head: l1Head, + L2Head: l2Head, + L2Claim: common.Hash(l2Claim), + L2OutputRoot: common.Hash(l2OutputRoot), + L2SequenceNumber: l2ClaimBlockNumber, } runCannon(t, ctx, sys, inputs) }) @@ -209,11 +209,11 @@ func testGranitePrecompiles(t *testing.T, allocType e2e_config.AllocType) { l1Head := l1HeadBlock.Hash() inputs := utils.LocalGameInputs{ - L1Head: l1Head, - L2Head: l2Head, - L2Claim: common.Hash(l2Claim), - L2OutputRoot: common.Hash(l2OutputRoot), - L2BlockNumber: l2ClaimBlockNumber, + L1Head: l1Head, + L2Head: l2Head, + L2Claim: common.Hash(l2Claim), + L2OutputRoot: common.Hash(l2OutputRoot), + L2SequenceNumber: l2ClaimBlockNumber, } runCannon(t, ctx, sys, inputs) } diff --git a/op-e2e/interop/interop_test.go b/op-e2e/interop/interop_test.go index 6ae812f160..ccff619e86 100644 --- a/op-e2e/interop/interop_test.go +++ b/op-e2e/interop/interop_test.go @@ -3,6 +3,7 @@ package interop import ( "context" "math/big" + "strings" "sync" "testing" "time" @@ -120,6 +121,9 @@ func TestInterop_SupervisorFinality(t *testing.T) { supervisor := s2.SupervisorClient() require.Eventually(t, func() bool { final, err := supervisor.FinalizedL1(context.Background()) + if err != nil && strings.Contains(err.Error(), "not initialized") { + return false + } require.NoError(t, err) return final.Number > 0 // this test takes about 30 seconds, with a longer Eventually timeout for CI @@ -140,8 +144,17 @@ func TestInterop_EmitLogs(t *testing.T) { ids := s2.L2IDs() chainA := ids[0] chainB := ids[1] - EmitterA := s2.DeployEmitterContract(chainA, "Alice") - EmitterB := s2.DeployEmitterContract(chainB, "Alice") + + // Deploy emitter to chain A + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + EmitterA := s2.DeployEmitterContract(ctx, chainA, "Alice") + + // Deploy emitter to chain B + ctx, cancel = context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + EmitterB := s2.DeployEmitterContract(ctx, chainB, "Alice") + payload1 := "SUPER JACKPOT!" numEmits := 10 // emit logs on both chains in parallel @@ -183,8 +196,8 @@ func TestInterop_EmitLogs(t *testing.T) { supervisor := s2.SupervisorClient() - // helper function to turn a log into an identifier and the expected hash of the payload - logToIdentifier := func(chainID string, log gethTypes.Log) (types.Identifier, common.Hash) { + // helper function to turn a log into an access-list object + logToAccess := func(chainID string, log gethTypes.Log) types.Access { client := s2.L2GethClient(chainID, "sequencer") // construct the expected hash of the log's payload // (topics concatenated with data) @@ -193,7 +206,7 @@ func TestInterop_EmitLogs(t *testing.T) { msgPayload = append(msgPayload, topic.Bytes()...) } msgPayload = append(msgPayload, log.Data...) - expectedHash := common.BytesToHash(crypto.Keccak256(msgPayload)) + msgHash := crypto.Keccak256Hash(msgPayload) // get block for the log (for timestamp) ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) @@ -201,43 +214,36 @@ func TestInterop_EmitLogs(t *testing.T) { block, err := client.BlockByHash(ctx, log.BlockHash) require.NoError(t, err) - // make an identifier out of the sample log - identifier := types.Identifier{ - Origin: log.Address, + args := types.ChecksumArgs{ BlockNumber: log.BlockNumber, - LogIndex: uint32(log.Index), Timestamp: block.Time(), + LogIndex: uint32(log.Index), ChainID: eth.ChainIDFromBig(s2.ChainID(chainID)), + LogHash: types.PayloadHashToLogHash(msgHash, log.Address), } - return identifier, expectedHash + return args.Access() } - // all logs should be cross-safe - for _, log := range logsA { - identifier, expectedHash := logToIdentifier(chainA, log) - safety, err := supervisor.CheckMessage(context.Background(), identifier, expectedHash, types.ExecutingDescriptor{Timestamp: identifier.Timestamp}) - require.NoError(t, err) - // the supervisor could progress the safety level more quickly than we expect, - // which is why we check for a minimum safety level - require.True(t, safety.AtLeastAsSafe(types.CrossSafe), "log: %v should be at least Cross-Safe, but is %s", log, safety.String()) + var accessEntries []types.Access + for _, evLog := range logsA { + accessEntries = append(accessEntries, logToAccess(chainA, evLog)) } - for _, log := range logsB { - identifier, expectedHash := logToIdentifier(chainB, log) - safety, err := supervisor.CheckMessage(context.Background(), identifier, expectedHash, types.ExecutingDescriptor{Timestamp: identifier.Timestamp}) - require.NoError(t, err) - // the supervisor could progress the safety level more quickly than we expect, - // which is why we check for a minimum safety level - require.True(t, safety.AtLeastAsSafe(types.CrossSafe), "log: %v should be at least Cross-Safe, but is %s", log, safety.String()) + for _, evLog := range logsB { + accessEntries = append(accessEntries, logToAccess(chainB, evLog)) } + accessList := types.EncodeAccessList(accessEntries) - // a log should be invalid if the timestamp is incorrect - identifier, expectedHash := logToIdentifier(chainA, logsA[0]) - // make the timestamp incorrect - identifier.Timestamp = 333 - safety, err := supervisor.CheckMessage(context.Background(), identifier, expectedHash, types.ExecutingDescriptor{Timestamp: 333}) - require.NoError(t, err) - require.Equal(t, types.Invalid, safety) + timestamp := uint64(time.Now().Unix()) + ed := types.ExecutingDescriptor{Timestamp: timestamp} + ctx = context.Background() + err = supervisor.CheckAccessList(ctx, accessList, types.CrossSafe, ed) + require.NoError(t, err, "logsA must all be cross-safe") + // a log should be invalid if the timestamp is incorrect + accessEntries[0].Timestamp = 333 + accessList = types.EncodeAccessList(accessEntries) + err = supervisor.CheckAccessList(ctx, accessList, types.CrossSafe, ed) + require.ErrorContains(t, err, "conflict") } config := SuperSystemConfig{ mempoolFiltering: false, @@ -254,25 +260,13 @@ func TestInteropBlockBuilding(t *testing.T) { ids := s2.L2IDs() chainA := ids[0] chainB := ids[1] - // We will initiate on chain A, and execute on chain B - s2.DeployEmitterContract(chainA, "Alice") - - // Add chain A as dependency to chain B, - // such that we can execute a message on B that was initiated on A. - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - depRec := s2.AddDependency(ctx, chainB, s2.ChainID(chainA)) - cancel() - t.Logf("Dependency set in L1 block %d", depRec.BlockNumber) rollupClA := s2.L2RollupClient(chainA, "sequencer") - // Now wait for the dependency to be visible in the L2 (receipt needs to be picked up) - require.Eventually(t, func() bool { - status, err := rollupClA.SyncStatus(context.Background()) - require.NoError(t, err) - return status.CrossUnsafeL2.L1Origin.Number >= depRec.BlockNumber.Uint64() - }, time.Second*30, time.Second, "wait for L1 origin to match dependency L1 block") - t.Log("Dependency information has been processed in L2 block") + // We will initiate on chain A, and execute on chain B + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + s2.DeployEmitterContract(ctx, chainA, "Alice") // emit log on chain A ctx, cancel = context.WithTimeout(context.Background(), 30*time.Second) @@ -314,6 +308,14 @@ func TestInteropBlockBuilding(t *testing.T) { t.Logf("invalid payload hash: %s", invalidPayloadHash) t.Logf("invalid log hash: %s", invalidLogHash) + // hack: geth ingress validates using head timestamp, but should be checking with head+blocktime timestamp, + // Until we fix that, we need an additional block to be built, otherwise we get hit by the aggressive ingress filter. + require.Eventually(t, func() bool { + status, err := rollupClA.SyncStatus(context.Background()) + require.NoError(t, err) + return status.CrossUnsafeL2.Time > identifier.Timestamp + }, time.Second*60, time.Second, "wait for emitted data to become cross-unsafe") + // submit executing txs on B t.Log("Testing invalid message") @@ -370,6 +372,9 @@ func TestMultiNode(t *testing.T) { supervisor := s2.SupervisorClient() require.Eventually(t, func() bool { final, err := supervisor.FinalizedL1(context.Background()) + if err != nil && strings.Contains(err.Error(), "not initialized") { + return false + } require.NoError(t, err) return final.Number > 0 // this test takes about 30 seconds, with a longer Eventually timeout for CI diff --git a/op-e2e/interop/supersystem.go b/op-e2e/interop/supersystem.go index 1da8484bf1..840eb1c2b4 100644 --- a/op-e2e/interop/supersystem.go +++ b/op-e2e/interop/supersystem.go @@ -34,8 +34,6 @@ import ( "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/geth" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/interop/contracts/bindings/emit" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/interop/contracts/bindings/inbox" - "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/interop/contracts/bindings/systemconfig" - "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait" "github.com/ethereum-optimism/optimism/op-e2e/system/helpers" l2os "github.com/ethereum-optimism/optimism/op-proposer/proposer" "github.com/ethereum-optimism/optimism/op-service/client" @@ -94,8 +92,7 @@ type SuperSystem interface { L2OperatorKey(network string, role devkeys.ChainOperatorRole) ecdsa.PrivateKey Address(network string, username string) common.Address Contract(network string, contractName string) interface{} - DeployEmitterContract(network string, username string) common.Address - AddDependency(ctx context.Context, network string, dep *big.Int) *types.Receipt + DeployEmitterContract(ctx context.Context, network string, username string) common.Address ValidateMessage( ctx context.Context, id string, @@ -423,18 +420,12 @@ func (s *interopE2ESystem) prepareL2s() map[string]l2Net { // prepareContracts prepares contract-bindings for the L2s func (s *interopE2ESystem) prepareContracts() { // Add bindings to common contracts for each L2 - l1GethClient := s.L1GethClient() - for id, l2Dep := range s.worldDeployment.L2s { + for id := range s.worldDeployment.L2s { { contract, err := inbox.NewInbox(predeploys.CrossL2InboxAddr, s.L2GethClient(id, "sequencer")) require.NoError(s.t, err) s.l2s[id].contracts["inbox"] = contract } - { - contract, err := systemconfig.NewSystemconfig(l2Dep.SystemConfigProxy, l1GethClient) - require.NoError(s.t, err) - s.l2s[id].contracts["systemconfig"] = contract - } } } @@ -514,13 +505,14 @@ func (s *interopE2ESystem) ValidateMessage( ) (*types.Receipt, error) { secret := s.UserKey(id, sender) auth, err := bind.NewKeyedTransactorWithChainID(&secret, s.l2s[id].chainID) + contract := s.Contract(id, "inbox").(*inbox.Inbox) require.NoError(s.t, err) auth.GasLimit = uint64(3000_000) - auth.GasPrice = big.NewInt(20_000_000_000) + auth.GasFeeCap = big.NewInt(21_000_000_000) + auth.GasTipCap = big.NewInt(1_000_000_000) - contract := s.Contract(id, "inbox").(*inbox.Inbox) identifier := inbox.Identifier{ Origin: msgIdentifier.Origin, BlockNumber: new(big.Int).SetUint64(msgIdentifier.BlockNumber), @@ -528,6 +520,14 @@ func (s *interopE2ESystem) ValidateMessage( Timestamp: new(big.Int).SetUint64(msgIdentifier.Timestamp), ChainId: msgIdentifier.ChainID.ToBig(), } + access := msgIdentifier.ChecksumArgs(msgHash).Access() + auth.AccessList = []types.AccessTuple{ + { + Address: predeploys.CrossL2InboxAddr, + StorageKeys: supervisortypes.EncodeAccessList([]supervisortypes.Access{access}), + }, + } + tx, err := contract.InboxTransactor.ValidateMessage(auth, identifier, msgHash) if expectedError != nil { require.ErrorContains(s.t, err, expectedError.Error()) @@ -539,37 +539,10 @@ func (s *interopE2ESystem) ValidateMessage( return bind.WaitMined(ctx, s.L2GethClient(id, "sequencer"), tx) // use the sequencer client to wait for the tx } -func (s *interopE2ESystem) AddDependency(ctx context.Context, id string, dep *big.Int) *types.Receipt { - // There is a note in OPContractsManagerInterop that the proxy-admin is used for now, - // even though it should be a separate dependency-set-manager address. - secret, err := s.hdWallet.Secret(devkeys.ChainOperatorKey{ - ChainID: s.l2s[id].chainID, - Role: devkeys.SystemConfigOwner, - }) - require.NoError(s.t, err) - - auth, err := bind.NewKeyedTransactorWithChainID(secret, s.worldOutput.L1.Genesis.Config.ChainID) - require.NoError(s.t, err) - - balance, err := s.l1GethClient.BalanceAt(ctx, crypto.PubkeyToAddress(secret.PublicKey), nil) - require.NoError(s.t, err) - require.False(s.t, balance.Sign() == 0, "system config owner needs a balance") - - auth.GasLimit = uint64(3000000) - auth.GasPrice = big.NewInt(20000000000) - - contract := s.Contract(id, "systemconfig").(*systemconfig.Systemconfig) - tx, err := contract.SystemconfigTransactor.AddDependency(auth, dep) - require.NoError(s.t, err) - - receipt, err := wait.ForReceiptOK(ctx, s.L1GethClient(), tx.Hash()) - require.NoError(s.t, err) - return receipt -} - // DeployEmitterContract deploys the Emitter contract on the L2 // it uses the sequencer node to deploy the contract func (s *interopE2ESystem) DeployEmitterContract( + ctx context.Context, id string, sender string, ) common.Address { @@ -578,7 +551,9 @@ func (s *interopE2ESystem) DeployEmitterContract( require.NoError(s.t, err) auth.GasLimit = uint64(3000000) auth.GasPrice = big.NewInt(20000000000) - address, _, _, err := emit.DeployEmit(auth, s.L2GethClient(id, "sequencer")) + address, tx, _, err := emit.DeployEmit(auth, s.L2GethClient(id, "sequencer")) + require.NoError(s.t, err) + _, err = bind.WaitMined(ctx, s.L2GethClient(id, "sequencer"), tx) require.NoError(s.t, err) contract, err := emit.NewEmit(address, s.L2GethClient(id, "sequencer")) require.NoError(s.t, err) diff --git a/op-e2e/interop/supersystem_l2.go b/op-e2e/interop/supersystem_l2.go index 3a5048beb6..db7a01c95a 100644 --- a/op-e2e/interop/supersystem_l2.go +++ b/op-e2e/interop/supersystem_l2.go @@ -25,6 +25,7 @@ import ( "github.com/ethereum-optimism/optimism/op-service/dial" "github.com/ethereum-optimism/optimism/op-service/endpoint" oplog "github.com/ethereum-optimism/optimism/op-service/log" + opsigner "github.com/ethereum-optimism/optimism/op-service/signer" "github.com/ethereum-optimism/optimism/op-service/sources" "github.com/ethereum-optimism/optimism/op-service/testlog" "github.com/ethereum/go-ethereum/eth/ethconfig" @@ -166,7 +167,7 @@ func (s *interopE2ESystem) newNodeForL2( }, Rollup: *l2Out.RollupCfg, P2PSigner: &p2p.PreparedSigner{ - Signer: p2p.NewLocalSigner(&p2pKey)}, + Signer: opsigner.NewLocalSigner(&p2pKey)}, RPC: node.RPCConfig{ ListenAddr: "127.0.0.1", ListenPort: 0, diff --git a/op-e2e/opgeth/op_geth.go b/op-e2e/opgeth/op_geth.go index e392037219..6555848f7d 100644 --- a/op-e2e/opgeth/op_geth.go +++ b/op-e2e/opgeth/op_geth.go @@ -61,7 +61,7 @@ func NewOpGeth(t testing.TB, ctx context.Context, cfg *e2esys.SystemConfig) (*Op l1Block := l1Genesis.ToBlock() allocsMode := e2eutils.GetL2AllocsMode(cfg.DeployConfig, l1Block.Time()) l2Allocs := config.L2Allocs(config.AllocTypeStandard, allocsMode) - l2Genesis, err := genesis.BuildL2Genesis(cfg.DeployConfig, l2Allocs, l1Block.Header()) + l2Genesis, err := genesis.BuildL2Genesis(cfg.DeployConfig, l2Allocs, eth.BlockRefFromHeader(l1Block.Header())) require.NoError(t, err) l2GenesisBlock := l2Genesis.ToBlock() @@ -89,7 +89,7 @@ func NewOpGeth(t testing.TB, ctx context.Context, cfg *e2esys.SystemConfig) (*Op require.NoError(t, err) // Finally create the engine client - rollupCfg, err := cfg.DeployConfig.RollupConfig(l1Block.Header(), l2GenesisBlock.Hash(), l2GenesisBlock.NumberU64()) + rollupCfg, err := cfg.DeployConfig.RollupConfig(eth.BlockRefFromHeader(l1Block.Header()), l2GenesisBlock.Hash(), l2GenesisBlock.NumberU64()) require.NoError(t, err) rollupCfg.Genesis = rollupGenesis l2Engine, err := sources.NewEngineClient( diff --git a/op-e2e/system/bridge/validity_test.go b/op-e2e/system/bridge/validity_test.go index 4c4ceff078..efd5900889 100644 --- a/op-e2e/system/bridge/validity_test.go +++ b/op-e2e/system/bridge/validity_test.go @@ -266,10 +266,6 @@ func TestMixedDepositValidity(t *testing.T) { } } -func TestMixedWithdrawalValidity_L2OO(t *testing.T) { - testMixedWithdrawalValidity(t, config.AllocTypeL2OO) -} - func TestMixedWithdrawalValidity_Standard(t *testing.T) { testMixedWithdrawalValidity(t, config.AllocTypeStandard) } @@ -449,6 +445,12 @@ func testMixedWithdrawalValidity(t *testing.T, allocType config.AllocType) { receiptCl := ethclient.NewClient(rpcClient) blockCl := ethclient.NewClient(rpcClient) + // Mine an empty block so that the timestamp is updated. Otherwise, + // proveWithdrawalTransaction gas estimation may fail because the current timestamp is + // the same as the dispute game creation timestamp. + err = wait.ForNextBlock(ctx, l1Client) + require.NoError(t, err) + // Now create the withdrawal params, err := helpers.ProveWithdrawalParameters(context.Background(), proofCl, receiptCl, blockCl, tx.Hash(), header, l2OutputOracle, disputeGameFactory, optimismPortal2, cfg.AllocType) require.Nil(t, err) @@ -575,7 +577,20 @@ func testMixedWithdrawalValidity(t *testing.T, allocType config.AllocType) { require.NoError(t, err) } + // Do a large deposit into the OptimismPortal so there's a balance to withdraw + depositAmount := big.NewInt(1_000_000_000_000) + transactor.Account.L1Opts.Value = depositAmount + tx, err := depositContract.DepositTransaction(transactor.Account.L1Opts, fromAddr, depositAmount, 120_000, false, nil) + require.NoError(t, err) + receipt, err := wait.ForReceiptOK(ctx, l1Client, tx.Hash()) + require.NoError(t, err) + require.Equal(t, receipt.Status, types.ReceiptStatusSuccessful) + + // Increase the expected nonce + transactor.ExpectedL1Nonce++ + // Finalize withdrawal + transactor.Account.L1Opts.Value = big.NewInt(0) _, err = depositContract.FinalizeWithdrawalTransaction( transactor.Account.L1Opts, withdrawalTransaction, diff --git a/op-e2e/system/bridge/withdrawal_test.go b/op-e2e/system/bridge/withdrawal_test.go index 1f56fe4c4a..12b5fe12e2 100644 --- a/op-e2e/system/bridge/withdrawal_test.go +++ b/op-e2e/system/bridge/withdrawal_test.go @@ -9,10 +9,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestWithdrawals_L2OO(t *testing.T) { - testWithdrawals(t, config.AllocTypeL2OO) -} - func TestWithdrawals_Standard(t *testing.T) { testWithdrawals(t, config.AllocTypeStandard) } diff --git a/op-e2e/system/e2esys/setup.go b/op-e2e/system/e2esys/setup.go index 8c63057660..7c50738963 100644 --- a/op-e2e/system/e2esys/setup.go +++ b/op-e2e/system/e2esys/setup.go @@ -71,6 +71,7 @@ import ( "github.com/ethereum-optimism/optimism/op-service/eth" oplog "github.com/ethereum-optimism/optimism/op-service/log" "github.com/ethereum-optimism/optimism/op-service/predeploys" + opsigner "github.com/ethereum-optimism/optimism/op-service/signer" "github.com/ethereum-optimism/optimism/op-service/sources" "github.com/ethereum-optimism/optimism/op-service/testlog" ) @@ -610,7 +611,7 @@ func (cfg SystemConfig) Start(t *testing.T, startOpts ...StartOption) (*System, t.Log("Generating L2 genesis", "l2_allocs_mode", string(allocsMode)) l2Allocs := config.L2Allocs(cfg.AllocType, allocsMode) - l2Genesis, err := genesis.BuildL2Genesis(cfg.DeployConfig, l2Allocs, l1Block.Header()) + l2Genesis, err := genesis.BuildL2Genesis(cfg.DeployConfig, l2Allocs, eth.BlockRefFromHeader(l1Block.Header())) if err != nil { return nil, err } @@ -671,6 +672,7 @@ func (cfg SystemConfig) Start(t *testing.T, startOpts ...StartOption) (*System, FjordTime: cfg.DeployConfig.FjordTime(uint64(cfg.DeployConfig.L1GenesisBlockTimestamp)), GraniteTime: cfg.DeployConfig.GraniteTime(uint64(cfg.DeployConfig.L1GenesisBlockTimestamp)), HoloceneTime: cfg.DeployConfig.HoloceneTime(uint64(cfg.DeployConfig.L1GenesisBlockTimestamp)), + PectraBlobScheduleTime: cfg.DeployConfig.PectraBlobScheduleTime(uint64(cfg.DeployConfig.L1GenesisBlockTimestamp)), IsthmusTime: cfg.DeployConfig.IsthmusTime(uint64(cfg.DeployConfig.L1GenesisBlockTimestamp)), InteropTime: cfg.DeployConfig.InteropTime(uint64(cfg.DeployConfig.L1GenesisBlockTimestamp)), ProtocolVersionsAddress: cfg.L1Deployments.ProtocolVersionsProxy, @@ -833,7 +835,7 @@ func (cfg SystemConfig) Start(t *testing.T, startOpts ...StartOption) (*System, c.P2P = p if c.Driver.SequencerEnabled && c.P2PSigner == nil { - c.P2PSigner = &p2p.PreparedSigner{Signer: p2p.NewLocalSigner(cfg.Secrets.SequencerP2P)} + c.P2PSigner = &p2p.PreparedSigner{Signer: opsigner.NewLocalSigner(cfg.Secrets.SequencerP2P)} } } diff --git a/op-e2e/system/helpers/tx_helper.go b/op-e2e/system/helpers/tx_helper.go index 10c16c0e74..fefddf8f98 100644 --- a/op-e2e/system/helpers/tx_helper.go +++ b/op-e2e/system/helpers/tx_helper.go @@ -53,7 +53,9 @@ func SendDepositTx(t *testing.T, cfg e2esys.SystemConfig, l1Client *ethclient.Cl t.Logf("SendDepositTx: included on L1") // Wait for transaction to be included on L2 - reconstructedDep, err := derive.UnmarshalDepositLogEvent(l1Receipt.Logs[0]) + // The last log is the deposit log + idx := len(l1Receipt.Logs) - 1 + reconstructedDep, err := derive.UnmarshalDepositLogEvent(l1Receipt.Logs[idx]) require.NoError(t, err, "Could not reconstruct L2 Deposit") tx = types.NewTx(reconstructedDep) l2Receipt, err := wait.ForReceipt(ctx, l2Client, tx.Hash(), l2Opts.ExpectedStatus) diff --git a/op-e2e/system/helpers/withdrawal_helper.go b/op-e2e/system/helpers/withdrawal_helper.go index a2a57c9e3b..787495e4aa 100644 --- a/op-e2e/system/helpers/withdrawal_helper.go +++ b/op-e2e/system/helpers/withdrawal_helper.go @@ -127,12 +127,19 @@ func ProveWithdrawal(t *testing.T, cfg e2esys.SystemConfig, clients ClientProvid require.NoError(t, err) } + // Wait for another block to be mined so that the timestamp increases. Otherwise, + // proveWithdrawalTransaction gas estimation may fail because the current timestamp is the same + // as the dispute game creation timestamp. + err = wait.ForNextBlock(ctx, l1Client) + require.NoError(t, err) + receiptCl := clients.NodeClient(l2NodeName) headerCl := clients.NodeClient(l2NodeName) proofCl := gethclient.New(receiptCl.Client()) ctx, cancel = context.WithTimeout(context.Background(), 30*time.Second) defer cancel() + // Get the latest header header, err := receiptCl.HeaderByNumber(ctx, new(big.Int).SetUint64(blockNumber)) require.NoError(t, err) diff --git a/op-e2e/system/proofs/proposer_fp_test.go b/op-e2e/system/proofs/proposer_fp_test.go index 971ed06a46..a4cccf138f 100644 --- a/op-e2e/system/proofs/proposer_fp_test.go +++ b/op-e2e/system/proofs/proposer_fp_test.go @@ -61,7 +61,7 @@ func TestL2OutputSubmitterFaultProofs(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - _, gameBlockNumber, err := proxy.GetBlockRange(ctx) + _, gameBlockNumber, err := proxy.GetGameRange(ctx) require.Nil(t, err) l2Output, err := rollupClient.OutputAtBlock(ctx, gameBlockNumber) require.Nil(t, err) diff --git a/op-e2e/system/proofs/proposer_l2oo_test.go b/op-e2e/system/proofs/proposer_l2oo_test.go deleted file mode 100644 index bf718c6165..0000000000 --- a/op-e2e/system/proofs/proposer_l2oo_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package proofs - -import ( - "context" - "math/big" - "testing" - "time" - - op_e2e "github.com/ethereum-optimism/optimism/op-e2e" - "github.com/ethereum-optimism/optimism/op-e2e/config" - - "github.com/ethereum-optimism/optimism/op-e2e/bindings" - "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/geth" - "github.com/ethereum-optimism/optimism/op-e2e/system/e2esys" - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/stretchr/testify/require" -) - -func TestL2OutputSubmitter(t *testing.T) { - op_e2e.InitParallel(t) - - cfg := e2esys.DefaultSystemConfig(t, e2esys.WithAllocType(config.AllocTypeL2OO)) - cfg.NonFinalizedProposals = true // speed up the time till we see output proposals - - sys, err := cfg.Start(t) - require.Nil(t, err, "Error starting up system") - - l1Client := sys.NodeClient("l1") - - rollupClient := sys.RollupClient("sequencer") - - // OutputOracle is already deployed - l2OutputOracle, err := bindings.NewL2OutputOracleCaller(cfg.L1Deployments.L2OutputOracleProxy, l1Client) - require.Nil(t, err) - - initialOutputBlockNumber, err := l2OutputOracle.LatestBlockNumber(&bind.CallOpts{}) - require.Nil(t, err) - - // Wait until the second output submission from L2. The output submitter submits outputs from the - // unsafe portion of the chain which gets reorged on startup. The sequencer has an out of date view - // when it creates it's first block and uses and old L1 Origin. It then does not submit a batch - // for that block and subsequently reorgs to match what the verifier derives when running the - // reconcillation process. - l2Verif := sys.NodeClient("verifier") - _, err = geth.WaitForBlock(big.NewInt(6), l2Verif) - require.Nil(t, err) - - // Wait for batch submitter to update L2 output oracle. - timeoutCh := time.After(15 * time.Second) - ticker := time.NewTicker(1 * time.Second) - defer ticker.Stop() - for { - l2ooBlockNumber, err := l2OutputOracle.LatestBlockNumber(&bind.CallOpts{}) - require.Nil(t, err) - - // Wait for the L2 output oracle to have been changed from the initial - // timestamp set in the contract constructor. - if l2ooBlockNumber.Cmp(initialOutputBlockNumber) > 0 { - // Retrieve the l2 output committed at this updated timestamp. - committedL2Output, err := l2OutputOracle.GetL2OutputAfter(&bind.CallOpts{}, l2ooBlockNumber) - require.NotEqual(t, [32]byte{}, committedL2Output.OutputRoot, "Empty L2 Output") - require.Nil(t, err) - - // Fetch the corresponding L2 block and assert the committed L2 - // output matches the block's state root. - // - // NOTE: This assertion will change once the L2 output format is - // finalized. - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - l2Output, err := rollupClient.OutputAtBlock(ctx, l2ooBlockNumber.Uint64()) - require.Nil(t, err) - require.Equal(t, l2Output.OutputRoot[:], committedL2Output.OutputRoot[:]) - break - } - - select { - case <-timeoutCh: - t.Fatalf("State root oracle not updated") - case <-ticker.C: - } - } -} diff --git a/op-node/chaincfg/chains_test.go b/op-node/chaincfg/chains_test.go index 8f741b7427..fa297ea23c 100644 --- a/op-node/chaincfg/chains_test.go +++ b/op-node/chaincfg/chains_test.go @@ -114,6 +114,7 @@ var sepoliaCfg = rollup.Config{ FjordTime: u64Ptr(1716998400), GraniteTime: u64Ptr(1723478400), HoloceneTime: u64Ptr(1732633200), + PectraBlobScheduleTime: u64Ptr(1742486400), ProtocolVersionsAddress: common.HexToAddress("0x79ADD5713B383DAa0a138d3C4780C7A1804a8090"), ChainOpConfig: defaultOpConfig, } @@ -152,6 +153,7 @@ var sepoliaDev0Cfg = rollup.Config{ FjordTime: u64Ptr(1715961600), GraniteTime: u64Ptr(1723046400), HoloceneTime: u64Ptr(1731682800), + PectraBlobScheduleTime: u64Ptr(1741687200), ProtocolVersionsAddress: common.HexToAddress("0x252CbE9517F731C618961D890D534183822dcC8d"), ChainOpConfig: defaultOpConfig, } diff --git a/op-node/cmd/genesis/cmd.go b/op-node/cmd/genesis/cmd.go index 054f293106..5c18f6988f 100644 --- a/op-node/cmd/genesis/cmd.go +++ b/op-node/cmd/genesis/cmd.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/ioutil" "github.com/ethereum-optimism/optimism/op-service/retry" "github.com/ethereum-optimism/optimism/op-service/sources/batching" @@ -191,13 +192,13 @@ var Subcommands = cli.Commands{ } // Build the L2 genesis block - l2Genesis, err := genesis.BuildL2Genesis(config, l2Allocs, l1StartBlock.Header()) + l2Genesis, err := genesis.BuildL2Genesis(config, l2Allocs, eth.BlockRefFromHeader(l1StartBlock.Header())) if err != nil { return fmt.Errorf("error creating l2 genesis: %w", err) } l2GenesisBlock := l2Genesis.ToBlock() - rollupConfig, err := config.RollupConfig(l1StartBlock.Header(), l2GenesisBlock.Hash(), l2GenesisBlock.Number().Uint64()) + rollupConfig, err := config.RollupConfig(eth.BlockRefFromHeader(l1StartBlock.Header()), l2GenesisBlock.Hash(), l2GenesisBlock.Number().Uint64()) if err != nil { return err } diff --git a/op-node/node/node.go b/op-node/node/node.go index 1a1c8dea37..2093af0c76 100644 --- a/op-node/node/node.go +++ b/op-node/node/node.go @@ -628,12 +628,11 @@ func (n *OpNode) PublishL2Payload(ctx context.Context, envelope *eth.ExecutionPa // publish to p2p, if we are running p2p at all if p2pNode := n.getP2PNodeIfEnabled(); p2pNode != nil { - payload := envelope.ExecutionPayload if n.p2pSigner == nil { - return fmt.Errorf("node has no p2p signer, payload %s cannot be published", payload.ID()) + return fmt.Errorf("node has no p2p signer, payload %s cannot be published", envelope.ID()) } - n.log.Info("Publishing signed execution payload on p2p", "id", payload.ID()) - return p2pNode.GossipOut().PublishL2Payload(ctx, envelope, n.p2pSigner) + n.log.Info("Publishing signed execution payload on p2p", "id", envelope.ID()) + return p2pNode.GossipOut().SignAndPublishL2Payload(ctx, envelope, n.p2pSigner) } // if p2p is not enabled then we just don't publish the payload return nil diff --git a/op-node/p2p/cli/load_signer.go b/op-node/p2p/cli/load_signer.go index 3c0c532edb..56cbec40a1 100644 --- a/op-node/p2p/cli/load_signer.go +++ b/op-node/p2p/cli/load_signer.go @@ -25,9 +25,9 @@ func LoadSignerSetup(ctx *cli.Context, logger log.Logger) (p2p.SignerSetup, erro return nil, fmt.Errorf("failed to read batch submitter key: %w", err) } - return &p2p.PreparedSigner{Signer: p2p.NewLocalSigner(priv)}, nil + return &p2p.PreparedSigner{Signer: opsigner.NewLocalSigner(priv)}, nil } else if signerCfg.Enabled() { - remoteSigner, err := p2p.NewRemoteSigner(logger, signerCfg) + remoteSigner, err := opsigner.NewRemoteSigner(logger, signerCfg) if err != nil { return nil, err } diff --git a/op-node/p2p/gossip.go b/op-node/p2p/gossip.go index 3da4a35729..07a32bdb7c 100644 --- a/op-node/p2p/gossip.go +++ b/op-node/p2p/gossip.go @@ -18,7 +18,6 @@ import ( "github.com/libp2p/go-libp2p/core/peer" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" "github.com/ethereum-optimism/optimism/op-node/rollup" @@ -298,10 +297,11 @@ func BuildBlocksValidator(log log.Logger, cfg *rollup.Config, runCfg GossipRunti } // message starts with compact-encoding secp256k1 encoded signature - signatureBytes, payloadBytes := data[:65], data[65:] + signature := eth.Bytes65(data[:65]) + payloadBytes := data[65:] // [REJECT] if the signature by the sequencer is not valid - result := verifyBlockSignature(log, cfg, runCfg, id, signatureBytes, payloadBytes) + result := verifyBlockSignature(log, cfg, runCfg, id, signature, payloadBytes) if result != pubsub.ValidationAccept { return result } @@ -427,30 +427,21 @@ func BuildBlocksValidator(log log.Logger, cfg *rollup.Config, runCfg GossipRunti } } -func verifyBlockSignature(log log.Logger, cfg *rollup.Config, runCfg GossipRuntimeConfig, id peer.ID, signatureBytes []byte, payloadBytes []byte) pubsub.ValidationResult { - signingHash, err := BlockSigningHash(cfg, payloadBytes) - if err != nil { - log.Warn("failed to compute block signing hash", "err", err, "peer", id) - return pubsub.ValidationReject - } - - pub, err := crypto.SigToPub(signingHash[:], signatureBytes) - if err != nil { - log.Warn("invalid block signature", "err", err, "peer", id) - return pubsub.ValidationReject +func verifyBlockSignature(log log.Logger, cfg *rollup.Config, runCfg GossipRuntimeConfig, id peer.ID, signature eth.Bytes65, payloadBytes []byte) pubsub.ValidationResult { + authCtx := &opsigner.OPStackP2PBlockAuthV1{ + Allowed: runCfg.P2PSequencerAddress(), + Chain: eth.ChainIDFromBig(cfg.L2ChainID), } - addr := crypto.PubkeyToAddress(*pub) - - // In the future we may load & validate block metadata before checking the signature. - // And then check the signer based on the metadata, to support e.g. multiple p2p signers at the same time. - // For now we only have one signer at a time and thus check the address directly. - // This means we may drop old payloads upon key rotation, - // but this can be recovered from like any other missed unsafe payload. - if expected := runCfg.P2PSequencerAddress(); expected == (common.Address{}) { - log.Warn("no configured p2p sequencer address, ignoring gossiped block", "peer", id, "addr", addr) + if authCtx.Allowed == (common.Address{}) { + log.Warn("no configured p2p sequencer address, ignoring gossiped block", "peer", id, "addr", authCtx.Allowed) return pubsub.ValidationIgnore - } else if addr != expected { - log.Warn("unexpected block author", "err", err, "peer", id, "addr", addr, "expected", expected) + } + block := opsigner.SignedP2PBlock{ + Raw: payloadBytes, + Signature: signature, + } + if err := block.VerifySignature(authCtx); err != nil { + log.Warn("invalid block signature", "err", err, "peer", id) return pubsub.ValidationReject } return pubsub.ValidationAccept @@ -470,7 +461,8 @@ type GossipTopicInfo interface { type GossipOut interface { GossipTopicInfo - PublishL2Payload(ctx context.Context, msg *eth.ExecutionPayloadEnvelope, signer Signer) error + SignAndPublishL2Payload(ctx context.Context, msg *eth.ExecutionPayloadEnvelope, signer Signer) error + PublishSignedL2Payload(ctx context.Context, signedEnvelope *opsigner.SignedExecutionPayloadEnvelope) error Close() error } @@ -548,7 +540,32 @@ func (p *publisher) BlocksTopicV4Peers() []peer.ID { return p.blocksV4.topic.ListPeers() } -func (p *publisher) PublishL2Payload(ctx context.Context, envelope *eth.ExecutionPayloadEnvelope, signer Signer) error { +func (p *publisher) PublishSignedL2Payload(ctx context.Context, signedEnvelope *opsigner.SignedExecutionPayloadEnvelope) error { + res := msgBufPool.Get().(*[]byte) + buf := bytes.NewBuffer((*res)[:0]) + defer func() { + *res = buf.Bytes() + defer msgBufPool.Put(res) + }() + + buf.Write(signedEnvelope.Signature[:]) + + if signedEnvelope.Envelope.ParentBeaconBlockRoot != nil { + if _, err := signedEnvelope.Envelope.MarshalSSZ(buf); err != nil { + return fmt.Errorf("failed to encoded execution payload envelope to publish: %w", err) + } + } else { + if _, err := signedEnvelope.Envelope.ExecutionPayload.MarshalSSZ(buf); err != nil { + return fmt.Errorf("failed to encoded execution payload to publish: %w", err) + } + } + + data := buf.Bytes() + timestamp := uint64(signedEnvelope.Envelope.ExecutionPayload.Timestamp) + return p.publishRawSignedPayload(ctx, timestamp, data) +} + +func (p *publisher) SignAndPublishL2Payload(ctx context.Context, envelope *eth.ExecutionPayloadEnvelope, signer Signer) error { res := msgBufPool.Get().(*[]byte) buf := bytes.NewBuffer((*res)[:0]) defer func() { @@ -567,24 +584,28 @@ func (p *publisher) PublishL2Payload(ctx context.Context, envelope *eth.Executio return fmt.Errorf("failed to encoded execution payload to publish: %w", err) } } - data := buf.Bytes() payloadData := data[65:] - sig, err := signer.Sign(ctx, SigningDomainBlocksV1, eth.ChainIDFromBig(p.cfg.L2ChainID), opsigner.PayloadHash(payloadData)) + payloadHash := opsigner.PayloadHash(payloadData) + chainID := eth.ChainIDFromBig(p.cfg.L2ChainID) + sig, err := signer.SignBlockV1(ctx, chainID, payloadHash) if err != nil { return fmt.Errorf("failed to sign execution payload with signer: %w", err) } copy(data[:65], sig[:]) + return p.publishRawSignedPayload(ctx, uint64(envelope.ExecutionPayload.Timestamp), data) +} +func (p *publisher) publishRawSignedPayload(ctx context.Context, timestamp uint64, data []byte) error { // compress the full message // This also copies the data, freeing up the original buffer to go back into the pool out := snappy.Encode(nil, data) - if p.cfg.IsIsthmus(uint64(envelope.ExecutionPayload.Timestamp)) { + if p.cfg.IsIsthmus(timestamp) { return p.blocksV4.topic.Publish(ctx, out) - } else if p.cfg.IsEcotone(uint64(envelope.ExecutionPayload.Timestamp)) { + } else if p.cfg.IsEcotone(timestamp) { return p.blocksV3.topic.Publish(ctx, out) - } else if p.cfg.IsCanyon(uint64(envelope.ExecutionPayload.Timestamp)) { + } else if p.cfg.IsCanyon(timestamp) { return p.blocksV2.topic.Publish(ctx, out) } else { return p.blocksV1.topic.Publish(ctx, out) diff --git a/op-node/p2p/gossip_test.go b/op-node/p2p/gossip_test.go index 4bffa826c9..de294a348b 100644 --- a/op-node/p2p/gossip_test.go +++ b/op-node/p2p/gossip_test.go @@ -3,7 +3,6 @@ package p2p import ( "bytes" "context" - "crypto/ecdsa" "fmt" "io" "math/big" @@ -14,7 +13,6 @@ import ( "github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-service/eth" - oprpc "github.com/ethereum-optimism/optimism/op-service/rpc" opsigner "github.com/ethereum-optimism/optimism/op-service/signer" "github.com/ethereum-optimism/optimism/op-service/testlog" "github.com/ethereum-optimism/optimism/op-service/testutils" @@ -24,7 +22,6 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/rpc" pubsub "github.com/libp2p/go-libp2p-pubsub" pubsub_pb "github.com/libp2p/go-libp2p-pubsub/pb" @@ -70,152 +67,37 @@ func TestVerifyBlockSignature(t *testing.T) { t.Run("Valid", func(t *testing.T) { runCfg := &testutils.MockRuntimeConfig{P2PSeqAddress: crypto.PubkeyToAddress(secrets.PublicKey)} - signer := &PreparedSigner{Signer: NewLocalSigner(secrets)} - sig, err := signer.Sign(context.Background(), SigningDomainBlocksV1, eth.ChainIDFromBig(cfg.L2ChainID), opsigner.PayloadHash(msg)) + signer := &PreparedSigner{Signer: opsigner.NewLocalSigner(secrets)} + sig, err := signer.SignBlockV1(context.Background(), eth.ChainIDFromBig(cfg.L2ChainID), opsigner.PayloadHash(msg)) require.NoError(t, err) - result := verifyBlockSignature(logger, cfg, runCfg, peerId, sig[:], msg) - require.Equal(t, pubsub.ValidationAccept, result) - }) - - t.Run("WrongSigner", func(t *testing.T) { - runCfg := &testutils.MockRuntimeConfig{P2PSeqAddress: common.HexToAddress("0x1234")} - signer := &PreparedSigner{Signer: NewLocalSigner(secrets)} - sig, err := signer.Sign(context.Background(), SigningDomainBlocksV1, eth.ChainIDFromBig(cfg.L2ChainID), opsigner.PayloadHash(msg)) - require.NoError(t, err) - result := verifyBlockSignature(logger, cfg, runCfg, peerId, sig[:], msg) - require.Equal(t, pubsub.ValidationReject, result) - }) - - t.Run("InvalidSignature", func(t *testing.T) { - runCfg := &testutils.MockRuntimeConfig{P2PSeqAddress: crypto.PubkeyToAddress(secrets.PublicKey)} - sig := make([]byte, 65) result := verifyBlockSignature(logger, cfg, runCfg, peerId, sig, msg) - require.Equal(t, pubsub.ValidationReject, result) - }) - - t.Run("NoSequencer", func(t *testing.T) { - runCfg := &testutils.MockRuntimeConfig{} - signer := &PreparedSigner{Signer: NewLocalSigner(secrets)} - sig, err := signer.Sign(context.Background(), SigningDomainBlocksV1, eth.ChainIDFromBig(cfg.L2ChainID), opsigner.PayloadHash(msg)) - require.NoError(t, err) - result := verifyBlockSignature(logger, cfg, runCfg, peerId, sig[:], msg) - require.Equal(t, pubsub.ValidationIgnore, result) - }) -} - -type mockRemoteSigner struct { - priv *ecdsa.PrivateKey -} - -func (t *mockRemoteSigner) SignBlockPayload(args opsigner.BlockPayloadArgs) (hexutil.Bytes, error) { - msg, err := args.Message() - if err != nil { - return nil, err - } - signingHash := msg.ToSigningHash() - signature, err := crypto.Sign(signingHash[:], t.priv) - if err != nil { - return nil, err - } - return signature, nil -} - -func TestVerifyBlockSignatureWithRemoteSigner(t *testing.T) { - secrets, err := crypto.GenerateKey() - require.NoError(t, err) - - remoteSigner := &mockRemoteSigner{secrets} - server := oprpc.NewServer( - "127.0.0.1", - 0, - "test", - ) - server.AddAPI(rpc.API{ - Namespace: "opsigner", - Service: remoteSigner, - }) - - require.NoError(t, server.Start()) - defer func() { - _ = server.Stop() - }() - - logger := testlog.Logger(t, log.LevelCrit) - cfg := &rollup.Config{ - L2ChainID: big.NewInt(100), - } - - peerId := peer.ID("foo") - msg := []byte("any msg") - - signerCfg := opsigner.NewCLIConfig() - signerCfg.Endpoint = fmt.Sprintf("http://%s", server.Endpoint()) - signerCfg.TLSConfig.TLSKey = "" - signerCfg.TLSConfig.TLSCert = "" - signerCfg.TLSConfig.TLSCaCert = "" - signerCfg.TLSConfig.Enabled = false - - t.Run("Valid", func(t *testing.T) { - runCfg := &testutils.MockRuntimeConfig{P2PSeqAddress: crypto.PubkeyToAddress(secrets.PublicKey)} - remoteSigner, err := NewRemoteSigner(logger, signerCfg) - require.NoError(t, err) - signer := &PreparedSigner{Signer: remoteSigner} - sig, err := signer.Sign(context.Background(), SigningDomainBlocksV1, eth.ChainIDFromBig(cfg.L2ChainID), opsigner.PayloadHash(msg)) - require.NoError(t, err) - result := verifyBlockSignature(logger, cfg, runCfg, peerId, sig[:], msg) require.Equal(t, pubsub.ValidationAccept, result) }) t.Run("WrongSigner", func(t *testing.T) { runCfg := &testutils.MockRuntimeConfig{P2PSeqAddress: common.HexToAddress("0x1234")} - remoteSigner, err := NewRemoteSigner(logger, signerCfg) - require.NoError(t, err) - signer := &PreparedSigner{Signer: remoteSigner} - sig, err := signer.Sign(context.Background(), SigningDomainBlocksV1, eth.ChainIDFromBig(cfg.L2ChainID), opsigner.PayloadHash(msg)) + signer := &PreparedSigner{Signer: opsigner.NewLocalSigner(secrets)} + sig, err := signer.SignBlockV1(context.Background(), eth.ChainIDFromBig(cfg.L2ChainID), opsigner.PayloadHash(msg)) require.NoError(t, err) - result := verifyBlockSignature(logger, cfg, runCfg, peerId, sig[:], msg) + result := verifyBlockSignature(logger, cfg, runCfg, peerId, sig, msg) require.Equal(t, pubsub.ValidationReject, result) }) t.Run("InvalidSignature", func(t *testing.T) { runCfg := &testutils.MockRuntimeConfig{P2PSeqAddress: crypto.PubkeyToAddress(secrets.PublicKey)} - sig := make([]byte, 65) + sig := eth.Bytes65{} result := verifyBlockSignature(logger, cfg, runCfg, peerId, sig, msg) require.Equal(t, pubsub.ValidationReject, result) }) t.Run("NoSequencer", func(t *testing.T) { runCfg := &testutils.MockRuntimeConfig{} - remoteSigner, err := NewRemoteSigner(logger, signerCfg) + signer := &PreparedSigner{Signer: opsigner.NewLocalSigner(secrets)} + sig, err := signer.SignBlockV1(context.Background(), eth.ChainIDFromBig(cfg.L2ChainID), opsigner.PayloadHash(msg)) require.NoError(t, err) - signer := &PreparedSigner{Signer: remoteSigner} - sig, err := signer.Sign(context.Background(), SigningDomainBlocksV1, eth.ChainIDFromBig(cfg.L2ChainID), opsigner.PayloadHash(msg)) - require.NoError(t, err) - result := verifyBlockSignature(logger, cfg, runCfg, peerId, sig[:], msg) + result := verifyBlockSignature(logger, cfg, runCfg, peerId, sig, msg) require.Equal(t, pubsub.ValidationIgnore, result) }) - - t.Run("RemoteSignerNoTLS", func(t *testing.T) { - signerCfg := opsigner.NewCLIConfig() - signerCfg.Endpoint = fmt.Sprintf("http://%s", server.Endpoint()) - signerCfg.TLSConfig.TLSKey = "invalid" - signerCfg.TLSConfig.TLSCert = "invalid" - signerCfg.TLSConfig.TLSCaCert = "invalid" - signerCfg.TLSConfig.Enabled = true - - _, err := NewRemoteSigner(logger, signerCfg) - require.Error(t, err) - }) - - t.Run("RemoteSignerInvalidEndpoint", func(t *testing.T) { - signerCfg := opsigner.NewCLIConfig() - signerCfg.Endpoint = "Invalid" - signerCfg.TLSConfig.TLSKey = "" - signerCfg.TLSConfig.TLSCert = "" - signerCfg.TLSConfig.TLSCaCert = "" - _, err := NewRemoteSigner(logger, signerCfg) - require.Error(t, err) - }) } type MarshalSSZ interface { @@ -230,7 +112,7 @@ func createSignedP2Payload(payload MarshalSSZ, signer Signer, l2ChainID *big.Int } data := buf.Bytes() payloadData := data[65:] - sig, err := signer.Sign(context.TODO(), SigningDomainBlocksV1, eth.ChainIDFromBig(l2ChainID), opsigner.PayloadHash(payloadData)) + sig, err := signer.SignBlockV1(context.TODO(), eth.ChainIDFromBig(l2ChainID), opsigner.PayloadHash(payloadData)) if err != nil { return nil, fmt.Errorf("failed to sign execution payload with signer: %w", err) } @@ -268,7 +150,7 @@ func TestBlockValidator(t *testing.T) { secrets, err := crypto.GenerateKey() require.NoError(t, err) runCfg := &testutils.MockRuntimeConfig{P2PSeqAddress: crypto.PubkeyToAddress(secrets.PublicKey)} - signer := &PreparedSigner{Signer: NewLocalSigner(secrets)} + signer := &PreparedSigner{Signer: opsigner.NewLocalSigner(secrets)} // Params Set 2: Call the validation function peerID := peer.ID("foo") diff --git a/op-node/p2p/signer.go b/op-node/p2p/signer.go index 7c963c9511..a63c328c74 100644 --- a/op-node/p2p/signer.go +++ b/op-node/p2p/signer.go @@ -2,102 +2,12 @@ package p2p import ( "context" - "crypto/ecdsa" - "errors" - "io" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/log" - - "github.com/ethereum-optimism/optimism/op-node/rollup" - "github.com/ethereum-optimism/optimism/op-service/eth" opsigner "github.com/ethereum-optimism/optimism/op-service/signer" ) -var SigningDomainBlocksV1 = [32]byte{} - type Signer interface { - Sign(ctx context.Context, domain eth.Bytes32, chainID eth.ChainID, payloadHash common.Hash) (sig *[65]byte, err error) - io.Closer -} - -func BlockSigningHash(cfg *rollup.Config, payloadBytes []byte) (common.Hash, error) { - msg := opsigner.BlockSigningMessage{ - Domain: SigningDomainBlocksV1, - ChainID: eth.ChainIDFromBig(cfg.L2ChainID), - PayloadHash: opsigner.PayloadHash(payloadBytes), - } - return msg.ToSigningHash(), nil -} - -// LocalSigner is suitable for testing -type LocalSigner struct { - priv *ecdsa.PrivateKey -} - -func NewLocalSigner(priv *ecdsa.PrivateKey) *LocalSigner { - return &LocalSigner{priv: priv} -} - -func (s *LocalSigner) Sign(ctx context.Context, domain eth.Bytes32, chainID eth.ChainID, payloadHash common.Hash) (sig *[65]byte, err error) { - if s.priv == nil { - return nil, errors.New("signer is closed") - } - msg := opsigner.BlockSigningMessage{ - Domain: domain, - ChainID: chainID, - PayloadHash: payloadHash, - } - signingHash := msg.ToSigningHash() - signature, err := crypto.Sign(signingHash[:], s.priv) - if err != nil { - return nil, err - } - return (*[65]byte)(signature), nil -} - -func (s *LocalSigner) Close() error { - s.priv = nil - return nil -} - -type RemoteSigner struct { - client *opsigner.SignerClient - sender *common.Address -} - -func NewRemoteSigner(logger log.Logger, config opsigner.CLIConfig) (*RemoteSigner, error) { - signerClient, err := opsigner.NewSignerClientFromConfig(logger, config) - if err != nil { - return nil, err - } - senderAddress := common.HexToAddress(config.Address) - return &RemoteSigner{signerClient, &senderAddress}, nil -} - -func (s *RemoteSigner) Sign(ctx context.Context, domain eth.Bytes32, chainID eth.ChainID, payloadHash common.Hash) (sig *[65]byte, err error) { - if s.client == nil { - return nil, errors.New("signer is closed") - } - - // We use V1 for now, since the server may not support V2 yet - blockPayloadArgs := &opsigner.BlockPayloadArgs{ - Domain: domain, - ChainID: chainID.ToBig(), - PayloadHash: payloadHash[:], - SenderAddress: s.sender, - } - signature, err := s.client.SignBlockPayload(ctx, blockPayloadArgs) - if err != nil { - return nil, err - } - return &signature, nil -} - -func (s *RemoteSigner) Close() error { - s.client = nil - return nil + opsigner.BlockSigner } type PreparedSigner struct { diff --git a/op-node/p2p/signer_test.go b/op-node/p2p/signer_test.go deleted file mode 100644 index a466a66ff5..0000000000 --- a/op-node/p2p/signer_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package p2p - -import ( - "math/big" - "testing" - - "github.com/ethereum-optimism/optimism/op-node/rollup" - opsigner "github.com/ethereum-optimism/optimism/op-service/signer" - "github.com/stretchr/testify/require" -) - -func TestSigningHash_DifferentDomain(t *testing.T) { - cfg := &rollup.Config{ - L2ChainID: big.NewInt(100), - } - - payloadBytes := []byte("arbitraryData") - msg, err := opsigner.NewBlockPayloadArgs(SigningDomainBlocksV1, cfg.L2ChainID, payloadBytes, nil).Message() - require.NoError(t, err, "creating first signing hash") - - msg2, err := opsigner.NewBlockPayloadArgs([32]byte{3}, cfg.L2ChainID, payloadBytes, nil).Message() - require.NoError(t, err, "creating second signing hash") - - hash := msg.ToSigningHash() - hash2 := msg2.ToSigningHash() - require.NotEqual(t, hash, hash2, "signing hash should be different when domain is different") -} - -func TestSigningHash_DifferentChainID(t *testing.T) { - cfg1 := &rollup.Config{ - L2ChainID: big.NewInt(100), - } - cfg2 := &rollup.Config{ - L2ChainID: big.NewInt(101), - } - - payloadBytes := []byte("arbitraryData") - msg, err := opsigner.NewBlockPayloadArgs(SigningDomainBlocksV1, cfg1.L2ChainID, payloadBytes, nil).Message() - require.NoError(t, err, "creating first signing hash") - - msg2, err := opsigner.NewBlockPayloadArgs(SigningDomainBlocksV1, cfg2.L2ChainID, payloadBytes, nil).Message() - require.NoError(t, err, "creating second signing hash") - - hash := msg.ToSigningHash() - hash2 := msg2.ToSigningHash() - require.NotEqual(t, hash, hash2, "signing hash should be different when chain ID is different") -} - -func TestSigningHash_DifferentPayload(t *testing.T) { - cfg := &rollup.Config{ - L2ChainID: big.NewInt(100), - } - - msg, err := opsigner.NewBlockPayloadArgs(SigningDomainBlocksV1, cfg.L2ChainID, []byte("payload1"), nil).Message() - require.NoError(t, err, "creating first signing hash") - - msg2, err := opsigner.NewBlockPayloadArgs(SigningDomainBlocksV1, cfg.L2ChainID, []byte("payload2"), nil).Message() - require.NoError(t, err, "creating second signing hash") - - hash := msg.ToSigningHash() - hash2 := msg2.ToSigningHash() - require.NotEqual(t, hash, hash2, "signing hash should be different when payload is different") -} - -func TestSigningHash_LimitChainID(t *testing.T) { - // ChainID with bitlen 257 - chainID := big.NewInt(1) - chainID = chainID.SetBit(chainID, 256, 1) - cfg := &rollup.Config{ - L2ChainID: chainID, - } - _, err := opsigner.NewBlockPayloadArgs(SigningDomainBlocksV1, cfg.L2ChainID, []byte("arbitraryData"), nil).Message() - require.ErrorContains(t, err, "chain_id is too large") -} diff --git a/op-node/rollup/derive/attributes.go b/op-node/rollup/derive/attributes.go index ea060f9964..2260a2ebf4 100644 --- a/op-node/rollup/derive/attributes.go +++ b/op-node/rollup/derive/attributes.go @@ -138,13 +138,6 @@ func (ba *FetchingAttributesBuilder) PreparePayloadAttributes(ctx context.Contex } var afterForceIncludeTxs []hexutil.Bytes - if ba.rollupCfg.IsInterop(nextL2Time) { - depositsCompleteTx, err := DepositsCompleteBytes(seqNumber, l1Info) - if err != nil { - return nil, NewCriticalError(fmt.Errorf("failed to create depositsCompleteTx: %w", err)) - } - afterForceIncludeTxs = append(afterForceIncludeTxs, depositsCompleteTx) - } txs := make([]hexutil.Bytes, 0, 1+len(depositTxs)+len(afterForceIncludeTxs)+len(upgradeTxs)) txs = append(txs, l1InfoTx) diff --git a/op-node/rollup/derive/attributes_test.go b/op-node/rollup/derive/attributes_test.go index cf42e81696..2101b1e8bb 100644 --- a/op-node/rollup/derive/attributes_test.go +++ b/op-node/rollup/derive/attributes_test.go @@ -226,13 +226,11 @@ func TestPreparePayloadAttributes(t *testing.T) { epoch := l1Info.ID() l1InfoTx, err := L1InfoDepositBytes(cfg, testSysCfg, seqNumber, l1Info, 0) require.NoError(t, err) - depositsComplete, err := DepositsCompleteBytes(seqNumber, l1Info) require.NoError(t, err) var l2Txs []eth.Data l2Txs = append(l2Txs, l1InfoTx) l2Txs = append(l2Txs, userDepositTxs...) - l2Txs = append(l2Txs, depositsComplete) l1Fetcher.ExpectFetchReceipts(epoch.Hash, l1Info, receipts, nil) attrBuilder := NewFetchingAttributesBuilder(cfg, l1Fetcher, l1CfgFetcher) @@ -242,8 +240,7 @@ func TestPreparePayloadAttributes(t *testing.T) { require.Equal(t, l2Parent.Time+cfg.BlockTime, uint64(attrs.Timestamp)) require.Equal(t, eth.Bytes32(l1Info.InfoMixDigest), attrs.PrevRandao) require.Equal(t, predeploys.SequencerFeeVaultAddr, attrs.SuggestedFeeRecipient) - require.Equal(t, len(l2Txs), len(attrs.Transactions), "Expected txs to equal l1 info tx + user deposit txs + DepositsComplete") - require.Equal(t, eth.Data(depositsComplete).String(), attrs.Transactions[len(l2Txs)-1].String()) + require.Equal(t, len(l2Txs), len(attrs.Transactions), "Expected txs to equal l1 info tx + user deposit txs") require.Equal(t, l2Txs, attrs.Transactions) require.True(t, attrs.NoTxPool) }) @@ -267,12 +264,10 @@ func TestPreparePayloadAttributes(t *testing.T) { epoch := l1Info.ID() l1InfoTx, err := L1InfoDepositBytes(cfg, testSysCfg, seqNumber, l1Info, 0) require.NoError(t, err) - depositsComplete, err := DepositsCompleteBytes(seqNumber, l1Info) require.NoError(t, err) var l2Txs []eth.Data l2Txs = append(l2Txs, l1InfoTx) - l2Txs = append(l2Txs, depositsComplete) l1Fetcher.ExpectInfoByHash(epoch.Hash, l1Info, nil) attrBuilder := NewFetchingAttributesBuilder(cfg, l1Fetcher, l1CfgFetcher) @@ -282,8 +277,7 @@ func TestPreparePayloadAttributes(t *testing.T) { require.Equal(t, l2Parent.Time+cfg.BlockTime, uint64(attrs.Timestamp)) require.Equal(t, eth.Bytes32(l1Info.InfoMixDigest), attrs.PrevRandao) require.Equal(t, predeploys.SequencerFeeVaultAddr, attrs.SuggestedFeeRecipient) - require.Equal(t, len(l2Txs), len(attrs.Transactions), "Expected txs to equal l1 info tx + user deposit txs + DepositsComplete") - require.Equal(t, eth.Data(depositsComplete).String(), attrs.Transactions[len(l2Txs)-1].String()) + require.Equal(t, len(l2Txs), len(attrs.Transactions), "Expected txs to equal l1 info tx + user deposit txs") require.Equal(t, l2Txs, attrs.Transactions) require.True(t, attrs.NoTxPool) }) diff --git a/op-node/rollup/derive/batch_test.go b/op-node/rollup/derive/batch_test.go index 9e48f265d7..c206739c97 100644 --- a/op-node/rollup/derive/batch_test.go +++ b/op-node/rollup/derive/batch_test.go @@ -34,19 +34,21 @@ func RandomRawSpanBatch(rng *rand.Rand, chainId *big.Int) *RawSpanBatch { blockTxCounts = append(blockTxCounts, blockTxCount) totalblockTxCounts += blockTxCount } - londonSigner := types.NewLondonSigner(chainId) + signer := types.NewIsthmusSigner(chainId) var txs [][]byte for i := 0; i < int(totalblockTxCounts); i++ { var tx *types.Transaction - switch i % 4 { + switch i % 5 { case 0: tx = testutils.RandomLegacyTx(rng, types.HomesteadSigner{}) case 1: - tx = testutils.RandomLegacyTx(rng, londonSigner) + tx = testutils.RandomLegacyTx(rng, signer) case 2: - tx = testutils.RandomAccessListTx(rng, londonSigner) + tx = testutils.RandomAccessListTx(rng, signer) case 3: - tx = testutils.RandomDynamicFeeTx(rng, londonSigner) + tx = testutils.RandomDynamicFeeTx(rng, signer) + case 4: + tx = testutils.RandomSetCodeTx(rng, signer) } rawTx, err := tx.MarshalBinary() if err != nil { diff --git a/op-node/rollup/derive/batch_test_utils.go b/op-node/rollup/derive/batch_test_utils.go index 5783b5ec39..bf28c1e00e 100644 --- a/op-node/rollup/derive/batch_test_utils.go +++ b/op-node/rollup/derive/batch_test_utils.go @@ -11,7 +11,7 @@ import ( ) func RandomSingularBatch(rng *rand.Rand, txCount int, chainID *big.Int) *SingularBatch { - signer := types.NewLondonSigner(chainID) + signer := types.NewIsthmusSigner(chainID) baseFee := big.NewInt(rng.Int63n(300_000_000_000)) txsEncoded := make([]hexutil.Bytes, 0, txCount) // force each tx to have equal chainID diff --git a/op-node/rollup/derive/batches.go b/op-node/rollup/derive/batches.go index 4e3e20d1e5..102145f97a 100644 --- a/op-node/rollup/derive/batches.go +++ b/op-node/rollup/derive/batches.go @@ -161,6 +161,8 @@ func checkSingularBatch(cfg *rollup.Config, log log.Logger, l1Blocks []eth.L1Blo } } + isIsthmus := cfg.IsIsthmus(batch.Timestamp) + // We can do this check earlier, but it's a more intensive one, so we do this last. for i, txBytes := range batch.Transactions { if len(txBytes) == 0 { @@ -171,6 +173,10 @@ func checkSingularBatch(cfg *rollup.Config, log log.Logger, l1Blocks []eth.L1Blo log.Warn("sequencers may not embed any deposits into batch data, but found tx that has one", "tx_index", i) return BatchDrop } + if !isIsthmus && txBytes[0] == types.SetCodeTxType { + log.Warn("sequencers may not embed any SetCode transactions before Isthmus", "tx_index", i) + return BatchDrop + } } return BatchAccept diff --git a/op-node/rollup/derive/batches_test.go b/op-node/rollup/derive/batches_test.go index a50773c71d..b434eb5300 100644 --- a/op-node/rollup/derive/batches_test.go +++ b/op-node/rollup/derive/batches_test.go @@ -73,7 +73,7 @@ func TestValidBatch(t *testing.T) { rng := rand.New(rand.NewSource(1234)) chainId := new(big.Int).SetUint64(rng.Uint64()) - signer := types.NewLondonSigner(chainId) + signer := types.NewIsthmusSigner(chainId) randTx := testutils.RandomTx(rng, new(big.Int).SetUint64(rng.Uint64()), signer) randTxData, _ := randTx.MarshalBinary() @@ -573,7 +573,27 @@ func TestValidBatch(t *testing.T) { }, }, }, - Expected: BatchDrop, + Expected: BatchDrop, + ExpectedLog: "sequencers may not embed any deposits into batch data, but found tx that has one", + }, + { + Name: "setCode tx included pre-Isthmus", + L1Blocks: []eth.L1BlockRef{l1A, l1B}, + L2SafeHead: l2A0, + Batch: BatchWithL1InclusionBlock{ + L1InclusionBlock: l1B, + Batch: &SingularBatch{ + ParentHash: l2A1.ParentHash, + EpochNum: rollup.Epoch(l2A1.L1Origin.Number), + EpochHash: l2A1.L1Origin.Hash, + Timestamp: l2A1.Time, + Transactions: []hexutil.Bytes{ + []byte{types.SetCodeTxType, 0}, // piece of data alike to a SetCodeTx + }, + }, + }, + Expected: BatchDrop, + ExpectedLog: "sequencers may not embed any SetCode transactions before Isthmus", }, { Name: "valid batch same epoch", @@ -630,6 +650,7 @@ func TestValidBatch(t *testing.T) { Expected: BatchDrop, }, } + spanBatchTestCases := []ValidBatchTestCase{ { Name: "missing L1 info", diff --git a/op-node/rollup/derive/channel_out_test.go b/op-node/rollup/derive/channel_out_test.go index e82653cfd9..f3097318ac 100644 --- a/op-node/rollup/derive/channel_out_test.go +++ b/op-node/rollup/derive/channel_out_test.go @@ -360,7 +360,7 @@ type maxBlocksTest struct { func TestSpanChannelOut_MaxBlocksPerSpanBatch(t *testing.T) { for i, tt := range []maxBlocksTest{ { - outputSize: 10_751, + outputSize: 9_109, exactFull: true, numBatches: 15, maxBlocks: 4, @@ -368,14 +368,14 @@ func TestSpanChannelOut_MaxBlocksPerSpanBatch(t *testing.T) { expLastNumBlocks: 3, }, { - outputSize: 11_000, + outputSize: 9_200, numBatches: 16, maxBlocks: 4, expNumSpanBatches: 4, expLastNumBlocks: 3, }, { - outputSize: 11_154, + outputSize: 9_310, exactFull: true, numBatches: 16, maxBlocks: 4, @@ -383,14 +383,14 @@ func TestSpanChannelOut_MaxBlocksPerSpanBatch(t *testing.T) { expLastNumBlocks: 4, }, { - outputSize: 11_500, + outputSize: 9_400, numBatches: 17, maxBlocks: 4, expNumSpanBatches: 4, expLastNumBlocks: 4, }, { - outputSize: 11_801, + outputSize: 9_933, exactFull: true, numBatches: 17, maxBlocks: 4, @@ -398,7 +398,7 @@ func TestSpanChannelOut_MaxBlocksPerSpanBatch(t *testing.T) { expLastNumBlocks: 1, }, { - outputSize: 12_000, + outputSize: 10_000, numBatches: 18, maxBlocks: 4, expNumSpanBatches: 5, diff --git a/op-node/rollup/derive/fuzz_parsers_test.go b/op-node/rollup/derive/fuzz_parsers_test.go index f8f2374e6b..5b435f4563 100644 --- a/op-node/rollup/derive/fuzz_parsers_test.go +++ b/op-node/rollup/derive/fuzz_parsers_test.go @@ -93,14 +93,6 @@ func FuzzL1InfoEcotoneRoundTrip(f *testing.F) { if !cmp.Equal(in, out, cmp.Comparer(testutils.BigEqual)) { t.Fatalf("The Ecotone data did not round trip correctly. in: %v. out: %v", in, out) } - enc, err = in.marshalBinaryInterop() - if err != nil { - t.Fatalf("Failed to marshal Interop binary: %v", err) - } - err = out.unmarshalBinaryInterop(enc) - if err != nil { - t.Fatalf("Failed to unmarshal Interop binary: %v", err) - } if !cmp.Equal(in, out, cmp.Comparer(testutils.BigEqual)) { t.Fatalf("The Interop data did not round trip correctly. in: %v. out: %v", in, out) } diff --git a/op-node/rollup/derive/isthmus_upgrade_transactions.go b/op-node/rollup/derive/isthmus_upgrade_transactions.go index 31c575af46..25bee50b06 100644 --- a/op-node/rollup/derive/isthmus_upgrade_transactions.go +++ b/op-node/rollup/derive/isthmus_upgrade_transactions.go @@ -30,9 +30,9 @@ var ( OperatorFeeVaultAddress = crypto.CreateAddress(OperatorFeeVaultDeployerAddress, 0) // Bytecodes - l1BlockIsthmusDeploymentBytecode = common.FromHex("0x608060405234801561001057600080fd5b506106ae806100206000396000f3fe608060405234801561001057600080fd5b50600436106101825760003560e01c806364ca23ef116100d8578063b80777ea1161008c578063e591b28211610066578063e591b282146103b0578063e81b2c6d146103d2578063f8206140146103db57600080fd5b8063b80777ea14610337578063c598591814610357578063d84447151461037757600080fd5b80638381f58a116100bd5780638381f58a146103115780638b239f73146103255780639e8c49661461032e57600080fd5b806364ca23ef146102e157806368d5dca6146102f557600080fd5b80634397dfef1161013a57806354fd4d501161011457806354fd4d501461025d578063550fcdc91461029f5780635cf24969146102d857600080fd5b80634397dfef146101fc578063440a5e20146102245780634d5d9a2a1461022c57600080fd5b806309bd5a601161016b57806309bd5a60146101a457806316d3bc7f146101c057806321326849146101ed57600080fd5b8063015d8eb914610187578063098999be1461019c575b600080fd5b61019a6101953660046105bc565b6103e4565b005b61019a610523565b6101ad60025481565b6040519081526020015b60405180910390f35b6008546101d49067ffffffffffffffff1681565b60405167ffffffffffffffff90911681526020016101b7565b604051600081526020016101b7565b6040805173eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee815260126020820152016101b7565b61019a61052d565b6008546102489068010000000000000000900463ffffffff1681565b60405163ffffffff90911681526020016101b7565b60408051808201909152600c81527f312e352e312d626574612e37000000000000000000000000000000000000000060208201525b6040516101b7919061062e565b60408051808201909152600381527f45544800000000000000000000000000000000000000000000000000000000006020820152610292565b6101ad60015481565b6003546101d49067ffffffffffffffff1681565b6003546102489068010000000000000000900463ffffffff1681565b6000546101d49067ffffffffffffffff1681565b6101ad60055481565b6101ad60065481565b6000546101d49068010000000000000000900467ffffffffffffffff1681565b600354610248906c01000000000000000000000000900463ffffffff1681565b60408051808201909152600581527f45746865720000000000000000000000000000000000000000000000000000006020820152610292565b60405173deaddeaddeaddeaddeaddeaddeaddeaddead000181526020016101b7565b6101ad60045481565b6101ad60075481565b3373deaddeaddeaddeaddeaddeaddeaddeaddead00011461048b576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603b60248201527f4c31426c6f636b3a206f6e6c7920746865206465706f7369746f72206163636f60448201527f756e742063616e20736574204c3120626c6f636b2076616c7565730000000000606482015260840160405180910390fd5b6000805467ffffffffffffffff98891668010000000000000000027fffffffffffffffffffffffffffffffff00000000000000000000000000000000909116998916999099179890981790975560019490945560029290925560038054919094167fffffffffffffffffffffffffffffffffffffffffffffffff00000000000000009190911617909255600491909155600555600655565b61052b610535565b565b61052b610548565b61053d610548565b60a43560a01c600855565b73deaddeaddeaddeaddeaddeaddeaddeaddead000133811461057257633cc50b456000526004601cfd5b60043560801c60035560143560801c60005560243560015560443560075560643560025560843560045550565b803567ffffffffffffffff811681146105b757600080fd5b919050565b600080600080600080600080610100898b0312156105d957600080fd5b6105e28961059f565b97506105f060208a0161059f565b9650604089013595506060890135945061060c60808a0161059f565b979a969950949793969560a0850135955060c08501359460e001359350915050565b600060208083528351808285015260005b8181101561065b5785810183015185820160400152820161063f565b8181111561066d576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01692909201604001939250505056fea164736f6c634300080f000a") - gasPriceOracleIsthmusDeploymentBytecode = common.FromHex("0x608060405234801561001057600080fd5b50611c3c806100206000396000f3fe608060405234801561001057600080fd5b50600436106101775760003560e01c806368d5dca6116100d8578063c59859181161008c578063f45e65d811610066578063f45e65d8146102ca578063f8206140146102d2578063fe173b971461026957600080fd5b8063c59859181461029c578063de26c4a1146102a4578063f1c7a58b146102b757600080fd5b80638e98b106116100bd5780638e98b1061461026f578063960e3a2314610277578063b54501bc1461028957600080fd5b806368d5dca61461024c5780636ef25c3a1461026957600080fd5b8063313ce5671161012f5780634ef6e224116101145780634ef6e224146101de578063519b4bd3146101fb57806354fd4d501461020357600080fd5b8063313ce567146101c457806349948e0e146101cb57600080fd5b8063275aedd211610160578063275aedd2146101a1578063291b0383146101b45780632e0f2625146101bc57600080fd5b80630c18c1621461017c57806322b90ab314610197575b600080fd5b6101846102da565b6040519081526020015b60405180910390f35b61019f6103fb565b005b6101846101af36600461168e565b610584565b61019f61070f565b610184600681565b6006610184565b6101846101d93660046116d6565b610937565b6000546101eb9060ff1681565b604051901515815260200161018e565b61018461096e565b61023f6040518060400160405280600c81526020017f312e332e312d626574612e35000000000000000000000000000000000000000081525081565b60405161018e91906117a5565b6102546109cf565b60405163ffffffff909116815260200161018e565b48610184565b61019f610a54565b6000546101eb90610100900460ff1681565b6000546101eb9062010000900460ff1681565b610254610c4e565b6101846102b23660046116d6565b610caf565b6101846102c536600461168e565b610da9565b610184610e85565b610184610f78565b6000805460ff1615610373576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602860248201527f47617350726963654f7261636c653a206f76657268656164282920697320646560448201527f707265636174656400000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16638b239f736040518163ffffffff1660e01b8152600401602060405180830381865afa1580156103d2573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103f69190611818565b905090565b3373deaddeaddeaddeaddeaddeaddeaddeaddead0001146104c4576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604160248201527f47617350726963654f7261636c653a206f6e6c7920746865206465706f73697460448201527f6f72206163636f756e742063616e2073657420697345636f746f6e6520666c6160648201527f6700000000000000000000000000000000000000000000000000000000000000608482015260a40161036a565b60005460ff1615610557576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f47617350726963654f7261636c653a2045636f746f6e6520616c72656164792060448201527f6163746976650000000000000000000000000000000000000000000000000000606482015260840161036a565b600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055565b6000805462010000900460ff1661059d57506000919050565b610709620f42406106668473420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16634d5d9a2a6040518163ffffffff1660e01b8152600401602060405180830381865afa158015610607573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061062b9190611831565b63ffffffff167fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff821583830293840490921491909117011790565b6106709190611886565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff166316d3bc7f6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156106cf573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906106f391906118c1565b67ffffffffffffffff1681019081106000031790565b92915050565b3373deaddeaddeaddeaddeaddeaddeaddeaddead0001146107d8576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604160248201527f47617350726963654f7261636c653a206f6e6c7920746865206465706f73697460448201527f6f72206163636f756e742063616e20736574206973497374686d757320666c6160648201527f6700000000000000000000000000000000000000000000000000000000000000608482015260a40161036a565b600054610100900460ff1661086f576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527f47617350726963654f7261636c653a20497374686d75732063616e206f6e6c7960448201527f2062652061637469766174656420616674657220466a6f726400000000000000606482015260840161036a565b60005462010000900460ff1615610908576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f47617350726963654f7261636c653a20497374686d757320616c72656164792060448201527f6163746976650000000000000000000000000000000000000000000000000000606482015260840161036a565b600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffff1662010000179055565b60008054610100900460ff16156109515761070982610fd9565b60005460ff16156109655761070982610ff8565b6107098261109c565b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16635cf249696040518163ffffffff1660e01b8152600401602060405180830381865afa1580156103d2573d6000803e3d6000fd5b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff166368d5dca66040518163ffffffff1660e01b8152600401602060405180830381865afa158015610a30573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103f69190611831565b3373deaddeaddeaddeaddeaddeaddeaddeaddead000114610af7576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603f60248201527f47617350726963654f7261636c653a206f6e6c7920746865206465706f73697460448201527f6f72206163636f756e742063616e20736574206973466a6f726420666c616700606482015260840161036a565b60005460ff16610b89576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527f47617350726963654f7261636c653a20466a6f72642063616e206f6e6c79206260448201527f65206163746976617465642061667465722045636f746f6e6500000000000000606482015260840161036a565b600054610100900460ff1615610c20576040517f08c379a0000000000000000000000000000000000000000000000000000000008152602060048201526024808201527f47617350726963654f7261636c653a20466a6f726420616c726561647920616360448201527f7469766500000000000000000000000000000000000000000000000000000000606482015260840161036a565b600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff16610100179055565b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff1663c59859186040518163ffffffff1660e01b8152600401602060405180830381865afa158015610a30573d6000803e3d6000fd5b60008054610100900460ff1615610cf657620f4240610ce1610cd0846111f0565b51610cdc9060446118eb565b61150d565b610cec906010611903565b6107099190611886565b6000610d018361156c565b60005490915060ff1615610d155792915050565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16638b239f736040518163ffffffff1660e01b8152600401602060405180830381865afa158015610d74573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610d989190611818565b610da290826118eb565b9392505050565b60008054610100900460ff16610e41576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603660248201527f47617350726963654f7261636c653a206765744c314665655570706572426f7560448201527f6e64206f6e6c7920737570706f72747320466a6f726400000000000000000000606482015260840161036a565b6000610e4e8360446118eb565b90506000610e5d60ff83611886565b610e6790836118eb565b610e729060106118eb565b9050610e7d816115fc565b949350505050565b6000805460ff1615610f19576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f47617350726963654f7261636c653a207363616c61722829206973206465707260448201527f6563617465640000000000000000000000000000000000000000000000000000606482015260840161036a565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16639e8c49666040518163ffffffff1660e01b8152600401602060405180830381865afa1580156103d2573d6000803e3d6000fd5b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff1663f82061406040518163ffffffff1660e01b8152600401602060405180830381865afa1580156103d2573d6000803e3d6000fd5b6000610709610fe7836111f0565b51610ff39060446118eb565b6115fc565b6000806110048361156c565b9050600061101061096e565b611018610c4e565b611023906010611940565b63ffffffff166110339190611903565b9050600061103f610f78565b6110476109cf565b63ffffffff166110579190611903565b9050600061106582846118eb565b61106f9085611903565b905061107d6006600a611a8c565b611088906010611903565b6110929082611886565b9695505050505050565b6000806110a88361156c565b9050600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16639e8c49666040518163ffffffff1660e01b8152600401602060405180830381865afa15801561110b573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061112f9190611818565b61113761096e565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16638b239f736040518163ffffffff1660e01b8152600401602060405180830381865afa158015611196573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906111ba9190611818565b6111c490856118eb565b6111ce9190611903565b6111d89190611903565b90506111e66006600a611a8c565b610e7d9082611886565b606061137f565b818153600101919050565b600082840393505b83811015610da25782810151828201511860001a159093029260010161120a565b825b60208210611277578251611242601f836111f7565b52602092909201917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09091019060210161122d565b8115610da257825161128c60018403836111f7565b520160010192915050565b60006001830392505b61010782106112d8576112ca8360ff166112c560fd6112c58760081c60e001896111f7565b6111f7565b9350610106820391506112a0565b60078210611305576112fe8360ff166112c5600785036112c58760081c60e001896111f7565b9050610da2565b610e7d8360ff166112c58560081c8560051b01876111f7565b61137782820361135b61134b84600081518060001a8160011a60081b178160021a60101b17915050919050565b639e3779b90260131c611fff1690565b8060021b6040510182815160e01c1860e01b8151188152505050565b600101919050565b6180003860405139618000604051016020830180600d8551820103826002015b818110156114b2576000805b50508051604051600082901a600183901a60081b1760029290921a60101b91909117639e3779b9810260111c617ffc16909101805160e081811c878603811890911b909118909152840190818303908484106114075750611442565b600184019350611fff821161143c578251600081901a600182901a60081b1760029190911a60101b17810361143c5750611442565b506113ab565b8383106114505750506114b2565b6001830392508583111561146e5761146b878788860361122b565b96505b611482600985016003850160038501611202565b915061148f878284611297565b9650506114a7846114a28684860161131e565b61131e565b91505080935061139f565b50506114c4838384885185010361122b565b925050506040519150618000820180820391508183526020830160005b838110156114f95782810151828201526020016114e1565b506000920191825250602001604052919050565b60008061151d83620cc394611903565b611547907ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd763200611a98565b90506115576064620f4240611b0c565b81121561070957610da26064620f4240611b0c565b80516000908190815b818110156115ef5784818151811061158f5761158f611bc8565b01602001517fff00000000000000000000000000000000000000000000000000000000000000166000036115cf576115c86004846118eb565b92506115dd565b6115da6010846118eb565b92505b806115e781611bf7565b915050611575565b50610e7d826104406118eb565b6000806116088361150d565b90506000611614610f78565b61161c6109cf565b63ffffffff1661162c9190611903565b61163461096e565b61163c610c4e565b611647906010611940565b63ffffffff166116579190611903565b61166191906118eb565b905061166f60066002611903565b61167a90600a611a8c565b6116848284611903565b610e7d9190611886565b6000602082840312156116a057600080fd5b5035919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000602082840312156116e857600080fd5b813567ffffffffffffffff8082111561170057600080fd5b818401915084601f83011261171457600080fd5b813581811115611726576117266116a7565b604051601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0908116603f0116810190838211818310171561176c5761176c6116a7565b8160405282815287602084870101111561178557600080fd5b826020860160208301376000928101602001929092525095945050505050565b600060208083528351808285015260005b818110156117d2578581018301518582016040015282016117b6565b818111156117e4576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b60006020828403121561182a57600080fd5b5051919050565b60006020828403121561184357600080fd5b815163ffffffff81168114610da257600080fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000826118bc577f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b500490565b6000602082840312156118d357600080fd5b815167ffffffffffffffff81168114610da257600080fd5b600082198211156118fe576118fe611857565b500190565b6000817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff048311821515161561193b5761193b611857565b500290565b600063ffffffff8083168185168183048111821515161561196357611963611857565b02949350505050565b600181815b808511156119c557817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff048211156119ab576119ab611857565b808516156119b857918102915b93841c9390800290611971565b509250929050565b6000826119dc57506001610709565b816119e957506000610709565b81600181146119ff5760028114611a0957611a25565b6001915050610709565b60ff841115611a1a57611a1a611857565b50506001821b610709565b5060208310610133831016604e8410600b8410161715611a48575081810a610709565b611a52838361196c565b807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff04821115611a8457611a84611857565b029392505050565b6000610da283836119cd565b6000808212827f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03841381151615611ad257611ad2611857565b827f8000000000000000000000000000000000000000000000000000000000000000038412811615611b0657611b06611857565b50500190565b60007f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff600084136000841385830485118282161615611b4d57611b4d611857565b7f80000000000000000000000000000000000000000000000000000000000000006000871286820588128184161615611b8857611b88611857565b60008712925087820587128484161615611ba457611ba4611857565b87850587128184161615611bba57611bba611857565b505050929093029392505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8203611c2857611c28611857565b506001019056fea164736f6c634300080f000a") - operatorFeeVaultDeploymentBytecode = common.FromHex("0x60e060405234801561001057600080fd5b5073420000000000000000000000000000000000001960a0526000608052600160c05260805160a05160c0516107ef6100a7600039600081816101b3015281816102450152818161044b015261048601526000818160b8015281816101800152818161039a01528181610429015281816104c201526105b70152600081816101ef01528181610279015261029d01526107ef6000f3fe60806040526004361061009a5760003560e01c806382356d8a1161006957806384411d651161004e57806384411d651461021d578063d0e12f9014610233578063d3e5792b1461026757600080fd5b806382356d8a146101a45780638312f149146101e057600080fd5b80630d9019e1146100a65780633ccfd60b1461010457806354fd4d501461011b57806366d003ac1461017157600080fd5b366100a157005b600080fd5b3480156100b257600080fd5b506100da7f000000000000000000000000000000000000000000000000000000000000000081565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020015b60405180910390f35b34801561011057600080fd5b5061011961029b565b005b34801561012757600080fd5b506101646040518060400160405280600c81526020017f312e352e302d626574612e35000000000000000000000000000000000000000081525081565b6040516100fb9190610671565b34801561017d57600080fd5b507f00000000000000000000000000000000000000000000000000000000000000006100da565b3480156101b057600080fd5b507f00000000000000000000000000000000000000000000000000000000000000005b6040516100fb919061074e565b3480156101ec57600080fd5b507f00000000000000000000000000000000000000000000000000000000000000005b6040519081526020016100fb565b34801561022957600080fd5b5061020f60005481565b34801561023f57600080fd5b506101d37f000000000000000000000000000000000000000000000000000000000000000081565b34801561027357600080fd5b5061020f7f000000000000000000000000000000000000000000000000000000000000000081565b7f0000000000000000000000000000000000000000000000000000000000000000471015610376576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604a60248201527f4665655661756c743a207769746864726177616c20616d6f756e74206d75737460448201527f2062652067726561746572207468616e206d696e696d756d207769746864726160648201527f77616c20616d6f756e7400000000000000000000000000000000000000000000608482015260a4015b60405180910390fd5b60004790508060008082825461038c9190610762565b9091555050604080518281527f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff166020820152338183015290517fc8a211cc64b6ed1b50595a9fcb1932b6d1e5a6e8ef15b60e5b1f988ea9086bba9181900360600190a17f38e04cbeb8c10f8f568618aa75be0f10b6729b8b4237743b4de20cbcde2839ee817f0000000000000000000000000000000000000000000000000000000000000000337f000000000000000000000000000000000000000000000000000000000000000060405161047a94939291906107a1565b60405180910390a160017f000000000000000000000000000000000000000000000000000000000000000060018111156104b6576104b66106e4565b0361057a5760006104e77f000000000000000000000000000000000000000000000000000000000000000083610649565b905080610576576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603060248201527f4665655661756c743a206661696c656420746f2073656e642045544820746f2060448201527f4c322066656520726563697069656e7400000000000000000000000000000000606482015260840161036d565b5050565b6040517fc2b3e5ac00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016600482015262061a80602482015260606044820152600060648201527342000000000000000000000000000000000000169063c2b3e5ac9083906084016000604051808303818588803b15801561062d57600080fd5b505af1158015610641573d6000803e3d6000fd5b505050505050565b6000610656835a8461065d565b9392505050565b6000806000806000858888f1949350505050565b600060208083528351808285015260005b8181101561069e57858101830151858201604001528201610682565b818111156106b0576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b6002811061074a577f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b9052565b6020810161075c8284610713565b92915050565b6000821982111561079c577f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b500190565b84815273ffffffffffffffffffffffffffffffffffffffff848116602083015283166040820152608081016107d96060830184610713565b9594505050505056fea164736f6c634300080f000a") + l1BlockIsthmusDeploymentBytecode = common.FromHex("0x608060405234801561001057600080fd5b506106ae806100206000396000f3fe608060405234801561001057600080fd5b50600436106101825760003560e01c806364ca23ef116100d8578063b80777ea1161008c578063e591b28211610066578063e591b282146103b0578063e81b2c6d146103d2578063f8206140146103db57600080fd5b8063b80777ea14610337578063c598591814610357578063d84447151461037757600080fd5b80638381f58a116100bd5780638381f58a146103115780638b239f73146103255780639e8c49661461032e57600080fd5b806364ca23ef146102e157806368d5dca6146102f557600080fd5b80634397dfef1161013a57806354fd4d501161011457806354fd4d501461025d578063550fcdc91461029f5780635cf24969146102d857600080fd5b80634397dfef146101fc578063440a5e20146102245780634d5d9a2a1461022c57600080fd5b806309bd5a601161016b57806309bd5a60146101a457806316d3bc7f146101c057806321326849146101ed57600080fd5b8063015d8eb914610187578063098999be1461019c575b600080fd5b61019a6101953660046105bc565b6103e4565b005b61019a610523565b6101ad60025481565b6040519081526020015b60405180910390f35b6008546101d49067ffffffffffffffff1681565b60405167ffffffffffffffff90911681526020016101b7565b604051600081526020016101b7565b6040805173eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee815260126020820152016101b7565b61019a61052d565b6008546102489068010000000000000000900463ffffffff1681565b60405163ffffffff90911681526020016101b7565b60408051808201909152600581527f312e362e3000000000000000000000000000000000000000000000000000000060208201525b6040516101b7919061062e565b60408051808201909152600381527f45544800000000000000000000000000000000000000000000000000000000006020820152610292565b6101ad60015481565b6003546101d49067ffffffffffffffff1681565b6003546102489068010000000000000000900463ffffffff1681565b6000546101d49067ffffffffffffffff1681565b6101ad60055481565b6101ad60065481565b6000546101d49068010000000000000000900467ffffffffffffffff1681565b600354610248906c01000000000000000000000000900463ffffffff1681565b60408051808201909152600581527f45746865720000000000000000000000000000000000000000000000000000006020820152610292565b60405173deaddeaddeaddeaddeaddeaddeaddeaddead000181526020016101b7565b6101ad60045481565b6101ad60075481565b3373deaddeaddeaddeaddeaddeaddeaddeaddead00011461048b576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603b60248201527f4c31426c6f636b3a206f6e6c7920746865206465706f7369746f72206163636f60448201527f756e742063616e20736574204c3120626c6f636b2076616c7565730000000000606482015260840160405180910390fd5b6000805467ffffffffffffffff98891668010000000000000000027fffffffffffffffffffffffffffffffff00000000000000000000000000000000909116998916999099179890981790975560019490945560029290925560038054919094167fffffffffffffffffffffffffffffffffffffffffffffffff00000000000000009190911617909255600491909155600555600655565b61052b610535565b565b61052b610548565b61053d610548565b60a43560a01c600855565b73deaddeaddeaddeaddeaddeaddeaddeaddead000133811461057257633cc50b456000526004601cfd5b60043560801c60035560143560801c60005560243560015560443560075560643560025560843560045550565b803567ffffffffffffffff811681146105b757600080fd5b919050565b600080600080600080600080610100898b0312156105d957600080fd5b6105e28961059f565b97506105f060208a0161059f565b9650604089013595506060890135945061060c60808a0161059f565b979a969950949793969560a0850135955060c08501359460e001359350915050565b600060208083528351808285015260005b8181101561065b5785810183015185820160400152820161063f565b8181111561066d576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01692909201604001939250505056fea164736f6c634300080f000a") + gasPriceOracleIsthmusDeploymentBytecode = common.FromHex("0x608060405234801561001057600080fd5b50611c3c806100206000396000f3fe608060405234801561001057600080fd5b50600436106101775760003560e01c806368d5dca6116100d8578063c59859181161008c578063f45e65d811610066578063f45e65d8146102ca578063f8206140146102d2578063fe173b971461026957600080fd5b8063c59859181461029c578063de26c4a1146102a4578063f1c7a58b146102b757600080fd5b80638e98b106116100bd5780638e98b1061461026f578063960e3a2314610277578063b54501bc1461028957600080fd5b806368d5dca61461024c5780636ef25c3a1461026957600080fd5b8063313ce5671161012f5780634ef6e224116101145780634ef6e224146101de578063519b4bd3146101fb57806354fd4d501461020357600080fd5b8063313ce567146101c457806349948e0e146101cb57600080fd5b8063275aedd211610160578063275aedd2146101a1578063291b0383146101b45780632e0f2625146101bc57600080fd5b80630c18c1621461017c57806322b90ab314610197575b600080fd5b6101846102da565b6040519081526020015b60405180910390f35b61019f6103fb565b005b6101846101af36600461168e565b610584565b61019f61070f565b610184600681565b6006610184565b6101846101d93660046116d6565b610937565b6000546101eb9060ff1681565b604051901515815260200161018e565b61018461096e565b61023f6040518060400160405280600581526020017f312e342e3000000000000000000000000000000000000000000000000000000081525081565b60405161018e91906117a5565b6102546109cf565b60405163ffffffff909116815260200161018e565b48610184565b61019f610a54565b6000546101eb90610100900460ff1681565b6000546101eb9062010000900460ff1681565b610254610c4e565b6101846102b23660046116d6565b610caf565b6101846102c536600461168e565b610da9565b610184610e85565b610184610f78565b6000805460ff1615610373576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602860248201527f47617350726963654f7261636c653a206f76657268656164282920697320646560448201527f707265636174656400000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16638b239f736040518163ffffffff1660e01b8152600401602060405180830381865afa1580156103d2573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103f69190611818565b905090565b3373deaddeaddeaddeaddeaddeaddeaddeaddead0001146104c4576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604160248201527f47617350726963654f7261636c653a206f6e6c7920746865206465706f73697460448201527f6f72206163636f756e742063616e2073657420697345636f746f6e6520666c6160648201527f6700000000000000000000000000000000000000000000000000000000000000608482015260a40161036a565b60005460ff1615610557576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f47617350726963654f7261636c653a2045636f746f6e6520616c72656164792060448201527f6163746976650000000000000000000000000000000000000000000000000000606482015260840161036a565b600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00166001179055565b6000805462010000900460ff1661059d57506000919050565b610709620f42406106668473420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16634d5d9a2a6040518163ffffffff1660e01b8152600401602060405180830381865afa158015610607573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061062b9190611831565b63ffffffff167fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff821583830293840490921491909117011790565b6106709190611886565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff166316d3bc7f6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156106cf573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906106f391906118c1565b67ffffffffffffffff1681019081106000031790565b92915050565b3373deaddeaddeaddeaddeaddeaddeaddeaddead0001146107d8576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604160248201527f47617350726963654f7261636c653a206f6e6c7920746865206465706f73697460448201527f6f72206163636f756e742063616e20736574206973497374686d757320666c6160648201527f6700000000000000000000000000000000000000000000000000000000000000608482015260a40161036a565b600054610100900460ff1661086f576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527f47617350726963654f7261636c653a20497374686d75732063616e206f6e6c7960448201527f2062652061637469766174656420616674657220466a6f726400000000000000606482015260840161036a565b60005462010000900460ff1615610908576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f47617350726963654f7261636c653a20497374686d757320616c72656164792060448201527f6163746976650000000000000000000000000000000000000000000000000000606482015260840161036a565b600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffff1662010000179055565b60008054610100900460ff16156109515761070982610fd9565b60005460ff16156109655761070982610ff8565b6107098261109c565b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16635cf249696040518163ffffffff1660e01b8152600401602060405180830381865afa1580156103d2573d6000803e3d6000fd5b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff166368d5dca66040518163ffffffff1660e01b8152600401602060405180830381865afa158015610a30573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103f69190611831565b3373deaddeaddeaddeaddeaddeaddeaddeaddead000114610af7576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603f60248201527f47617350726963654f7261636c653a206f6e6c7920746865206465706f73697460448201527f6f72206163636f756e742063616e20736574206973466a6f726420666c616700606482015260840161036a565b60005460ff16610b89576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527f47617350726963654f7261636c653a20466a6f72642063616e206f6e6c79206260448201527f65206163746976617465642061667465722045636f746f6e6500000000000000606482015260840161036a565b600054610100900460ff1615610c20576040517f08c379a0000000000000000000000000000000000000000000000000000000008152602060048201526024808201527f47617350726963654f7261636c653a20466a6f726420616c726561647920616360448201527f7469766500000000000000000000000000000000000000000000000000000000606482015260840161036a565b600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff16610100179055565b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff1663c59859186040518163ffffffff1660e01b8152600401602060405180830381865afa158015610a30573d6000803e3d6000fd5b60008054610100900460ff1615610cf657620f4240610ce1610cd0846111f0565b51610cdc9060446118eb565b61150d565b610cec906010611903565b6107099190611886565b6000610d018361156c565b60005490915060ff1615610d155792915050565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16638b239f736040518163ffffffff1660e01b8152600401602060405180830381865afa158015610d74573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610d989190611818565b610da290826118eb565b9392505050565b60008054610100900460ff16610e41576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603660248201527f47617350726963654f7261636c653a206765744c314665655570706572426f7560448201527f6e64206f6e6c7920737570706f72747320466a6f726400000000000000000000606482015260840161036a565b6000610e4e8360446118eb565b90506000610e5d60ff83611886565b610e6790836118eb565b610e729060106118eb565b9050610e7d816115fc565b949350505050565b6000805460ff1615610f19576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f47617350726963654f7261636c653a207363616c61722829206973206465707260448201527f6563617465640000000000000000000000000000000000000000000000000000606482015260840161036a565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16639e8c49666040518163ffffffff1660e01b8152600401602060405180830381865afa1580156103d2573d6000803e3d6000fd5b600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff1663f82061406040518163ffffffff1660e01b8152600401602060405180830381865afa1580156103d2573d6000803e3d6000fd5b6000610709610fe7836111f0565b51610ff39060446118eb565b6115fc565b6000806110048361156c565b9050600061101061096e565b611018610c4e565b611023906010611940565b63ffffffff166110339190611903565b9050600061103f610f78565b6110476109cf565b63ffffffff166110579190611903565b9050600061106582846118eb565b61106f9085611903565b905061107d6006600a611a8c565b611088906010611903565b6110929082611886565b9695505050505050565b6000806110a88361156c565b9050600073420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16639e8c49666040518163ffffffff1660e01b8152600401602060405180830381865afa15801561110b573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061112f9190611818565b61113761096e565b73420000000000000000000000000000000000001573ffffffffffffffffffffffffffffffffffffffff16638b239f736040518163ffffffff1660e01b8152600401602060405180830381865afa158015611196573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906111ba9190611818565b6111c490856118eb565b6111ce9190611903565b6111d89190611903565b90506111e66006600a611a8c565b610e7d9082611886565b606061137f565b818153600101919050565b600082840393505b83811015610da25782810151828201511860001a159093029260010161120a565b825b60208210611277578251611242601f836111f7565b52602092909201917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09091019060210161122d565b8115610da257825161128c60018403836111f7565b520160010192915050565b60006001830392505b61010782106112d8576112ca8360ff166112c560fd6112c58760081c60e001896111f7565b6111f7565b9350610106820391506112a0565b60078210611305576112fe8360ff166112c5600785036112c58760081c60e001896111f7565b9050610da2565b610e7d8360ff166112c58560081c8560051b01876111f7565b61137782820361135b61134b84600081518060001a8160011a60081b178160021a60101b17915050919050565b639e3779b90260131c611fff1690565b8060021b6040510182815160e01c1860e01b8151188152505050565b600101919050565b6180003860405139618000604051016020830180600d8551820103826002015b818110156114b2576000805b50508051604051600082901a600183901a60081b1760029290921a60101b91909117639e3779b9810260111c617ffc16909101805160e081811c878603811890911b909118909152840190818303908484106114075750611442565b600184019350611fff821161143c578251600081901a600182901a60081b1760029190911a60101b17810361143c5750611442565b506113ab565b8383106114505750506114b2565b6001830392508583111561146e5761146b878788860361122b565b96505b611482600985016003850160038501611202565b915061148f878284611297565b9650506114a7846114a28684860161131e565b61131e565b91505080935061139f565b50506114c4838384885185010361122b565b925050506040519150618000820180820391508183526020830160005b838110156114f95782810151828201526020016114e1565b506000920191825250602001604052919050565b60008061151d83620cc394611903565b611547907ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd763200611a98565b90506115576064620f4240611b0c565b81121561070957610da26064620f4240611b0c565b80516000908190815b818110156115ef5784818151811061158f5761158f611bc8565b01602001517fff00000000000000000000000000000000000000000000000000000000000000166000036115cf576115c86004846118eb565b92506115dd565b6115da6010846118eb565b92505b806115e781611bf7565b915050611575565b50610e7d826104406118eb565b6000806116088361150d565b90506000611614610f78565b61161c6109cf565b63ffffffff1661162c9190611903565b61163461096e565b61163c610c4e565b611647906010611940565b63ffffffff166116579190611903565b61166191906118eb565b905061166f60066002611903565b61167a90600a611a8c565b6116848284611903565b610e7d9190611886565b6000602082840312156116a057600080fd5b5035919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000602082840312156116e857600080fd5b813567ffffffffffffffff8082111561170057600080fd5b818401915084601f83011261171457600080fd5b813581811115611726576117266116a7565b604051601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0908116603f0116810190838211818310171561176c5761176c6116a7565b8160405282815287602084870101111561178557600080fd5b826020860160208301376000928101602001929092525095945050505050565b600060208083528351808285015260005b818110156117d2578581018301518582016040015282016117b6565b818111156117e4576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b60006020828403121561182a57600080fd5b5051919050565b60006020828403121561184357600080fd5b815163ffffffff81168114610da257600080fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000826118bc577f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b500490565b6000602082840312156118d357600080fd5b815167ffffffffffffffff81168114610da257600080fd5b600082198211156118fe576118fe611857565b500190565b6000817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff048311821515161561193b5761193b611857565b500290565b600063ffffffff8083168185168183048111821515161561196357611963611857565b02949350505050565b600181815b808511156119c557817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff048211156119ab576119ab611857565b808516156119b857918102915b93841c9390800290611971565b509250929050565b6000826119dc57506001610709565b816119e957506000610709565b81600181146119ff5760028114611a0957611a25565b6001915050610709565b60ff841115611a1a57611a1a611857565b50506001821b610709565b5060208310610133831016604e8410600b8410161715611a48575081810a610709565b611a52838361196c565b807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff04821115611a8457611a84611857565b029392505050565b6000610da283836119cd565b6000808212827f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03841381151615611ad257611ad2611857565b827f8000000000000000000000000000000000000000000000000000000000000000038412811615611b0657611b06611857565b50500190565b60007f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff600084136000841385830485118282161615611b4d57611b4d611857565b7f80000000000000000000000000000000000000000000000000000000000000006000871286820588128184161615611b8857611b88611857565b60008712925087820587128484161615611ba457611ba4611857565b87850587128184161615611bba57611bba611857565b505050929093029392505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8203611c2857611c28611857565b506001019056fea164736f6c634300080f000a") + operatorFeeVaultDeploymentBytecode = common.FromHex("0x60e060405234801561001057600080fd5b5073420000000000000000000000000000000000001960a0526000608052600160c05260805160a05160c0516107ef6100a7600039600081816101b3015281816102450152818161044b015261048601526000818160b8015281816101800152818161039a01528181610429015281816104c201526105b70152600081816101ef01528181610279015261029d01526107ef6000f3fe60806040526004361061009a5760003560e01c806382356d8a1161006957806384411d651161004e57806384411d651461021d578063d0e12f9014610233578063d3e5792b1461026757600080fd5b806382356d8a146101a45780638312f149146101e057600080fd5b80630d9019e1146100a65780633ccfd60b1461010457806354fd4d501461011b57806366d003ac1461017157600080fd5b366100a157005b600080fd5b3480156100b257600080fd5b506100da7f000000000000000000000000000000000000000000000000000000000000000081565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020015b60405180910390f35b34801561011057600080fd5b5061011961029b565b005b34801561012757600080fd5b506101646040518060400160405280600581526020017f312e302e3000000000000000000000000000000000000000000000000000000081525081565b6040516100fb9190610671565b34801561017d57600080fd5b507f00000000000000000000000000000000000000000000000000000000000000006100da565b3480156101b057600080fd5b507f00000000000000000000000000000000000000000000000000000000000000005b6040516100fb919061074e565b3480156101ec57600080fd5b507f00000000000000000000000000000000000000000000000000000000000000005b6040519081526020016100fb565b34801561022957600080fd5b5061020f60005481565b34801561023f57600080fd5b506101d37f000000000000000000000000000000000000000000000000000000000000000081565b34801561027357600080fd5b5061020f7f000000000000000000000000000000000000000000000000000000000000000081565b7f0000000000000000000000000000000000000000000000000000000000000000471015610376576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604a60248201527f4665655661756c743a207769746864726177616c20616d6f756e74206d75737460448201527f2062652067726561746572207468616e206d696e696d756d207769746864726160648201527f77616c20616d6f756e7400000000000000000000000000000000000000000000608482015260a4015b60405180910390fd5b60004790508060008082825461038c9190610762565b9091555050604080518281527f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff166020820152338183015290517fc8a211cc64b6ed1b50595a9fcb1932b6d1e5a6e8ef15b60e5b1f988ea9086bba9181900360600190a17f38e04cbeb8c10f8f568618aa75be0f10b6729b8b4237743b4de20cbcde2839ee817f0000000000000000000000000000000000000000000000000000000000000000337f000000000000000000000000000000000000000000000000000000000000000060405161047a94939291906107a1565b60405180910390a160017f000000000000000000000000000000000000000000000000000000000000000060018111156104b6576104b66106e4565b0361057a5760006104e77f000000000000000000000000000000000000000000000000000000000000000083610649565b905080610576576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603060248201527f4665655661756c743a206661696c656420746f2073656e642045544820746f2060448201527f4c322066656520726563697069656e7400000000000000000000000000000000606482015260840161036d565b5050565b6040517fc2b3e5ac00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016600482015262061a80602482015260606044820152600060648201527342000000000000000000000000000000000000169063c2b3e5ac9083906084016000604051808303818588803b15801561062d57600080fd5b505af1158015610641573d6000803e3d6000fd5b505050505050565b6000610656835a8461065d565b9392505050565b6000806000806000858888f1949350505050565b600060208083528351808285015260005b8181101561069e57858101830151858201604001528201610682565b818111156106b0576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b6002811061074a577f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b9052565b6020810161075c8284610713565b92915050565b6000821982111561079c577f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b500190565b84815273ffffffffffffffffffffffffffffffffffffffff848116602083015283166040820152608081016107d96060830184610713565b9594505050505056fea164736f6c634300080f000a") // Enable Isthmus Parameters enableIsthmusSource = UpgradeDepositSource{Intent: "Isthmus: Gas Price Oracle Set Isthmus"} diff --git a/op-node/rollup/derive/l1_block_info.go b/op-node/rollup/derive/l1_block_info.go index 0fae5de811..ddeb35c5cd 100644 --- a/op-node/rollup/derive/l1_block_info.go +++ b/op-node/rollup/derive/l1_block_info.go @@ -21,27 +21,16 @@ const ( L1InfoFuncBedrockSignature = "setL1BlockValues(uint64,uint64,uint256,bytes32,uint64,bytes32,uint256,uint256)" L1InfoFuncEcotoneSignature = "setL1BlockValuesEcotone()" L1InfoFuncIsthmusSignature = "setL1BlockValuesIsthmus()" - L1InfoFuncInteropSignature = "setL1BlockValuesInterop()" - DepositsCompleteSignature = "depositsComplete()" L1InfoArguments = 8 L1InfoBedrockLen = 4 + 32*L1InfoArguments L1InfoEcotoneLen = 4 + 32*5 // after Ecotone upgrade, args are packed into 5 32-byte slots L1InfoIsthmusLen = 4 + 32*5 + 4 + 8 // after Isthmus upgrade, additionally pack in operator fee scalar and constant - DepositsCompleteLen = 4 // only the selector - // DepositsCompleteGas allocates 21k gas for intrinsic tx costs, and - // an additional 15k to ensure that the DepositsComplete call does not run out of gas. - // GasBenchMark_L1BlockInterop_DepositsComplete:test_depositsComplete_benchmark() (gas: 7768) - // GasBenchMark_L1BlockInterop_DepositsComplete_Warm:test_depositsComplete_benchmark() (gas: 5768) - // see `test_depositsComplete_benchmark` at: `/packages/contracts-bedrock/test/BenchmarkTest.t.sol` - DepositsCompleteGas = uint64(21_000 + 15_000) ) var ( L1InfoFuncBedrockBytes4 = crypto.Keccak256([]byte(L1InfoFuncBedrockSignature))[:4] L1InfoFuncEcotoneBytes4 = crypto.Keccak256([]byte(L1InfoFuncEcotoneSignature))[:4] L1InfoFuncIsthmusBytes4 = crypto.Keccak256([]byte(L1InfoFuncIsthmusSignature))[:4] - L1InfoFuncInteropBytes4 = crypto.Keccak256([]byte(L1InfoFuncInteropSignature))[:4] - DepositsCompleteBytes4 = crypto.Keccak256([]byte(DepositsCompleteSignature))[:4] L1InfoDepositerAddress = common.HexToAddress("0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001") L1BlockAddress = predeploys.L1BlockAddr ErrInvalidFormat = errors.New("invalid ecotone l1 block info format") @@ -288,14 +277,6 @@ func (info *L1BlockInfo) marshalBinaryIsthmus() ([]byte, error) { return out, nil } -func (info *L1BlockInfo) marshalBinaryInterop() ([]byte, error) { - out, err := marshalBinaryWithSignature(info, L1InfoFuncInteropBytes4) - if err != nil { - return nil, fmt.Errorf("failed to marshal Interop l1 block info: %w", err) - } - return out, nil -} - func marshalBinaryWithSignature(info *L1BlockInfo, signature []byte) ([]byte, error) { w := bytes.NewBuffer(make([]byte, 0, L1InfoIsthmusLen)) if err := solabi.WriteSignature(w, signature); err != nil { @@ -342,10 +323,6 @@ func marshalBinaryWithSignature(info *L1BlockInfo, signature []byte) ([]byte, er return w.Bytes(), nil } -func (info *L1BlockInfo) unmarshalBinaryInterop(data []byte) error { - return unmarshalBinaryWithSignatureAndData(info, L1InfoFuncInteropBytes4, data) -} - func unmarshalBinaryWithSignatureAndData(info *L1BlockInfo, signature []byte, data []byte) error { if len(data) != L1InfoIsthmusLen { return fmt.Errorf("data is unexpected length: %d", len(data)) @@ -406,16 +383,6 @@ func isEcotoneButNotFirstBlock(rollupCfg *rollup.Config, l2Timestamp uint64) boo return rollupCfg.IsEcotone(l2Timestamp) && !rollupCfg.IsEcotoneActivationBlock(l2Timestamp) } -// isInteropButNotFirstBlock returns whether the specified block is subject to the Interop upgrade, -// but is not the activation block itself. -func isInteropButNotFirstBlock(rollupCfg *rollup.Config, l2Timestamp uint64) bool { - // Since we use the pre-interop L1 tx one last time during the upgrade block, - // we must disallow the deposit-txs from using the CrossL2Inbox during this block. - // If the CrossL2Inbox does not exist yet, then it is safe, - // but we have to ensure that the spec and code puts any Interop upgrade-txs after the user deposits. - return rollupCfg.IsInterop(l2Timestamp) && !rollupCfg.IsInteropActivationBlock(l2Timestamp) -} - // isIsthmusButNotFirstBlock returns whether the specified block is subject to the Isthmus upgrade, // but is not the activation block itself. func isIsthmusButNotFirstBlock(rollupCfg *rollup.Config, l2Timestamp uint64) bool { @@ -426,9 +393,6 @@ func isIsthmusButNotFirstBlock(rollupCfg *rollup.Config, l2Timestamp uint64) boo func L1BlockInfoFromBytes(rollupCfg *rollup.Config, l2BlockTime uint64, data []byte) (*L1BlockInfo, error) { var info L1BlockInfo // Important, this should be ordered from most recent to oldest - if isInteropButNotFirstBlock(rollupCfg, l2BlockTime) { - return &info, info.unmarshalBinaryInterop(data) - } if isIsthmusButNotFirstBlock(rollupCfg, l2BlockTime) { return &info, info.unmarshalBinaryIsthmus(data) } @@ -453,6 +417,19 @@ func L1InfoDeposit(rollupCfg *rollup.Config, sysCfg eth.SystemConfig, seqNumber if isEcotoneButNotFirstBlock(rollupCfg, l2Timestamp) { isIsthmusActivated := isIsthmusButNotFirstBlock(rollupCfg, l2Timestamp) l1BlockInfo.BlobBaseFee = block.BlobBaseFee() + + // Apply Cancun blob base fee calculation if this chain needs the L1 Pectra + // blob schedule fix (mostly Holesky and Sepolia OP-Stack chains). + if t := rollupCfg.PectraBlobScheduleTime; t != nil && block.Time() < *t { + if ebg := block.ExcessBlobGas(); ebg != nil { + l1BlockInfo.BlobBaseFee = eth.CalcBlobFeeCancun(*ebg) + } else { + // If L1 isn't on Cancun yet. It should already have been set + // to nil above in this case anyways. + l1BlockInfo.BlobBaseFee = nil + } + } + if l1BlockInfo.BlobBaseFee == nil { // The L2 spec states to use the MIN_BLOB_GASPRICE from EIP-4844 if not yet active on L1. l1BlockInfo.BlobBaseFee = big.NewInt(1) @@ -470,27 +447,20 @@ func L1InfoDeposit(rollupCfg *rollup.Config, sysCfg eth.SystemConfig, seqNumber l1BlockInfo.OperatorFeeConstant = operatorFee.Constant } - if isInteropButNotFirstBlock(rollupCfg, l2Timestamp) { - out, err := l1BlockInfo.marshalBinaryInterop() + if isIsthmusActivated { + out, err := l1BlockInfo.marshalBinaryIsthmus() if err != nil { - return nil, fmt.Errorf("failed to marshal Interop l1 block info: %w", err) + return nil, fmt.Errorf("failed to marshal Isthmus l1 block info: %w", err) } data = out } else { - if isIsthmusActivated { - out, err := l1BlockInfo.marshalBinaryIsthmus() - if err != nil { - return nil, fmt.Errorf("failed to marshal Isthmus l1 block info: %w", err) - } - data = out - } else { - out, err := l1BlockInfo.marshalBinaryEcotone() - if err != nil { - return nil, fmt.Errorf("failed to marshal Ecotone l1 block info: %w", err) - } - data = out + out, err := l1BlockInfo.marshalBinaryEcotone() + if err != nil { + return nil, fmt.Errorf("failed to marshal Ecotone l1 block info: %w", err) } + data = out } + } else { l1BlockInfo.L1FeeOverhead = sysCfg.Overhead l1BlockInfo.L1FeeScalar = sysCfg.Scalar @@ -538,34 +508,3 @@ func L1InfoDepositBytes(rollupCfg *rollup.Config, sysCfg eth.SystemConfig, seqNu } return opaqueL1Tx, nil } - -func DepositsCompleteDeposit(seqNumber uint64, block eth.BlockInfo) (*types.DepositTx, error) { - source := AfterForceIncludeSource{ - L1BlockHash: block.Hash(), - SeqNumber: seqNumber, - } - out := &types.DepositTx{ - SourceHash: source.SourceHash(), - From: L1InfoDepositerAddress, - To: &L1BlockAddress, - Mint: nil, - Value: big.NewInt(0), - Gas: DepositsCompleteGas, - IsSystemTransaction: false, - Data: DepositsCompleteBytes4, - } - return out, nil -} - -func DepositsCompleteBytes(seqNumber uint64, l1Info eth.BlockInfo) ([]byte, error) { - dep, err := DepositsCompleteDeposit(seqNumber, l1Info) - if err != nil { - return nil, fmt.Errorf("failed to create DepositsComplete tx: %w", err) - } - depositsCompleteTx := types.NewTx(dep) - opaqueDepositsCompleteTx, err := depositsCompleteTx.MarshalBinary() - if err != nil { - return nil, fmt.Errorf("failed to encode DepositsComplete tx: %w", err) - } - return opaqueDepositsCompleteTx, nil -} diff --git a/op-node/rollup/derive/l1_block_info_test.go b/op-node/rollup/derive/l1_block_info_test.go index 03e1b4ec65..c634a9ae4e 100644 --- a/op-node/rollup/derive/l1_block_info_test.go +++ b/op-node/rollup/derive/l1_block_info_test.go @@ -10,7 +10,6 @@ import ( "github.com/stretchr/testify/require" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-service/eth" @@ -205,7 +204,6 @@ func TestParseL1InfoDepositTxData(t *testing.T) { require.False(t, depTx.IsSystemTransaction) require.Equal(t, depTx.Gas, uint64(RegolithSystemTxGas)) require.Equal(t, L1InfoIsthmusLen, len(depTx.Data), "the length is same in interop") - require.Equal(t, L1InfoFuncInteropBytes4, depTx.Data[:4], "upgrade is active, need interop signature") }) t.Run("activation-block interop", func(t *testing.T) { rng := rand.New(rand.NewSource(1234)) @@ -234,39 +232,3 @@ func TestParseL1InfoDepositTxData(t *testing.T) { require.Equal(t, L1InfoIsthmusLen, len(depTx.Data)) }) } - -func TestDepositsCompleteBytes(t *testing.T) { - randomSeqNr := func(rng *rand.Rand) uint64 { - return rng.Uint64() - } - t.Run("valid return bytes", func(t *testing.T) { - rng := rand.New(rand.NewSource(1234)) - info := testutils.MakeBlockInfo(nil)(rng) - depTxByes, err := DepositsCompleteBytes(randomSeqNr(rng), info) - require.NoError(t, err) - var depTx types.Transaction - require.NoError(t, depTx.UnmarshalBinary(depTxByes)) - require.Equal(t, uint8(types.DepositTxType), depTx.Type()) - require.Equal(t, depTx.Data(), DepositsCompleteBytes4) - require.Equal(t, DepositsCompleteLen, len(depTx.Data())) - require.Equal(t, DepositsCompleteGas, depTx.Gas()) - require.False(t, depTx.IsSystemTx()) - require.Equal(t, depTx.Value(), big.NewInt(0)) - signer := types.LatestSignerForChainID(depTx.ChainId()) - sender, err := signer.Sender(&depTx) - require.NoError(t, err) - require.Equal(t, L1InfoDepositerAddress, sender) - }) - t.Run("valid return Transaction", func(t *testing.T) { - rng := rand.New(rand.NewSource(1234)) - info := testutils.MakeBlockInfo(nil)(rng) - depTx, err := DepositsCompleteDeposit(randomSeqNr(rng), info) - require.NoError(t, err) - require.Equal(t, depTx.Data, DepositsCompleteBytes4) - require.Equal(t, DepositsCompleteLen, len(depTx.Data)) - require.Equal(t, DepositsCompleteGas, depTx.Gas) - require.False(t, depTx.IsSystemTransaction) - require.Equal(t, depTx.Value, big.NewInt(0)) - require.Equal(t, L1InfoDepositerAddress, depTx.From) - }) -} diff --git a/op-node/rollup/derive/span_batch_test.go b/op-node/rollup/derive/span_batch_test.go index 4c02c46b2d..96628c92c1 100644 --- a/op-node/rollup/derive/span_batch_test.go +++ b/op-node/rollup/derive/span_batch_test.go @@ -478,13 +478,14 @@ func TestSpanBatchReadTxData(t *testing.T) { {"legacy tx", 32, testutils.RandomLegacyTx, true}, {"access list tx", 32, testutils.RandomAccessListTx, true}, {"dynamic fee tx", 32, testutils.RandomDynamicFeeTx, true}, + {"setcode tx", 32, testutils.RandomSetCodeTx, true}, } for i, testCase := range cases { t.Run(testCase.name, func(t *testing.T) { rng := rand.New(rand.NewSource(int64(0x109550 + i))) chainID := new(big.Int).SetUint64(rng.Uint64()) - signer := types.NewLondonSigner(chainID) + signer := types.NewIsthmusSigner(chainID) if !testCase.protected { signer = types.HomesteadSigner{} } diff --git a/op-node/rollup/derive/span_batch_tx.go b/op-node/rollup/derive/span_batch_tx.go index 5f5bffb02b..876fbd9347 100644 --- a/op-node/rollup/derive/span_batch_tx.go +++ b/op-node/rollup/derive/span_batch_tx.go @@ -8,6 +8,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rlp" + "github.com/holiman/uint256" ) type spanBatchTxData interface { @@ -45,6 +46,17 @@ type spanBatchDynamicFeeTxData struct { func (txData *spanBatchDynamicFeeTxData) txType() byte { return types.DynamicFeeTxType } +type spanBatchSetCodeTxData struct { + Value *uint256.Int + GasTipCap *uint256.Int // a.k.a. maxPriorityFeePerGas + GasFeeCap *uint256.Int // a.k.a. maxFeePerGas + Data []byte + AccessList types.AccessList + AuthorizationList []types.SetCodeAuthorization +} + +func (txData *spanBatchSetCodeTxData) txType() byte { return types.SetCodeTxType } + // Type returns the transaction type. func (tx *spanBatchTx) Type() uint8 { return tx.inner.txType() @@ -93,6 +105,13 @@ func (tx *spanBatchTx) decodeTyped(b []byte) (spanBatchTxData, error) { return nil, fmt.Errorf("failed to decode spanBatchDynamicFeeTxData: %w", err) } return &inner, nil + case types.SetCodeTxType: + var inner spanBatchSetCodeTxData + err := rlp.DecodeBytes(b[1:], &inner) + if err != nil { + return nil, fmt.Errorf("failed to decode spanBatchSetCodeTxData: %w", err) + } + return &inner, nil default: return nil, types.ErrTxTypeNotSupported } @@ -168,6 +187,27 @@ func (tx *spanBatchTx) convertToFullTx(nonce, gas uint64, to *common.Address, ch R: R, S: S, } + case types.SetCodeTxType: + if to == nil { + return nil, fmt.Errorf("to address is required for SetCodeTx") + } + + setCodeTxInner := tx.inner.(*spanBatchSetCodeTxData) + inner = &types.SetCodeTx{ + ChainID: uint256.MustFromBig(chainID), + Nonce: nonce, + GasTipCap: setCodeTxInner.GasTipCap, + GasFeeCap: setCodeTxInner.GasFeeCap, + Gas: gas, + To: *to, + Value: setCodeTxInner.Value, + Data: setCodeTxInner.Data, + AccessList: setCodeTxInner.AccessList, + AuthList: setCodeTxInner.AuthorizationList, + V: uint256.MustFromBig(V), + R: uint256.MustFromBig(R), + S: uint256.MustFromBig(S), + } default: return nil, fmt.Errorf("invalid tx type: %d", tx.Type()) } @@ -199,6 +239,15 @@ func newSpanBatchTx(tx *types.Transaction) (*spanBatchTx, error) { Data: tx.Data(), AccessList: tx.AccessList(), } + case types.SetCodeTxType: + inner = &spanBatchSetCodeTxData{ + GasTipCap: uint256.MustFromBig(tx.GasTipCap()), + GasFeeCap: uint256.MustFromBig(tx.GasFeeCap()), + Value: uint256.MustFromBig(tx.Value()), + Data: tx.Data(), + AccessList: tx.AccessList(), + AuthorizationList: tx.SetCodeAuthorizations(), + } default: return nil, fmt.Errorf("invalid tx type: %d", tx.Type()) } diff --git a/op-node/rollup/derive/span_batch_tx_test.go b/op-node/rollup/derive/span_batch_tx_test.go index 55a9101da5..d215a0f2c6 100644 --- a/op-node/rollup/derive/span_batch_tx_test.go +++ b/op-node/rollup/derive/span_batch_tx_test.go @@ -24,13 +24,14 @@ func TestSpanBatchTxConvert(t *testing.T) { {"legacy tx", 32, testutils.RandomLegacyTx, true}, {"access list tx", 32, testutils.RandomAccessListTx, true}, {"dynamic fee tx", 32, testutils.RandomDynamicFeeTx, true}, + {"setcode tx", 32, testutils.RandomSetCodeTx, true}, } for i, testCase := range cases { t.Run(testCase.name, func(t *testing.T) { rng := rand.New(rand.NewSource(int64(0x1331 + i))) chainID := big.NewInt(rng.Int63n(1000)) - signer := types.NewLondonSigner(chainID) + signer := types.NewIsthmusSigner(chainID) if !testCase.protected { signer = types.HomesteadSigner{} } @@ -63,13 +64,14 @@ func TestSpanBatchTxRoundTrip(t *testing.T) { {"legacy tx", 32, testutils.RandomLegacyTx, true}, {"access list tx", 32, testutils.RandomAccessListTx, true}, {"dynamic fee tx", 32, testutils.RandomDynamicFeeTx, true}, + {"setcode tx", 32, testutils.RandomSetCodeTx, true}, } for i, testCase := range cases { t.Run(testCase.name, func(t *testing.T) { rng := rand.New(rand.NewSource(int64(0x1332 + i))) chainID := big.NewInt(rng.Int63n(1000)) - signer := types.NewLondonSigner(chainID) + signer := types.NewIsthmusSigner(chainID) if !testCase.protected { signer = types.HomesteadSigner{} } @@ -142,3 +144,11 @@ func TestSpanBatchTxDecodeInvalid(t *testing.T) { err = sbtx.UnmarshalBinary(invalidLegacyTxDecoded) require.ErrorContains(t, err, "failed to decode spanBatchLegacyTxData") } + +func TestSpanBatchTxSetCodeInvalidTo(t *testing.T) { + // invalid to for setcode tx + var sbtx spanBatchTx + sbtx.inner = &spanBatchSetCodeTxData{} + _, err := sbtx.convertToFullTx(0, 0, nil, nil, nil, nil, nil) + require.ErrorContains(t, err, "to address is required for SetCodeTx") +} diff --git a/op-node/rollup/derive/span_batch_txs.go b/op-node/rollup/derive/span_batch_txs.go index e4bc73d0f9..7c354430fc 100644 --- a/op-node/rollup/derive/span_batch_txs.go +++ b/op-node/rollup/derive/span_batch_txs.go @@ -271,6 +271,8 @@ func (btx *spanBatchTxs) recoverV(chainID *big.Int) error { v = bit case types.DynamicFeeTxType: v = bit + case types.SetCodeTxType: + v = bit default: return fmt.Errorf("invalid tx type: %d", txType) } @@ -386,6 +388,8 @@ func convertVToYParity(v uint64, txType int) (uint, error) { yParityBit = uint(v) case types.DynamicFeeTxType: yParityBit = uint(v) + case types.SetCodeTxType: + yParityBit = uint(v) default: return 0, fmt.Errorf("invalid tx type: %d", txType) } diff --git a/op-node/rollup/derive/span_batch_txs_test.go b/op-node/rollup/derive/span_batch_txs_test.go index 20f0389941..5cd5ee966d 100644 --- a/op-node/rollup/derive/span_batch_txs_test.go +++ b/op-node/rollup/derive/span_batch_txs_test.go @@ -332,14 +332,15 @@ func TestSpanBatchTxsRecoverV(t *testing.T) { rng := rand.New(rand.NewSource(0x123)) chainID := big.NewInt(rng.Int63n(1000)) - londonSigner := types.NewLondonSigner(chainID) + isthmusSigner := types.NewIsthmusSigner(chainID) totalblockTxCount := 20 + rng.Intn(100) cases := []txTypeTest{ {"unprotected legacy tx", testutils.RandomLegacyTx, types.HomesteadSigner{}}, - {"legacy tx", testutils.RandomLegacyTx, londonSigner}, - {"access list tx", testutils.RandomAccessListTx, londonSigner}, - {"dynamic fee tx", testutils.RandomDynamicFeeTx, londonSigner}, + {"legacy tx", testutils.RandomLegacyTx, isthmusSigner}, + {"access list tx", testutils.RandomAccessListTx, isthmusSigner}, + {"dynamic fee tx", testutils.RandomDynamicFeeTx, isthmusSigner}, + {"setcode tx", testutils.RandomSetCodeTx, isthmusSigner}, } for _, testCase := range cases { @@ -423,13 +424,14 @@ func TestSpanBatchTxsRoundTrip(t *testing.T) { func TestSpanBatchTxsRoundTripFullTxs(t *testing.T) { rng := rand.New(rand.NewSource(0x13377331)) chainID := big.NewInt(rng.Int63n(1000)) - londonSigner := types.NewLondonSigner(chainID) + isthmusSigner := types.NewIsthmusSigner(chainID) cases := []txTypeTest{ {"unprotected legacy tx", testutils.RandomLegacyTx, types.HomesteadSigner{}}, - {"legacy tx", testutils.RandomLegacyTx, londonSigner}, - {"access list tx", testutils.RandomAccessListTx, londonSigner}, - {"dynamic fee tx", testutils.RandomDynamicFeeTx, londonSigner}, + {"legacy tx", testutils.RandomLegacyTx, isthmusSigner}, + {"access list tx", testutils.RandomAccessListTx, isthmusSigner}, + {"dynamic fee tx", testutils.RandomDynamicFeeTx, isthmusSigner}, + {"setcode tx", testutils.RandomSetCodeTx, isthmusSigner}, } for _, testCase := range cases { @@ -473,13 +475,14 @@ func TestSpanBatchTxsRecoverVInvalidTxType(t *testing.T) { func TestSpanBatchTxsFullTxNotEnoughTxTos(t *testing.T) { rng := rand.New(rand.NewSource(0x13572468)) chainID := big.NewInt(rng.Int63n(1000)) - londonSigner := types.NewLondonSigner(chainID) + isthmusSigner := types.NewIsthmusSigner(chainID) cases := []txTypeTest{ {"unprotected legacy tx", testutils.RandomLegacyTx, types.HomesteadSigner{}}, - {"legacy tx", testutils.RandomLegacyTx, londonSigner}, - {"access list tx", testutils.RandomAccessListTx, londonSigner}, - {"dynamic fee tx", testutils.RandomDynamicFeeTx, londonSigner}, + {"legacy tx", testutils.RandomLegacyTx, isthmusSigner}, + {"access list tx", testutils.RandomAccessListTx, isthmusSigner}, + {"dynamic fee tx", testutils.RandomDynamicFeeTx, isthmusSigner}, + {"setcode tx", testutils.RandomSetCodeTx, isthmusSigner}, } for _, testCase := range cases { diff --git a/op-node/rollup/derive/test/random.go b/op-node/rollup/derive/test/random.go index c4e1b011ea..8502855d01 100644 --- a/op-node/rollup/derive/test/random.go +++ b/op-node/rollup/derive/test/random.go @@ -40,7 +40,7 @@ func RandomL2BlockWithChainId(rng *rand.Rand, txCount int, chainId *big.Int) *ty } func RandomL2BlockWithChainIdAndTime(rng *rand.Rand, txCount int, chainId *big.Int, t time.Time) *types.Block { - signer := types.NewLondonSigner(chainId) + signer := types.NewIsthmusSigner(chainId) block, _ := RandomL2Block(rng, 0, t) txs := []*types.Transaction{block.Transactions()[0]} // L1 info deposit TX for i := 0; i < txCount; i++ { diff --git a/op-node/rollup/engine/engine_controller.go b/op-node/rollup/engine/engine_controller.go index 67ea651c99..9a37ff7073 100644 --- a/op-node/rollup/engine/engine_controller.go +++ b/op-node/rollup/engine/engine_controller.go @@ -342,7 +342,7 @@ func (e *EngineController) TryUpdateEngine(ctx context.Context) error { if errors.As(err, &rpcErr) { switch eth.ErrorCode(rpcErr.ErrorCode()) { case eth.InvalidForkchoiceState: - return derive.NewResetError(fmt.Errorf("forkchoice update was inconsistent with engine, need reset to resolve: %w", rpcErr)) + return derive.NewResetError(fmt.Errorf("forkchoice update was inconsistent with engine, need reset to resolve: %w", err)) default: return derive.NewTemporaryError(fmt.Errorf("unexpected error code in forkchoice-updated response: %w", err)) } @@ -423,7 +423,7 @@ func (e *EngineController) InsertUnsafePayload(ctx context.Context, envelope *et if errors.As(err, &rpcErr) { switch eth.ErrorCode(rpcErr.ErrorCode()) { case eth.InvalidForkchoiceState: - return derive.NewResetError(fmt.Errorf("pre-unsafe-block forkchoice update was inconsistent with engine, need reset to resolve: %w", rpcErr)) + return derive.NewResetError(fmt.Errorf("pre-unsafe-block forkchoice update was inconsistent with engine, need reset to resolve: %w", err)) default: return derive.NewTemporaryError(fmt.Errorf("unexpected error code in forkchoice-updated response: %w", err)) } @@ -513,7 +513,7 @@ func (e *EngineController) TryBackupUnsafeReorg(ctx context.Context) (bool, erro switch eth.ErrorCode(rpcErr.ErrorCode()) { case eth.InvalidForkchoiceState: e.SetBackupUnsafeL2Head(eth.L2BlockRef{}, false) - return true, derive.NewResetError(fmt.Errorf("forkchoice update was inconsistent with engine, need reset to resolve: %w", rpcErr)) + return true, derive.NewResetError(fmt.Errorf("forkchoice update was inconsistent with engine, need reset to resolve: %w", err)) default: // Retry when forkChoiceUpdate returns non-input error. // Do not reset backupUnsafeHead because it will be used again. diff --git a/op-node/rollup/engine/engine_update.go b/op-node/rollup/engine/engine_update.go index dbf578d303..7ea984388c 100644 --- a/op-node/rollup/engine/engine_update.go +++ b/op-node/rollup/engine/engine_update.go @@ -89,9 +89,9 @@ func startPayload(ctx context.Context, eng ExecEngine, fc eth.ForkchoiceState, a if errors.As(err, &rpcErr) { switch code := eth.ErrorCode(rpcErr.ErrorCode()); code { case eth.InvalidForkchoiceState: - return eth.PayloadID{}, BlockInsertPrestateErr, fmt.Errorf("pre-block-creation forkchoice update was inconsistent with engine, need reset to resolve: %w", rpcErr) + return eth.PayloadID{}, BlockInsertPrestateErr, fmt.Errorf("pre-block-creation forkchoice update was inconsistent with engine, need reset to resolve: %w", err) case eth.InvalidPayloadAttributes: - return eth.PayloadID{}, BlockInsertPayloadErr, fmt.Errorf("payload attributes are not valid, cannot build block: %w", rpcErr) + return eth.PayloadID{}, BlockInsertPayloadErr, fmt.Errorf("payload attributes are not valid, cannot build block: %w", err) default: if code.IsEngineError() { return eth.PayloadID{}, BlockInsertPrestateErr, fmt.Errorf("unexpected engine error code in forkchoice-updated response: %w", err) diff --git a/op-node/rollup/sequencing/sequencer.go b/op-node/rollup/sequencing/sequencer.go index d66bcbad02..d2bba36bb2 100644 --- a/op-node/rollup/sequencing/sequencer.go +++ b/op-node/rollup/sequencing/sequencer.go @@ -545,11 +545,23 @@ func (d *Sequencer) startBuildingBlock() { d.log.Info("Sequencing Fjord upgrade block") } - // For the Granite activation block we shouldn't include any sequencer transactions. + // For the Granite activation block we can include sequencer transactions. if d.rollupCfg.IsGraniteActivationBlock(uint64(attrs.Timestamp)) { d.log.Info("Sequencing Granite upgrade block") } + // For the Isthmus activation block we shouldn't include any sequencer transactions. + if d.rollupCfg.IsIsthmusActivationBlock(uint64(attrs.Timestamp)) { + attrs.NoTxPool = true + d.log.Info("Sequencing Isthmus upgrade block") + } + + // For the Interop activation block we must not include any sequencer transactions. + if d.rollupCfg.IsInteropActivationBlock(uint64(attrs.Timestamp)) { + attrs.NoTxPool = true + d.log.Info("Sequencing Interop upgrade block") + } + d.log.Debug("prepared attributes for new block", "num", l2Head.Number+1, "time", uint64(attrs.Timestamp), "origin", l1Origin, "origin_time", l1Origin.Time, "noTxPool", attrs.NoTxPool) diff --git a/op-node/rollup/superchain.go b/op-node/rollup/superchain.go index b73c70e96f..ac7de14419 100644 --- a/op-node/rollup/superchain.go +++ b/op-node/rollup/superchain.go @@ -88,6 +88,7 @@ func LoadOPStackRollupConfig(chainID uint64) (*Config, error) { FjordTime: hardforks.FjordTime, GraniteTime: hardforks.GraniteTime, HoloceneTime: hardforks.HoloceneTime, + PectraBlobScheduleTime: hardforks.PectraBlobScheduleTime, IsthmusTime: hardforks.IsthmusTime, BatchInboxAddress: chConfig.BatchInboxAddr, DepositContractAddress: *addrs.OptimismPortalProxy, diff --git a/op-node/rollup/sync/start.go b/op-node/rollup/sync/start.go index 15a811bce5..d779ed22b3 100644 --- a/op-node/rollup/sync/start.go +++ b/op-node/rollup/sync/start.go @@ -134,8 +134,7 @@ func FindL2Heads(ctx context.Context, cfg *rollup.Config, l1 L1Chain, l2 L2Chain // Check if the execution engine corrupted, and forkchoice is ahead of the remaining chain: // in this case we must go back to the prior head, to reprocess the pruned finalized/safe data. if result.Unsafe.Number < result.Finalized.Number || result.Unsafe.Number < result.Safe.Number { - lgr.Error("Unsafe head is behind known finalized/safe blocks, "+ - "execution-engine chain must have been rewound without forkchoice update. Attempting recovery now.", + lgr.Error("Unsafe head is behind known finalized/safe blocks, execution-engine chain must have been rewound without forkchoice update. Attempting recovery now.", "unsafe_head", result.Unsafe, "safe_head", result.Safe, "finalized_head", result.Finalized) return &FindHeadsResult{Unsafe: result.Unsafe, Safe: result.Unsafe, Finalized: result.Unsafe}, nil } diff --git a/op-node/rollup/types.go b/op-node/rollup/types.go index 7b144e09da..b35da90649 100644 --- a/op-node/rollup/types.go +++ b/op-node/rollup/types.go @@ -9,13 +9,12 @@ import ( "math/big" "time" + altda "github.com/ethereum-optimism/optimism/op-alt-da" + "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" - - altda "github.com/ethereum-optimism/optimism/op-alt-da" - "github.com/ethereum-optimism/optimism/op-service/eth" ) var ( @@ -141,14 +140,24 @@ type Config struct { // L1 address that declares the protocol versions, optional (Beta feature) ProtocolVersionsAddress common.Address `json:"protocol_versions_address,omitempty"` - // AltDAConfig. We are in the process of migrating to the AltDAConfig from these legacy top level values - AltDAConfig *AltDAConfig `json:"alt_da,omitempty"` - // ChainOpConfig is the OptimismConfig of the execution layer ChainConfig. // It is used during safe chain consolidation to translate zero SystemConfig EIP1559 // parameters to the protocol values, like the execution layer does. // If missing, it is loaded by the op-node from the embedded superchain config at startup. ChainOpConfig *params.OptimismConfig `json:"chain_op_config,omitempty"` + + // Optional Features + + // AltDAConfig. We are in the process of migrating to the AltDAConfig from these legacy top level values + AltDAConfig *AltDAConfig `json:"alt_da,omitempty"` + + // PectraBlobScheduleTime sets the time until which (but not including) the blob base fee + // calculations for the L1 Block Info use the pre-Prague=Cancun blob parameters. + // This feature is optional and if not active, the L1 Block Info calculation uses the Prague + // blob parameters for the first L1 Prague block, as was intended. + // This feature (de)activates by L1 origin timestamp, to keep a consistent L1 block info per L2 + // epoch. + PectraBlobScheduleTime *uint64 `json:"pectra_blob_schedule_time,omitempty"` } // ValidateL1Config checks L1 config variables for errors. @@ -651,15 +660,9 @@ func (c *Config) Description(l2Chains map[string]string) string { banner += fmt.Sprintf(" L1 block: %s %d\n", c.Genesis.L1.Hash, c.Genesis.L1.Number) // Report the upgrade configuration banner += "Post-Bedrock Network Upgrades (timestamp based):\n" - banner += fmt.Sprintf(" - Regolith: %s\n", fmtForkTimeOrUnset(c.RegolithTime)) - banner += fmt.Sprintf(" - Canyon: %s\n", fmtForkTimeOrUnset(c.CanyonTime)) - banner += fmt.Sprintf(" - Delta: %s\n", fmtForkTimeOrUnset(c.DeltaTime)) - banner += fmt.Sprintf(" - Ecotone: %s\n", fmtForkTimeOrUnset(c.EcotoneTime)) - banner += fmt.Sprintf(" - Fjord: %s\n", fmtForkTimeOrUnset(c.FjordTime)) - banner += fmt.Sprintf(" - Granite: %s\n", fmtForkTimeOrUnset(c.GraniteTime)) - banner += fmt.Sprintf(" - Holocene: %s\n", fmtForkTimeOrUnset(c.HoloceneTime)) - banner += fmt.Sprintf(" - Isthmus: %s\n", fmtForkTimeOrUnset(c.IsthmusTime)) - banner += fmt.Sprintf(" - Interop: %s\n", fmtForkTimeOrUnset(c.InteropTime)) + c.forEachFork(func(name string, _ string, time *uint64) { + banner += fmt.Sprintf(" - %v: %s\n", name, fmtForkTimeOrUnset(time)) + }) // Report the protocol version banner += fmt.Sprintf("Node supports up to OP-Stack Protocol Version: %s\n", OPStackSupport) if c.AltDAConfig != nil { @@ -685,20 +688,44 @@ func (c *Config) LogDescription(log log.Logger, l2Chains map[string]string) { networkL1 = "unknown L1" } - log.Info("Rollup Config", "l2_chain_id", c.L2ChainID, "l2_network", networkL2, "l1_chain_id", c.L1ChainID, - "l1_network", networkL1, "l2_start_time", c.Genesis.L2Time, "l2_block_hash", c.Genesis.L2.Hash.String(), - "l2_block_number", c.Genesis.L2.Number, "l1_block_hash", c.Genesis.L1.Hash.String(), - "l1_block_number", c.Genesis.L1.Number, "regolith_time", fmtForkTimeOrUnset(c.RegolithTime), - "canyon_time", fmtForkTimeOrUnset(c.CanyonTime), - "delta_time", fmtForkTimeOrUnset(c.DeltaTime), - "ecotone_time", fmtForkTimeOrUnset(c.EcotoneTime), - "fjord_time", fmtForkTimeOrUnset(c.FjordTime), - "granite_time", fmtForkTimeOrUnset(c.GraniteTime), - "holocene_time", fmtForkTimeOrUnset(c.HoloceneTime), - "isthmus_time", fmtForkTimeOrUnset(c.IsthmusTime), - "interop_time", fmtForkTimeOrUnset(c.InteropTime), - "alt_da", c.AltDAConfig != nil, - ) + ctx := []any{ + "l2_chain_id", c.L2ChainID, + "l2_network", networkL2, + "l1_chain_id", c.L1ChainID, + "l1_network", networkL1, + "l2_start_time", c.Genesis.L2Time, + "l2_block_hash", c.Genesis.L2.Hash.String(), + "l2_block_number", c.Genesis.L2.Number, + "l1_block_hash", c.Genesis.L1.Hash.String(), + "l1_block_number", c.Genesis.L1.Number, + } + c.forEachFork(func(_ string, logName string, time *uint64) { + ctx = append(ctx, logName, fmtForkTimeOrUnset(time)) + }) + if c.AltDAConfig != nil { + ctx = append(ctx, "alt_da", *c.AltDAConfig) + } + if c.PectraBlobScheduleTime != nil { + // only print in config if set at all + ctx = append(ctx, "pectra_blob_schedule_time", fmtForkTimeOrUnset(c.PectraBlobScheduleTime)) + } + log.Info("Rollup Config", ctx...) +} + +func (c *Config) forEachFork(callback func(name string, logName string, time *uint64)) { + callback("Regolith", "regolith_time", c.RegolithTime) + callback("Canyon", "canyon_time", c.CanyonTime) + callback("Delta", "delta_time", c.DeltaTime) + callback("Ecotone", "ecotone_time", c.EcotoneTime) + callback("Fjord", "fjord_time", c.FjordTime) + callback("Granite", "granite_time", c.GraniteTime) + callback("Holocene", "holocene_time", c.HoloceneTime) + if c.PectraBlobScheduleTime != nil { + // only report if config is set + callback("Pectra Blob Schedule", "pectra_blob_schedule_time", c.PectraBlobScheduleTime) + } + callback("Isthmus", "isthmus_time", c.IsthmusTime) + callback("Interop", "interop_time", c.InteropTime) } func (c *Config) ParseRollupConfig(in io.Reader) error { diff --git a/op-node/service.go b/op-node/service.go index 9ed2e83927..81f269ed08 100644 --- a/op-node/service.go +++ b/op-node/service.go @@ -266,6 +266,10 @@ func applyOverrides(ctx *cli.Context, rollupConfig *rollup.Config) { holocene := ctx.Uint64(opflags.HoloceneOverrideFlagName) rollupConfig.HoloceneTime = &holocene } + if ctx.IsSet(opflags.PectraBlobScheduleOverrideFlagName) { + pectrablobschedule := ctx.Uint64(opflags.PectraBlobScheduleOverrideFlagName) + rollupConfig.PectraBlobScheduleTime = &pectrablobschedule + } } func NewSyncConfig(ctx *cli.Context, log log.Logger) (*sync.Config, error) { diff --git a/op-program/Makefile b/op-program/Makefile index c2462ea33b..b8a005b5cd 100644 --- a/op-program/Makefile +++ b/op-program/Makefile @@ -69,12 +69,6 @@ clean: test: go test -v ./... -verify-sepolia: op-program-host op-program-client - env GO111MODULE=on go run ./verify/sepolia/cmd/sepolia.go --l1 $$SEPOLIA_L1URL --l1.beacon $$SEPOLIA_BEACON_URL --l2 $$SEPOLIA_L2URL --datadir /tmp/test-sepolia - -verify-devnet: op-program-host op-program-client - env GO111MODULE=on go run ./verify/devnet/cmd/devnet.go --l1 http://localhost:8545 --l1.beacon http://localhost:5052 --l2 http://localhost:9545 --datadir /tmp/test-devnet - capture-mainnet-genesis: op-program-host op-program-client rm -rf "$(COMPAT_DIR)/mainnet-genesis" "$(COMPAT_DIR)/mainnet-genesis.tar.bz" env GO111MODULE=on go run ./verify/mainnet/cmd/mainnet.go --l1 $$MAINNET_L1URL --l1.beacon $$MAINNET_BEACON_URL --l2 $$MAINNET_L2URL --datadir "$(COMPAT_DIR)/mainnet-genesis" --l1.head "0x4903424f6cc2cfba7c2bf8c8f48ca46721c963fa64b411cfee3697b781e3e5f1" --l2.start "105235063" --l2.end "105235064" @@ -122,8 +116,6 @@ verify-compat: verify-sepolia-delta verify-sepolia-ecotone verify-mainnet-genesi clean \ test \ capture-goerli-verify \ - verify-sepolia \ - verify-devnet \ capture-mainnet-genesis \ capture-sepolia-delta \ capture-sepolia-ecotone \ diff --git a/op-program/chainconfig/chaincfg.go b/op-program/chainconfig/chaincfg.go index 7c638f669c..46cb7818f7 100644 --- a/op-program/chainconfig/chaincfg.go +++ b/op-program/chainconfig/chaincfg.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "os" + "strings" "github.com/ethereum-optimism/optimism/op-service/superutil" "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/backend/depset" @@ -25,6 +26,29 @@ func OPSepoliaChainConfig() *params.ChainConfig { //go:embed configs/*json var customChainConfigFS embed.FS +func CustomChainIDs() ([]eth.ChainID, error) { + return customChainIDs(customChainConfigFS) +} + +func customChainIDs(customChainFS embed.FS) ([]eth.ChainID, error) { + entries, err := customChainFS.ReadDir("configs") + if err != nil { + return nil, fmt.Errorf("failed to list custom configs: %w", err) + } + var chainIDs []eth.ChainID + for _, entry := range entries { + if strings.HasSuffix(entry.Name(), "-genesis-l2.json") { + chainID, err := eth.ParseDecimalChainID(strings.TrimSuffix(entry.Name(), "-genesis-l2.json")) + if err != nil { + return nil, fmt.Errorf("incorrectly named genesis-l2 config (%s): %w", entry.Name(), err) + } + chainIDs = append(chainIDs, chainID) + } + } + + return chainIDs, nil +} + func RollupConfigByChainID(chainID eth.ChainID) (*rollup.Config, error) { config, err := rollup.LoadOPStackRollupConfig(eth.EvilChainIDToUInt64(chainID)) if err == nil { @@ -37,7 +61,7 @@ func rollupConfigByChainID(chainID eth.ChainID, customChainFS embed.FS) (*rollup // Load custom rollup configs from embed FS file, err := customChainFS.Open(fmt.Sprintf("configs/%v-rollup.json", chainID)) if errors.Is(err, os.ErrNotExist) { - return nil, fmt.Errorf("no rollup config available for chain ID: %d", chainID) + return nil, fmt.Errorf("no rollup config available for chain ID: %v", chainID) } else if err != nil { return nil, fmt.Errorf("failed to get rollup config for chain ID %v: %w", chainID, err) } @@ -59,7 +83,7 @@ func chainConfigByChainID(chainID eth.ChainID, customChainFS embed.FS) (*params. // Load from custom chain configs from embed FS data, err := customChainFS.ReadFile(fmt.Sprintf("configs/%v-genesis-l2.json", chainID)) if errors.Is(err, os.ErrNotExist) { - return nil, fmt.Errorf("no chain config available for chain ID: %d", chainID) + return nil, fmt.Errorf("no chain config available for chain ID: %v", chainID) } else if err != nil { return nil, fmt.Errorf("failed to get chain config for chain ID %v: %w", chainID, err) } @@ -84,7 +108,7 @@ func mustLoadChainConfig(name string) *params.ChainConfig { } func DependencySetByChainID(chainID eth.ChainID) (depset.DependencySet, error) { - // TODO(#13887): Load from the superchain registry when available. + // TODO(#14771): Load from the superchain registry when available. return dependencySetByChainID(chainID, customChainConfigFS) } @@ -92,7 +116,7 @@ func dependencySetByChainID(chainID eth.ChainID, customChainFS embed.FS) (depset // Load custom dependency set configs from embed FS data, err := customChainFS.ReadFile("configs/depsets.json") if errors.Is(err, os.ErrNotExist) { - return nil, fmt.Errorf("no dependency set available for chain ID: %d", chainID) + return nil, fmt.Errorf("no dependency set available for chain ID: %v", chainID) } else if err != nil { return nil, fmt.Errorf("failed to get dependency set for chain ID %v: %w", chainID, err) } @@ -108,5 +132,5 @@ func dependencySetByChainID(chainID eth.ChainID, customChainFS embed.FS) (depset return depSet, nil } } - return nil, fmt.Errorf("no dependency set config includes chain ID: %d", chainID) + return nil, fmt.Errorf("no dependency set config includes chain ID: %v", chainID) } diff --git a/op-program/chainconfig/chaincfg_test.go b/op-program/chainconfig/chaincfg_test.go index a493e1b464..be097afa3b 100644 --- a/op-program/chainconfig/chaincfg_test.go +++ b/op-program/chainconfig/chaincfg_test.go @@ -43,3 +43,9 @@ func TestGetCustomDependencySetConfig(t *testing.T) { _, err = dependencySetByChainID(eth.ChainIDFromUInt64(900), test.TestCustomChainConfigFS) require.Error(t, err) } + +func TestListCustomChainIDs(t *testing.T) { + actual, err := customChainIDs(test.TestCustomChainConfigFS) + require.NoError(t, err) + require.Equal(t, []eth.ChainID{eth.ChainIDFromUInt64(901)}, actual) +} diff --git a/op-program/client/interop/consolidate.go b/op-program/client/interop/consolidate.go index 459012be07..d45628a596 100644 --- a/op-program/client/interop/consolidate.go +++ b/op-program/client/interop/consolidate.go @@ -275,9 +275,15 @@ func (d *consolidateCheckDeps) Contains(chain eth.ChainID, query supervisortypes for _, receipt := range receipts { for i, log := range receipt.Logs { if current+uint32(i) == query.LogIdx { - msgHash := logToMessageHash(log) - if msgHash != query.LogHash { - return supervisortypes.BlockSeal{}, fmt.Errorf("payload hash mismatch: %s != %s: %w", msgHash, query.LogHash, supervisortypes.ErrConflict) + checksum := supervisortypes.ChecksumArgs{ + BlockNumber: query.BlockNum, + LogIndex: query.LogIdx, + Timestamp: query.Timestamp, + ChainID: chain, + LogHash: logToLogHash(log), + }.Checksum() + if checksum != query.Checksum { + return supervisortypes.BlockSeal{}, fmt.Errorf("checksum mismatch: %s != %s: %w", checksum, query.Checksum, supervisortypes.ErrConflict) } else if block.Time() != query.Timestamp { return supervisortypes.BlockSeal{}, fmt.Errorf("block timestamp mismatch: %d != %d: %w", block.Time(), query.Timestamp, supervisortypes.ErrConflict) } else { @@ -294,7 +300,7 @@ func (d *consolidateCheckDeps) Contains(chain eth.ChainID, query supervisortypes return supervisortypes.BlockSeal{}, fmt.Errorf("log not found") } -func logToMessageHash(l *ethtypes.Log) common.Hash { +func logToLogHash(l *ethtypes.Log) common.Hash { payloadHash := crypto.Keccak256Hash(supervisortypes.LogToMessagePayload(l)) return supervisortypes.PayloadHashToLogHash(payloadHash, l.Address) } @@ -309,8 +315,8 @@ func (d *consolidateCheckDeps) IsLocalUnsafe(chainID eth.ChainID, block eth.Bloc return nil } -func (d *consolidateCheckDeps) ParentBlock(chainID eth.ChainID, parentOf eth.BlockID) (parent eth.BlockID, err error) { - block, err := d.CanonBlockByNumber(d.oracle, parentOf.Number-1, chainID) +func (d *consolidateCheckDeps) FindBlockID(chainID eth.ChainID, num uint64) (blockID eth.BlockID, err error) { + block, err := d.CanonBlockByNumber(d.oracle, num, chainID) if err != nil { return eth.BlockID{}, err } diff --git a/op-program/client/interop/interop.go b/op-program/client/interop/interop.go index ac97779380..5174ab04fd 100644 --- a/op-program/client/interop/interop.go +++ b/op-program/client/interop/interop.go @@ -82,18 +82,21 @@ func stateTransition(logger log.Logger, bootInfo *boot.BootInfoInterop, l1Preima if err != nil { return common.Hash{}, err } + logger.Info("Loaded agreed state", "step", transitionState.Step) // Strictly, the state transition ends when superRoot.Timestamp == bootInfo.GameTimestamp. // Since the valid state transition ends at the game timestamp, there isn't any valid hash resulting from // an agreed prestate and so the program panics to make it clear that the setup is invalid. // The honest actor will never agree to a prestate where superRoot.Timestamp > bootInfo.GameTimestamp and so will // be unaffected by this if superRoot.Timestamp == bootInfo.GameTimestamp { + logger.Info("Already reached game timestamp. No derivation required.") return bootInfo.AgreedPrestate, nil } else if superRoot.Timestamp > bootInfo.GameTimestamp { panic(fmt.Errorf("agreed prestate timestamp %v is after the game timestamp %v", superRoot.Timestamp, bootInfo.GameTimestamp)) } expectedPendingProgress := transitionState.PendingProgress if transitionState.Step < uint64(len(superRoot.Chains)) { + logger.Info("Deriving optimistic block") block, err := deriveOptimisticBlock(logger, bootInfo, l1PreimageOracle, l2PreimageOracle, superRoot, transitionState, tasks) if errors.Is(err, ErrL1HeadReached) { return InvalidTransitionHash, nil @@ -102,6 +105,7 @@ func stateTransition(logger log.Logger, bootInfo *boot.BootInfoInterop, l1Preima } expectedPendingProgress = append(expectedPendingProgress, block) } else if transitionState.Step == ConsolidateStep { + logger.Info("Running consolidate step") // sanity check if len(transitionState.PendingProgress) >= ConsolidateStep { return common.Hash{}, fmt.Errorf("pending progress length does not match the expected step") diff --git a/op-program/client/l2/engine_backend_test.go b/op-program/client/l2/engine_backend_test.go index b204b3d12d..30b1601570 100644 --- a/op-program/client/l2/engine_backend_test.go +++ b/op-program/client/l2/engine_backend_test.go @@ -343,7 +343,7 @@ func setupOracle(t *testing.T, blockCount int, headBlockNumber int, enableEcoton } l1Genesis, err := genesis.NewL1Genesis(deployConfig) require.NoError(t, err) - l2Genesis, err := genesis.NewL2Genesis(deployConfig, l1Genesis.ToBlock().Header()) + l2Genesis, err := genesis.NewL2Genesis(deployConfig, eth.BlockRefFromHeader(l1Genesis.ToBlock().Header())) require.NoError(t, err) l2Genesis.Alloc[fundedAddress] = types.Account{ diff --git a/op-program/client/l2/engineapi/block_processor.go b/op-program/client/l2/engineapi/block_processor.go index 260cf9885c..8991ac78ce 100644 --- a/op-program/client/l2/engineapi/block_processor.go +++ b/op-program/client/l2/engineapi/block_processor.go @@ -95,7 +95,7 @@ func NewBlockProcessorFromHeader(provider BlockDataProvider, h *types.Header) (* vmenv := vm.NewEVM(context, statedb, provider.Config(), vm.Config{PrecompileOverrides: precompileOverrides}) return vmenv } - vmenv := mkEVM() + var vmenv *vm.EVM if h.ParentBeaconRoot != nil { if provider.Config().IsCancun(header.Number, header.Time) { // Blob tx not supported on optimism chains but fields must be set when Cancun is active. @@ -103,7 +103,11 @@ func NewBlockProcessorFromHeader(provider BlockDataProvider, h *types.Header) (* header.BlobGasUsed = &zero header.ExcessBlobGas = &zero } + // core.NewEVMBlockContext need to be called after the blob gas fields are set + vmenv = mkEVM() core.ProcessBeaconBlockRoot(*header.ParentBeaconRoot, vmenv) + } else { + vmenv = mkEVM() } if provider.Config().IsPrague(header.Number, header.Time) { core.ProcessParentBlockHash(header.ParentHash, vmenv) diff --git a/op-program/client/l2/engineapi/l2_engine_api.go b/op-program/client/l2/engineapi/l2_engine_api.go index b326bd94e5..1fbd222e6d 100644 --- a/op-program/client/l2/engineapi/l2_engine_api.go +++ b/op-program/client/l2/engineapi/l2_engine_api.go @@ -123,6 +123,11 @@ func (ea *L2EngineAPI) ForcedEmpty() bool { return ea.l2ForceEmpty } +// SetForceEmpty changes the way the remainder of the block is being built +func (ea *L2EngineAPI) SetForceEmpty(v bool) { + ea.l2ForceEmpty = v +} + func (ea *L2EngineAPI) PendingIndices(from common.Address) uint64 { return ea.pendingIndices[from] } @@ -385,6 +390,10 @@ func (ea *L2EngineAPI) NewPayloadV4(ctx context.Context, params *eth.ExecutionPa return ð.PayloadStatusV1{Status: eth.ExecutionInvalid}, engine.UnsupportedFork.With(errors.New("newPayloadV4 called pre-isthmus")) } + if params.WithdrawalsRoot == nil { + return ð.PayloadStatusV1{Status: eth.ExecutionInvalid}, engine.InvalidParams.With(errors.New("nil withdrawalsRoot post-isthmus")) + } + requests := convertRequests(executionRequests) return ea.newPayload(ctx, params, versionedHashes, beaconRoot, requests) } diff --git a/op-program/client/l2/engineapi/l2_engine_api_test.go b/op-program/client/l2/engineapi/l2_engine_api_test.go index 998b4c942d..a358830045 100644 --- a/op-program/client/l2/engineapi/l2_engine_api_test.go +++ b/op-program/client/l2/engineapi/l2_engine_api_test.go @@ -23,12 +23,14 @@ import ( func TestNewPayloadV4(t *testing.T) { cases := []struct { - isthmusTime uint64 - blockTime uint64 - expectedError string + isthmusTime uint64 + blockTime uint64 + expectedError string + nilWithdrawalRoot bool }{ - {6, 5, engine.UnsupportedFork.Error()}, // before isthmus - {6, 8, ""}, // after isthmus + {6, 5, engine.UnsupportedFork.Error(), false}, // before isthmus + {6, 8, "", false}, // after isthmus + {6, 8, "Invalid parameters", true}, // after isthmus, nil withdrawal root } logger, _ := testlog.CaptureLogger(t, log.LvlInfo) @@ -76,6 +78,10 @@ func TestNewPayloadV4(t *testing.T) { require.NoError(t, err) require.NotNil(t, envelope) + if c.nilWithdrawalRoot { + envelope.ExecutionPayload.WithdrawalsRoot = nil + } + newPayloadResult, err := engineAPI.NewPayloadV4(context.Background(), envelope.ExecutionPayload, []common.Hash{}, envelope.ParentBeaconBlockRoot, []hexutil.Bytes{}) if c.expectedError != "" { require.ErrorContains(t, err, c.expectedError) diff --git a/op-program/host/cmd/main.go b/op-program/host/cmd/main.go index 75dc138398..aa08389237 100644 --- a/op-program/host/cmd/main.go +++ b/op-program/host/cmd/main.go @@ -6,6 +6,7 @@ import ( "github.com/ethereum-optimism/optimism/op-program/host" "github.com/ethereum-optimism/optimism/op-program/host/config" "github.com/ethereum-optimism/optimism/op-program/host/flags" + "github.com/ethereum-optimism/optimism/op-program/host/subcmds" "github.com/ethereum-optimism/optimism/op-program/host/version" opservice "github.com/ethereum-optimism/optimism/op-service" oplog "github.com/ethereum-optimism/optimism/op-service/log" @@ -44,6 +45,9 @@ func run(args []string, action ConfigAction) error { app.Name = "op-program" app.Usage = "Optimism Fault Proof Program" app.Description = "The Optimism Fault Proof Program fault proof program that runs through the rollup state-transition to verify an L2 output from L1 inputs." + app.Commands = []*cli.Command{ + subcmds.ConfigsCommand, + } app.Action = func(ctx *cli.Context) error { logger, err := setupLogging(ctx) if err != nil { diff --git a/op-program/host/config/config.go b/op-program/host/config/config.go index a1a768cc41..0ab01a9aa0 100644 --- a/op-program/host/config/config.go +++ b/op-program/host/config/config.go @@ -336,7 +336,7 @@ func NewConfigFromCLI(log log.Logger, ctx *cli.Context) (*Config, error) { if interopEnabled { depsetConfigPath := ctx.Path(flags.DepsetConfig.Name) if depsetConfigPath == "" { - // TODO(#13887): Load static config dependency from embed if no path is provided + // TODO(#14771): Load static config dependency from embed if no path is provided return nil, fmt.Errorf("empty depset config path") } dependencySet, err = loadDepsetConfig(depsetConfigPath) diff --git a/op-program/host/flags/flags.go b/op-program/host/flags/flags.go index 9adf43cf20..baeb3d2f84 100644 --- a/op-program/host/flags/flags.go +++ b/op-program/host/flags/flags.go @@ -67,7 +67,7 @@ var ( } L2Head = &cli.StringFlag{ Name: "l2.head", - Usage: "Hash of the L2 block at l2.outputroot", + Usage: "Hash of the L2 block at l2.outputroot. Used for non-interop games.", EnvVars: prefixEnvVars("L2_HEAD"), } L2OutputRoot = &cli.StringFlag{ @@ -83,13 +83,14 @@ var ( } L2Claim = &cli.StringFlag{ Name: "l2.claim", - Usage: "Claimed L2 output root to validate", + Usage: "Claimed proposal root to validate", EnvVars: prefixEnvVars("L2_CLAIM"), } L2BlockNumber = &cli.Uint64Flag{ Name: "l2.blocknumber", - Usage: "Number of the L2 block that the claim is from", - EnvVars: prefixEnvVars("L2_BLOCK_NUM"), + Aliases: []string{"l2.sequencenumber"}, + Usage: "L2 block number or timestamp that the claim is from", + EnvVars: append(prefixEnvVars("L2_BLOCK_NUM"), prefixEnvVars("L2_SEQUENCE_NUMBER")...), } L2GenesisPath = &cli.StringSliceFlag{ Name: "l2.genesis", diff --git a/op-program/host/subcmds/configs_cmd.go b/op-program/host/subcmds/configs_cmd.go new file mode 100644 index 0000000000..5619dda2d8 --- /dev/null +++ b/op-program/host/subcmds/configs_cmd.go @@ -0,0 +1,98 @@ +package subcmds + +import ( + "fmt" + + "github.com/ethereum-optimism/optimism/op-node/chaincfg" + "github.com/ethereum-optimism/optimism/op-program/chainconfig" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum/go-ethereum/superchain" + "github.com/urfave/cli/v2" +) + +var ( + ConfigsChainIDFlag = &cli.StringFlag{ + Name: "chain-id", + Usage: "Chain ID to report chain configuration for", + } + ConfigsNetworkFlag = &cli.StringFlag{ + Name: "network", + Usage: "Network to report chain configuration for", + } +) + +var ConfigsCommand = &cli.Command{ + Name: "configs", + Usage: "List the supported chain configurations", + Description: "List the supported chain configurations.", + Action: ListConfigs, + Flags: []cli.Flag{ + ConfigsChainIDFlag, + ConfigsNetworkFlag, + }, +} + +func ListConfigs(ctx *cli.Context) error { + if ctx.IsSet(ConfigsChainIDFlag.Name) { + chainID, err := eth.ParseDecimalChainID(ctx.String(ConfigsChainIDFlag.Name)) + if err != nil { + return fmt.Errorf("invalid chain ID: %w", err) + } + if err := listChain(chainID); err != nil { + return err + } + } + if ctx.IsSet(ConfigsNetworkFlag.Name) { + if err := listNamedChain(ctx.String(ConfigsNetworkFlag.Name)); err != nil { + return err + } + } + if !ctx.IsSet(ConfigsChainIDFlag.Name) && !ctx.IsSet(ConfigsNetworkFlag.Name) { + return listAllChains() + } + return nil +} + +func listAllChains() error { + chainNames := superchain.ChainNames() + for _, name := range chainNames { + if err := listNamedChain(name); err != nil { + return err + } + } + customChainIDs, err := chainconfig.CustomChainIDs() + if err != nil { + return err + } + for _, chainID := range customChainIDs { + if err := listChain(chainID); err != nil { + return err + } + } + return nil +} + +func listNamedChain(name string) error { + ch := chaincfg.ChainByName(name) + chainID := eth.ChainIDFromUInt64(ch.ChainID) + err := listChain(chainID) + if err != nil { + return err + } + return nil +} + +func listChain(chainID eth.ChainID) error { + cfg, err := chainconfig.RollupConfigByChainID(chainID) + if err != nil { + return err + } + // Double check the L2 genesis is really available + _, err = chainconfig.ChainConfigByChainID(chainID) + if err != nil { + return err + } + description := cfg.Description(chaincfg.L2ChainIDToNetworkDisplayName) + fmt.Println(description) + return nil +} diff --git a/op-program/prestates/releases.go b/op-program/prestates/releases.go deleted file mode 100644 index 71f159f0a1..0000000000 --- a/op-program/prestates/releases.go +++ /dev/null @@ -1,35 +0,0 @@ -package prestates - -// This package is imported by the superchain-registry as part of chain validation -// tests. Please do not delete these files unless the downstream dependency is removed. - -import ( - _ "embed" - "encoding/json" - "fmt" -) - -//go:embed releases.json -var releasesJSON []byte - -type Release struct { - Version string `json:"version"` - Hash string `json:"hash"` - GovernanceApproved bool `json:"governanceApproved"` - Type ReleaseType `json:"type"` -} - -type ReleaseType string - -const Cannon64Type ReleaseType = "cannon64" - -// GetReleases reads the contents of the releases.json file -func GetReleases() ([]Release, error) { - var releases []Release - err := json.Unmarshal(releasesJSON, &releases) - if err != nil { - return nil, fmt.Errorf("failed to parse JSON: %w", err) - } - - return releases, nil -} diff --git a/op-program/prestates/releases.json b/op-program/prestates/releases.json deleted file mode 100644 index f66063232b..0000000000 --- a/op-program/prestates/releases.json +++ /dev/null @@ -1,170 +0,0 @@ -[ - { - "version": "1.5.0-rc.3", - "hash": "0x039970872142f48b189d18dcbc03a3737338d098b0101713dc2d6710f9deb5ef", - "type": "cannon64" - }, - { - "version": "1.5.0-rc.3", - "hash": "0x039facea52b20c605c05efb0a33560a92de7074218998f75bcdf61e8989cb5d9" - }, - { - "version": "1.5.0-rc.2", - "hash": "0x03a7d967025dc434a9ca65154acdb88a7b658147b9b049f0b2f5ecfb9179b0fe", - "type": "cannon64" - }, - { - "version": "1.5.0-rc.2", - "hash": "0x035ac388b5cb22acf52a2063cfde108d09b1888655d21f02f595f9c3ea6cbdcd" - }, - { - "version": "1.5.0-rc.1", - "hash": "0x03f83792f653160f3274b0888e998077a27e1f74cb35bcb20d86021e769340aa", - "type": "cannon64" - }, - { - "version": "1.5.0-rc.1", - "hash": "0x03dfa3b3ac66e8fae9f338824237ebacff616df928cf7dada0e14be2531bc1f4" - }, - { - "version": "1.4.1-rc.3", - "hash": "0x03d7f817d7bb1321533aeeee5e0f2031cc69d167c4a17bf2816b4cc8b1be4077", - "type": "cannon64" - }, - { - "version": "1.4.1-rc.3", - "hash": "0x03ea123151750b03569369130a73c390b0b5e10c722ab42d762d116318325bb7" - }, - { - "version": "1.4.1-rc.2", - "hash": "0x0386cde2f2b1bde1189ac9c9b7d66774e6260eca778223def326bfe680c14ab9", - "type": "cannon64" - }, - { - "version": "1.4.1-rc.2", - "hash": "0x03045fd433fb5391c40751939d7cb5e9dfe83cf156f9395566a311e7fe9d3aa2" - }, - { - "version": "1.4.1-rc.1", - "hash": "0x0386cde2f2b1bde1189ac9c9b7d66774e6260eca778223def326bfe680c14ab9", - "type": "cannon64" - }, - { - "version": "1.4.1-rc.1", - "hash": "0x03045fd433fb5391c40751939d7cb5e9dfe83cf156f9395566a311e7fe9d3aa2" - }, - { - "version": "1.4.0", - "hash": "0x03b7eaa4e3cbce90381921a4b48008f4769871d64f93d113fcadca08ecee503b", - "type": "cannon64" - }, - { - "version": "1.4.0", - "hash": "0x03f89406817db1ed7fd8b31e13300444652cdb0b9c509a674de43483b2f83568", - "governanceApproved": true - }, - { - "version": "1.4.0-unichain-mainnet", - "hash": "0x03d5baff435a6828cfa13679e7ef34ac73d4d674550426c7d9a8005deb6a6db3", - "type": "cannon64" - }, - { - "version": "1.4.0-unichain-mainnet", - "hash": "0x0336751a224445089ba5456c8028376a0faf2bafa81d35f43fab8730258cdf37", - "governanceApproved": true - }, - { - "version": "1.4.0-rc.3", - "hash": "0x03b7eaa4e3cbce90381921a4b48008f4769871d64f93d113fcadca08ecee503b", - "type": "cannon64" - }, - { - "version": "1.4.0-rc.3", - "hash": "0x03f89406817db1ed7fd8b31e13300444652cdb0b9c509a674de43483b2f83568" - }, - { - "version": "1.4.0-rc.2", - "hash": "0x0348ce2059f718af75729c2c56860551b46b665956a641b3cb2cd51e50b7b725", - "type": "cannon64" - }, - { - "version": "1.4.0-rc.2", - "hash": "0x0364e4e72922e7d649338f558f8a14b50ca31922a1484e73ea03987fb1516095" - }, - { - "version": "1.4.0-rc.1", - "hash": "0x032e5d6119ee983cb87deae3eef16ea6086f2347433c99f1820d60f36a24a6e6", - "type": "cannon64" - }, - { - "version": "1.4.0-rc.1", - "hash": "0x03925193e3e89f87835bbdf3a813f60b2aa818a36bbe71cd5d8fd7e79f5e8afe" - }, - { - "version": "1.3.1-ink", - "hash": "0x03c50b9fd04bdadc228205f340767bbf2d01a030aec39903120d3559d94bb8cc" - }, - { - "version": "1.3.1", - "hash": "0x038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c", - "governanceApproved": true - }, - { - "version": "1.3.1-rc.2", - "hash": "0x038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c" - }, - { - "version": "1.3.1-rc.1", - "hash": "0x03e806a2859a875267a563462a06d4d1d1b455a9efee959a46e21e54b6caf69a" - }, - { - "version": "1.3.0-rc.3", - "hash": "0x030de10d9da911a2b180ecfae2aeaba8758961fc28262ce989458c6f9a547922" - }, - { - "version": "1.3.0-rc.2", - "hash": "0x0385c3f8ee78491001d92b90b07d0cf387b7b52ab9b83b4d87c994e92cf823ba" - }, - { - "version": "1.3.0-rc.1", - "hash": "0x0367c4aa897bffbded0b523f277ca892298dc3c691baf37bc2099b86024f9673" - }, - { - "version": "1.2.0", - "hash": "0x03617abec0b255dc7fc7a0513a2c2220140a1dcd7a1c8eca567659bd67e05cea", - "governanceApproved": true - }, - { - "version": "1.1.0", - "hash": "0x03e69d3de5155f4a80da99dd534561cbddd4f9dd56c9ecc704d6886625711d2b" - }, - { - "version": "1.0.1", - "hash": "0x0398bdd93e2e9313befdf82beb709da6a4daf35ce1abb42d8a998ec9bc1c572e" - }, - { - "version": "1.0.0", - "hash": "0x037ef3c1a487960b0e633d3e513df020c43432769f41a634d18a9595cbf53c55", - "governanceApproved": true - }, - { - "version": "0.3.1", - "hash": "0x037ef3c1a487960b0e633d3e513df020c43432769f41a634d18a9595cbf53c55" - }, - { - "version": "0.3.0", - "hash": "0x034c8cc69f22c35ae386a97136715dd48aaf97fd190942a111bfa680c2f2f421" - }, - { - "version": "0.2.0", - "hash": "0x031e3b504740d0b1264e8cf72b6dde0d497184cfb3f98e451c6be8b33bd3f808" - }, - { - "version": "0.1.0", - "hash": "0x038942ec840131a63c49fa514a3f0577ae401fd5584d56ad50cdf5a8b41d4538" - }, - { - "version": "0.0.1", - "hash": "0x03babef4b4c6d866d56e6356d961839fd9475931d11e0ea507420a87b0cadbdd" - } -] diff --git a/op-program/prestates/releases_test.go b/op-program/prestates/releases_test.go deleted file mode 100644 index 88a5507897..0000000000 --- a/op-program/prestates/releases_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package prestates - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestGetReleases(t *testing.T) { - releases, err := GetReleases() - require.NoError(t, err, "expected no error while parsing embedded releases.json") - - foundGovernanceApproved := false - foundCannon64Release := false - for _, release := range releases { - if release.GovernanceApproved { - foundGovernanceApproved = true - break - } - if release.Type == Cannon64Type { - foundCannon64Release = true - } - } - require.True(t, foundGovernanceApproved, "expected to find at least one GovernanceApproved release") - require.True(t, foundCannon64Release, "expected to find at least one Cannon64 release") -} diff --git a/op-program/prestates/verify/verify.go b/op-program/prestates/verify/verify.go index 20d08d6023..41bc92e19e 100644 --- a/op-program/prestates/verify/verify.go +++ b/op-program/prestates/verify/verify.go @@ -4,15 +4,24 @@ import ( "encoding/json" "flag" "fmt" + "io" + "net/http" "os" "slices" - "github.com/ethereum-optimism/optimism/op-program/prestates" + "github.com/BurntSushi/toml" ) +// standardPrestatesUrl is the URL to the TOML file in superchain registry that defines the list of standard prestates +// Note that this explicitly points to the main branch and is not pinned to a specific version. The verification check +// intends to +const standardPrestatesUrl = "https://raw.githubusercontent.com/ethereum-optimism/superchain-registry/refs/heads/main/validation/standard/standard-prestates.toml" + func main() { var inputFile string flag.StringVar(&inputFile, "input", "", "Releases JSON file to verify") + var expectedFile string + flag.StringVar(&expectedFile, "expected", "", "Override the expected TOML file") flag.Parse() if inputFile == "" { _, _ = fmt.Fprintln(os.Stderr, "Must specify --input") @@ -31,14 +40,14 @@ func main() { _, _ = fmt.Fprintf(os.Stderr, "Failed to read input file: %v\n", err.Error()) os.Exit(2) } - var actual []prestates.Release + var actual []Release err = json.Unmarshal(input, &actual) if err != nil { _, _ = fmt.Fprintf(os.Stderr, "Failed to parse JSON: %v\n", err.Error()) os.Exit(2) } - expected, err := prestates.GetReleases() + expected, err := loadReleases(expectedFile) if err != nil { _, _ = fmt.Fprintf(os.Stderr, "Failed to load expected releases: %v\n", err.Error()) os.Exit(2) @@ -52,36 +61,71 @@ func main() { } return -1 } - sortFunc := func(a, b prestates.Release) int { + sortFunc := func(a, b Release) int { if a.Version > b.Version { return 1 } else if a.Version == b.Version { - return stringCompare(string(a.Type), string(b.Type)) + return stringCompare(a.Type, b.Type) } return -1 } slices.SortFunc(actual, sortFunc) - slices.SortFunc(expected, sortFunc) differs := false report := "" - for i := 0; i < max(len(actual), len(expected)); i++ { - get := func(arr []prestates.Release, idx int) string { - if i >= len(arr) { - return "" - } else { - return formatRelease(arr[i]) + for _, release := range actual { + var expectedPrestate Prestate + standardVersion := expected.Prestates[release.Version] + for _, prestate := range standardVersion { + if prestate.Type == release.Type { + expectedPrestate = prestate + break } } - expectedStr := get(expected, i) - actualStr := get(actual, i) + var expectedStr string + if expectedPrestate == (Prestate{}) { + expectedStr = "" + } else { + expectedStr = formatRelease(Release{ + Version: release.Version, + Type: expectedPrestate.Type, + Hash: expectedPrestate.Hash, + }) + } + actualStr := formatRelease(release) releaseDiffers := expectedStr != actualStr marker := "✅" if releaseDiffers { marker = "❌" + differs = true + } + report += fmt.Sprintf("%v Expected: %v\tActual: %v\n", marker, expectedStr, actualStr) + } + // Verify there aren't any additional entries in expected + totalExpected := 0 + for version, prestates := range expected.Prestates { + for _, prestate := range prestates { + totalExpected++ + // Try to find an actual release matching this expected one + contains := slices.ContainsFunc(actual, func(release Release) bool { + return release.Version == version && release.Type == prestate.Type + }) + if contains { + continue + } + expectedStr := formatRelease(Release{ + Version: version, + Hash: prestate.Hash, + Type: prestate.Type, + }) + report += fmt.Sprintf("❌ Expected: %v\tActual: \n", expectedStr) + differs = true } - report += fmt.Sprintf("%v %d\tExpected: %v\tActual: %v\n", marker, i, expectedStr, actualStr) - differs = differs || releaseDiffers + } + // Sanity check entries are not duplicated in the standard prestates + if totalExpected != len(actual) { + report += fmt.Sprintf("❌ Found %v expected prestates but %v actual\n", totalExpected, len(actual)) + differs = true } fmt.Println(report) if differs { @@ -89,6 +133,48 @@ func main() { } } -func formatRelease(release prestates.Release) string { +func formatRelease(release Release) string { return fmt.Sprintf("%-13v %s %-10v", release.Version, release.Hash, release.Type) } + +func loadReleases(overrideFile string) (*Prestates, error) { + var data []byte + if overrideFile != "" { + d, err := os.ReadFile(overrideFile) + if err != nil { + return nil, fmt.Errorf("failed to read override file (%v): %w", overrideFile, err) + } + data = d + } else { + resp, err := http.Get(standardPrestatesUrl) + if err != nil { + return nil, fmt.Errorf("failed to download standard prestates from %v: %w", standardPrestatesUrl, err) + } + defer resp.Body.Close() + data, err = io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read standard prestates from %v: %w", standardPrestatesUrl, err) + } + } + var standardPrestates Prestates + err := toml.Unmarshal(data, &standardPrestates) + if err != nil { + return nil, fmt.Errorf("failed to parse standard prestates from %v: %w", standardPrestatesUrl, err) + } + return &standardPrestates, nil +} + +type Prestates struct { + Prestates map[string][]Prestate `toml:"prestates"` +} + +type Prestate struct { + Type string `toml:"type"` + Hash string `toml:"hash"` +} + +type Release struct { + Version string `json:"version"` + Hash string `json:"hash"` + Type string `json:"type"` +} diff --git a/op-program/scripts/build-prestates.sh b/op-program/scripts/build-prestates.sh index a36b598c14..2eee2b2feb 100755 --- a/op-program/scripts/build-prestates.sh +++ b/op-program/scripts/build-prestates.sh @@ -34,6 +34,11 @@ do LOG_FILE="${LOGS_DIR}/build-${SHORT_VERSION}.txt" echo "Building Version: ${VERSION} Logs: ${LOG_FILE}" git checkout "${VERSION}" > "${LOG_FILE}" 2>&1 + if [ -f mise.toml ] + then + echo "Install dependencies with mise" >> "${LOG_FILE}" + mise install -v -y >> "${LOG_FILE}" 2>&1 + fi rm -rf "${BIN_DIR}" make reproducible-prestate >> "${LOG_FILE}" 2>&1 HASH=$(cat "${BIN_DIR}/prestate-proof.json" | jq -r .pre) @@ -43,8 +48,8 @@ do else cp "${BIN_DIR}/prestate.json" "${STATES_DIR}/${HASH}.json" fi - VERSIONS_JSON=$(echo "${VERSIONS_JSON}" | jq ". += [{\"version\": \"${SHORT_VERSION}\", \"hash\": \"${HASH}\"}]") - echo "Built ${VERSION}: ${HASH}" + VERSIONS_JSON=$(echo "${VERSIONS_JSON}" | jq ". += [{\"version\": \"${SHORT_VERSION}\", \"hash\": \"${HASH}\", \"type\": \"cannon32\"}]") + echo "Built cannon32 ${VERSION}: ${HASH}" if [ -f "${BIN_DIR}/prestate-proof-mt64.json" ]; then HASH=$(cat "${BIN_DIR}/prestate-proof-mt64.json" | jq -r .pre) diff --git a/op-proposer/contracts/disputegamefactory.go b/op-proposer/contracts/disputegamefactory.go index 16c612594e..838887209e 100644 --- a/op-proposer/contracts/disputegamefactory.go +++ b/op-proposer/contracts/disputegamefactory.go @@ -41,6 +41,9 @@ type DisputeGameFactory struct { func NewDisputeGameFactory(addr common.Address, caller *batching.MultiCaller, networkTimeout time.Duration) *DisputeGameFactory { factoryABI := snapshots.LoadDisputeGameFactoryABI() + // Note: Games might have different ABIs (eg SuperFaultDisputeGame) but since only a very small part of the ABI + // is actually needed, proposer always uses the latest FaultDisputeGameABI. Compatibility with other ABIs is tested + // in disputegamefactory_test.go gameABI := snapshots.LoadFaultDisputeGameABI() return &DisputeGameFactory{ caller: caller, diff --git a/op-proposer/contracts/disputegamefactory_test.go b/op-proposer/contracts/disputegamefactory_test.go index 6254f84a79..b315836959 100644 --- a/op-proposer/contracts/disputegamefactory_test.go +++ b/op-proposer/contracts/disputegamefactory_test.go @@ -11,6 +11,7 @@ import ( "github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock" batchingTest "github.com/ethereum-optimism/optimism/op-service/sources/batching/test" "github.com/ethereum-optimism/optimism/packages/contracts-bedrock/snapshots" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" ) @@ -21,168 +22,184 @@ var proposerAddr = common.Address{0xaa, 0xbb} func TestHasProposedSince(t *testing.T) { cutOffTime := time.Unix(1000, 0) - t.Run("NoProposals", func(t *testing.T) { - stubRpc, factory := setupDisputeGameFactoryTest(t) - withClaims(stubRpc) - - proposed, proposalTime, claim, err := factory.HasProposedSince(context.Background(), proposerAddr, cutOffTime, 0) - require.NoError(t, err) - require.False(t, proposed) - require.Equal(t, time.Time{}, proposalTime) - require.Equal(t, common.Hash{}, claim) - }) - - t.Run("NoMatchingProposal", func(t *testing.T) { - stubRpc, factory := setupDisputeGameFactoryTest(t) - withClaims( - stubRpc, - gameMetadata{ - GameType: 0, - Timestamp: time.Unix(1600, 0), - Address: common.Address{0x22}, - Proposer: common.Address{0xee}, // Wrong proposer - }, - gameMetadata{ - GameType: 1, // Wrong game type - Timestamp: time.Unix(1700, 0), - Address: common.Address{0x33}, - Proposer: proposerAddr, - }, - ) - - proposed, proposalTime, claim, err := factory.HasProposedSince(context.Background(), proposerAddr, cutOffTime, 0) - require.NoError(t, err) - require.False(t, proposed) - require.Equal(t, time.Time{}, proposalTime) - require.Equal(t, common.Hash{}, claim) - }) - - t.Run("MatchingProposalBeforeCutOff", func(t *testing.T) { - stubRpc, factory := setupDisputeGameFactoryTest(t) - withClaims( - stubRpc, - gameMetadata{ - GameType: 0, - Timestamp: time.Unix(999, 0), - Address: common.Address{0x11}, - Proposer: proposerAddr, - }, - gameMetadata{ - GameType: 0, - Timestamp: time.Unix(1600, 0), - Address: common.Address{0x22}, - Proposer: common.Address{0xee}, // Wrong proposer - }, - gameMetadata{ - GameType: 1, // Wrong game type - Timestamp: time.Unix(1700, 0), - Address: common.Address{0x33}, - Proposer: proposerAddr, - }, - ) - - proposed, proposalTime, claim, err := factory.HasProposedSince(context.Background(), proposerAddr, cutOffTime, 0) - require.NoError(t, err) - require.False(t, proposed) - require.Equal(t, time.Time{}, proposalTime) - require.Equal(t, common.Hash{}, claim) - }) - - t.Run("MatchingProposalAtCutOff", func(t *testing.T) { - stubRpc, factory := setupDisputeGameFactoryTest(t) - withClaims( - stubRpc, - gameMetadata{ - GameType: 0, - Timestamp: cutOffTime, - Address: common.Address{0x11}, - Proposer: proposerAddr, - }, - gameMetadata{ - GameType: 0, - Timestamp: time.Unix(1600, 0), - Address: common.Address{0x22}, - Proposer: common.Address{0xee}, // Wrong proposer - }, - gameMetadata{ - GameType: 1, // Wrong game type - Timestamp: time.Unix(1700, 0), - Address: common.Address{0x33}, - Proposer: proposerAddr, - }, - ) - - proposed, proposalTime, claim, err := factory.HasProposedSince(context.Background(), proposerAddr, cutOffTime, 0) - require.NoError(t, err) - require.True(t, proposed) - require.Equal(t, cutOffTime, proposalTime) - require.Equal(t, common.Hash{0xdd}, claim) - }) - - t.Run("MatchingProposalAfterCutOff", func(t *testing.T) { - stubRpc, factory := setupDisputeGameFactoryTest(t) - expectedProposalTime := time.Unix(1100, 0) - withClaims( - stubRpc, - gameMetadata{ - GameType: 0, - Timestamp: expectedProposalTime, - Address: common.Address{0x11}, - Proposer: proposerAddr, - }, - gameMetadata{ - GameType: 0, - Timestamp: time.Unix(1600, 0), - Address: common.Address{0x22}, - Proposer: common.Address{0xee}, // Wrong proposer - }, - gameMetadata{ - GameType: 1, // Wrong game type - Timestamp: time.Unix(1700, 0), - Address: common.Address{0x33}, - Proposer: proposerAddr, - }, - ) - - proposed, proposalTime, claim, err := factory.HasProposedSince(context.Background(), proposerAddr, cutOffTime, 0) - require.NoError(t, err) - require.True(t, proposed) - require.Equal(t, expectedProposalTime, proposalTime) - require.Equal(t, common.Hash{0xdd}, claim) - }) - - t.Run("MultipleMatchingProposalAfterCutOff", func(t *testing.T) { - stubRpc, factory := setupDisputeGameFactoryTest(t) - expectedProposalTime := time.Unix(1600, 0) - withClaims( - stubRpc, - gameMetadata{ - GameType: 0, - Timestamp: time.Unix(1400, 0), - Address: common.Address{0x11}, - Proposer: proposerAddr, - }, - gameMetadata{ - GameType: 0, - Timestamp: time.Unix(1500, 0), - Address: common.Address{0x22}, - Proposer: proposerAddr, - }, - gameMetadata{ - GameType: 0, - Timestamp: expectedProposalTime, - Address: common.Address{0x33}, - Proposer: proposerAddr, - }, - ) - - proposed, proposalTime, claim, err := factory.HasProposedSince(context.Background(), proposerAddr, cutOffTime, 0) - require.NoError(t, err) - require.True(t, proposed) - // Should find the most recent proposal - require.Equal(t, expectedProposalTime, proposalTime) - require.Equal(t, common.Hash{0xdd}, claim) - }) + gameContractTypes := []struct { + name string + abi *abi.ABI + }{ + {"FaultDisputeGame", snapshots.LoadFaultDisputeGameABI()}, + {"SuperFaultDisputeGame", snapshots.LoadSuperFaultDisputeGameABI()}, + } + + for _, contractType := range gameContractTypes { + contractType := contractType + t.Run("NoProposals-"+contractType.name, func(t *testing.T) { + stubRpc, factory := setupDisputeGameFactoryTest(t) + withClaims(stubRpc, contractType.abi) + + proposed, proposalTime, claim, err := factory.HasProposedSince(context.Background(), proposerAddr, cutOffTime, 0) + require.NoError(t, err) + require.False(t, proposed) + require.Equal(t, time.Time{}, proposalTime) + require.Equal(t, common.Hash{}, claim) + }) + + t.Run("NoMatchingProposal-"+contractType.name, func(t *testing.T) { + stubRpc, factory := setupDisputeGameFactoryTest(t) + withClaims( + stubRpc, + contractType.abi, + gameMetadata{ + GameType: 0, + Timestamp: time.Unix(1600, 0), + Address: common.Address{0x22}, + Proposer: common.Address{0xee}, // Wrong proposer + }, + gameMetadata{ + GameType: 1, // Wrong game type + Timestamp: time.Unix(1700, 0), + Address: common.Address{0x33}, + Proposer: proposerAddr, + }, + ) + + proposed, proposalTime, claim, err := factory.HasProposedSince(context.Background(), proposerAddr, cutOffTime, 0) + require.NoError(t, err) + require.False(t, proposed) + require.Equal(t, time.Time{}, proposalTime) + require.Equal(t, common.Hash{}, claim) + }) + + t.Run("MatchingProposalBeforeCutOff-"+contractType.name, func(t *testing.T) { + stubRpc, factory := setupDisputeGameFactoryTest(t) + withClaims( + stubRpc, + contractType.abi, + gameMetadata{ + GameType: 0, + Timestamp: time.Unix(999, 0), + Address: common.Address{0x11}, + Proposer: proposerAddr, + }, + gameMetadata{ + GameType: 0, + Timestamp: time.Unix(1600, 0), + Address: common.Address{0x22}, + Proposer: common.Address{0xee}, // Wrong proposer + }, + gameMetadata{ + GameType: 1, // Wrong game type + Timestamp: time.Unix(1700, 0), + Address: common.Address{0x33}, + Proposer: proposerAddr, + }, + ) + + proposed, proposalTime, claim, err := factory.HasProposedSince(context.Background(), proposerAddr, cutOffTime, 0) + require.NoError(t, err) + require.False(t, proposed) + require.Equal(t, time.Time{}, proposalTime) + require.Equal(t, common.Hash{}, claim) + }) + + t.Run("MatchingProposalAtCutOff-"+contractType.name, func(t *testing.T) { + stubRpc, factory := setupDisputeGameFactoryTest(t) + withClaims( + stubRpc, + contractType.abi, + gameMetadata{ + GameType: 0, + Timestamp: cutOffTime, + Address: common.Address{0x11}, + Proposer: proposerAddr, + }, + gameMetadata{ + GameType: 0, + Timestamp: time.Unix(1600, 0), + Address: common.Address{0x22}, + Proposer: common.Address{0xee}, // Wrong proposer + }, + gameMetadata{ + GameType: 1, // Wrong game type + Timestamp: time.Unix(1700, 0), + Address: common.Address{0x33}, + Proposer: proposerAddr, + }, + ) + + proposed, proposalTime, claim, err := factory.HasProposedSince(context.Background(), proposerAddr, cutOffTime, 0) + require.NoError(t, err) + require.True(t, proposed) + require.Equal(t, cutOffTime, proposalTime) + require.Equal(t, common.Hash{0xdd}, claim) + }) + + t.Run("MatchingProposalAfterCutOff-"+contractType.name, func(t *testing.T) { + stubRpc, factory := setupDisputeGameFactoryTest(t) + expectedProposalTime := time.Unix(1100, 0) + withClaims( + stubRpc, + contractType.abi, + gameMetadata{ + GameType: 0, + Timestamp: expectedProposalTime, + Address: common.Address{0x11}, + Proposer: proposerAddr, + }, + gameMetadata{ + GameType: 0, + Timestamp: time.Unix(1600, 0), + Address: common.Address{0x22}, + Proposer: common.Address{0xee}, // Wrong proposer + }, + gameMetadata{ + GameType: 1, // Wrong game type + Timestamp: time.Unix(1700, 0), + Address: common.Address{0x33}, + Proposer: proposerAddr, + }, + ) + + proposed, proposalTime, claim, err := factory.HasProposedSince(context.Background(), proposerAddr, cutOffTime, 0) + require.NoError(t, err) + require.True(t, proposed) + require.Equal(t, expectedProposalTime, proposalTime) + require.Equal(t, common.Hash{0xdd}, claim) + }) + + t.Run("MultipleMatchingProposalAfterCutOff-"+contractType.name, func(t *testing.T) { + stubRpc, factory := setupDisputeGameFactoryTest(t) + expectedProposalTime := time.Unix(1600, 0) + withClaims( + stubRpc, + contractType.abi, + gameMetadata{ + GameType: 0, + Timestamp: time.Unix(1400, 0), + Address: common.Address{0x11}, + Proposer: proposerAddr, + }, + gameMetadata{ + GameType: 0, + Timestamp: time.Unix(1500, 0), + Address: common.Address{0x22}, + Proposer: proposerAddr, + }, + gameMetadata{ + GameType: 0, + Timestamp: expectedProposalTime, + Address: common.Address{0x33}, + Proposer: proposerAddr, + }, + ) + + proposed, proposalTime, claim, err := factory.HasProposedSince(context.Background(), proposerAddr, cutOffTime, 0) + require.NoError(t, err) + require.True(t, proposed) + // Should find the most recent proposal + require.Equal(t, expectedProposalTime, proposalTime) + require.Equal(t, common.Hash{0xdd}, claim) + }) + } } func TestProposalTx(t *testing.T) { @@ -200,8 +217,7 @@ func TestProposalTx(t *testing.T) { require.Truef(t, bond.Cmp(tx.Value) == 0, "Expected bond %v but was %v", bond, tx.Value) } -func withClaims(stubRpc *batchingTest.AbiBasedRpc, games ...gameMetadata) { - gameAbi := snapshots.LoadFaultDisputeGameABI() +func withClaims(stubRpc *batchingTest.AbiBasedRpc, gameAbi *abi.ABI, games ...gameMetadata) { stubRpc.SetResponse(factoryAddr, methodGameCount, rpcblock.Latest, nil, []interface{}{big.NewInt(int64(len(games)))}) for i, game := range games { stubRpc.SetResponse(factoryAddr, methodGameAtIndex, rpcblock.Latest, []interface{}{big.NewInt(int64(i))}, []interface{}{ @@ -210,6 +226,9 @@ func withClaims(stubRpc *batchingTest.AbiBasedRpc, games ...gameMetadata) { game.Address, }) stubRpc.AddContract(game.Address, gameAbi) + // Note: If this method ABI changes, the proposer will need to be updated to handle both the old and new versions + // since existing dispute games are never changed and the proposer may need to load a game using an old version + // to find its last proposal. stubRpc.SetResponse(game.Address, methodClaim, rpcblock.Latest, []interface{}{big.NewInt(0)}, []interface{}{ uint32(math.MaxUint32), // Parent address (none for root claim) common.Address{}, // Countered by diff --git a/op-service/eth/block_info.go b/op-service/eth/block_info.go index 9530c17ca6..0b1c7c46c4 100644 --- a/op-service/eth/block_info.go +++ b/op-service/eth/block_info.go @@ -21,6 +21,7 @@ type BlockInfo interface { // BlobBaseFee returns the result of computing the blob fee from excessDataGas, or nil if the // block isn't a Dencun (4844 capable) block BlobBaseFee() *big.Int + ExcessBlobGas() *uint64 ReceiptHash() common.Hash GasUsed() uint64 GasLimit() uint64 @@ -130,6 +131,10 @@ func (h *headerBlockInfo) BlobBaseFee() *big.Int { return CalcBlobFeeDefault(h.header) } +func (h *headerBlockInfo) ExcessBlobGas() *uint64 { + return h.header.ExcessBlobGas +} + func (h *headerBlockInfo) ReceiptHash() common.Hash { return h.header.ReceiptHash } diff --git a/op-service/eth/id.go b/op-service/eth/id.go index 3eecdbc3ba..9e13100d15 100644 --- a/op-service/eth/id.go +++ b/op-service/eth/id.go @@ -98,6 +98,14 @@ func (id L1BlockRef) ParentID() BlockID { // Because L1BlockRefs are strict subsets of L2BlockRefs, BlockRef is a direct alias of L1BlockRef type BlockRef = L1BlockRef +func BlockRefFromHeader(h *types.Header) *BlockRef { + return &BlockRef{ + Hash: h.Hash(), + Number: h.Number.Uint64(), + ParentHash: h.ParentHash, + Time: h.Time, + } +} func (id L2BlockRef) ID() BlockID { return BlockID{ Hash: id.Hash, diff --git a/op-service/flags/flags.go b/op-service/flags/flags.go index f8be269303..a4fdbdb481 100644 --- a/op-service/flags/flags.go +++ b/op-service/flags/flags.go @@ -11,14 +11,15 @@ import ( ) const ( - RollupConfigFlagName = "rollup.config" - NetworkFlagName = "network" - CanyonOverrideFlagName = "override.canyon" - DeltaOverrideFlagName = "override.delta" - EcotoneOverrideFlagName = "override.ecotone" - FjordOverrideFlagName = "override.fjord" - GraniteOverrideFlagName = "override.granite" - HoloceneOverrideFlagName = "override.holocene" + RollupConfigFlagName = "rollup.config" + NetworkFlagName = "network" + CanyonOverrideFlagName = "override.canyon" + DeltaOverrideFlagName = "override.delta" + EcotoneOverrideFlagName = "override.ecotone" + FjordOverrideFlagName = "override.fjord" + GraniteOverrideFlagName = "override.granite" + HoloceneOverrideFlagName = "override.holocene" + PectraBlobScheduleOverrideFlagName = "override.pectrablobschedule" ) func CLIFlags(envPrefix string, category string) []cli.Flag { @@ -65,6 +66,13 @@ func CLIFlags(envPrefix string, category string) []cli.Flag { Hidden: false, Category: category, }, + &cli.Uint64Flag{ + Name: PectraBlobScheduleOverrideFlagName, + Usage: "Manually specify the PectraBlobSchedule fork timestamp, overriding the bundled setting", + EnvVars: opservice.PrefixEnvVar(envPrefix, "OVERRIDE_PECTRABLOBSCHEDULE"), + Hidden: false, + Category: category, + }, CLINetworkFlag(envPrefix, category), CLIRollupConfigFlag(envPrefix, category), } diff --git a/op-service/signer/block_auth.go b/op-service/signer/block_auth.go new file mode 100644 index 0000000000..6d26b8e8b3 --- /dev/null +++ b/op-service/signer/block_auth.go @@ -0,0 +1,92 @@ +package signer + +import ( + "context" + "errors" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/ethereum-optimism/optimism/op-service/eth" +) + +type BlockSigner interface { + // SignBlockV1 signs a P2P block, with the V1 signing domain + SignBlockV1(ctx context.Context, chainID eth.ChainID, payloadHash common.Hash) (sig eth.Bytes65, err error) + // Close closes the signer + Close() error +} + +// Authenticator abstractly represents the contextual information and logic needed to verify a SignedObject. +// E.g. a block is verified with context of the chain-ID, the allowed signers, and a versioned signing domain. +// Other block types (future OP-Stack versions, extensions, or e.g. L1 test chains) +// may be verified differently, providing different authentication context. +type Authenticator interface { +} + +// SignedObject abstractly represents an object of which the signature can be checked. +type SignedObject interface { + // VerifySignature verifies the signature of the signed object, + // and returns an error if the signature is not valid. + VerifySignature(auth Authenticator) error +} + +// OPStackP2PBlockAuth provides a P2P block authenticator. +type OPStackP2PBlockAuth interface { + // Check if the given signer is allowed to send messages + Check(signer common.Address) error + // Domain used to sign the message + Domain() [32]byte + // ChainID used to sign the message + ChainID() eth.ChainID + // VerifyP2PBlockSignature verifies a block with payload-hash and signature + VerifyP2PBlockSignature(payloadHash common.Hash, signature eth.Bytes65) error +} + +// SigningDomainBlocksV1 is the original signing domain used for P2P OP-Stack blocks. +// This domain is a fully zeroed 32 bytes. +var SigningDomainBlocksV1 = [32]byte{} + +// OPStackP2PBlockAuthV1 provides the V1 OP-Stack P2P block authentication context. +type OPStackP2PBlockAuthV1 struct { + Allowed common.Address + Chain eth.ChainID +} + +var _ OPStackP2PBlockAuth = (*OPStackP2PBlockAuthV1)(nil) + +func (a *OPStackP2PBlockAuthV1) Check(signer common.Address) error { + if a.Allowed == (common.Address{}) { + return errors.New("missing signer address configuration") + } + if a.Allowed == signer { + return nil + } + return errors.New("unrecognized signer") +} + +func (a *OPStackP2PBlockAuthV1) Domain() [32]byte { + return SigningDomainBlocksV1 +} + +func (a *OPStackP2PBlockAuthV1) ChainID() eth.ChainID { + return a.Chain +} + +func (a *OPStackP2PBlockAuthV1) VerifyP2PBlockSignature(payloadHash common.Hash, signature eth.Bytes65) error { + msg := BlockSigningMessage{ + Domain: a.Domain(), + ChainID: a.ChainID(), + PayloadHash: payloadHash, + } + signingHash := msg.ToSigningHash() + + pub, err := crypto.SigToPub(signingHash[:], signature[:]) + if err != nil { + return fmt.Errorf("failed to recover pubkey from signing hash %s and pubkey %s: %w", + signingHash, signature, err) + } + addr := crypto.PubkeyToAddress(*pub) + return a.Check(addr) +} diff --git a/op-service/signer/block_auth_test.go b/op-service/signer/block_auth_test.go new file mode 100644 index 0000000000..0326f9e0e3 --- /dev/null +++ b/op-service/signer/block_auth_test.go @@ -0,0 +1,63 @@ +package signer + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/ethereum-optimism/optimism/op-service/eth" +) + +func TestBlockAuth(t *testing.T) { + good := common.Address{0: 123} + chainID := eth.ChainIDFromUInt64(42) + v1Auth := OPStackP2PBlockAuthV1{ + Allowed: good, + Chain: chainID, + } + require.Equal(t, chainID, v1Auth.ChainID()) + t.Run("checks", func(t *testing.T) { + require.NoError(t, v1Auth.Check(good)) + require.ErrorContains(t, v1Auth.Check(common.Address{}), "unrecognized") + require.ErrorContains(t, v1Auth.Check(common.Address{0: 124}), "unrecognized") + }) + + t.Run("missing config addr", func(t *testing.T) { + missingAddr := OPStackP2PBlockAuthV1{ + Allowed: common.Address{}, + Chain: chainID, + } + require.ErrorContains(t, missingAddr.Check(good), "missing") + }) + + payload := []byte("test") + payloadHash := crypto.Keccak256Hash(payload) + t.Run("valid case", func(t *testing.T) { + require.ErrorContains(t, v1Auth.VerifyP2PBlockSignature(payloadHash, eth.Bytes65{}), "fail") + }) + + keyA, err := crypto.GenerateKey() + require.NoError(t, err) + signer := NewLocalSigner(keyA) + sigA, err := signer.SignBlockV1(context.Background(), chainID, payloadHash) + require.NoError(t, err) + t.Run("empty sig", func(t *testing.T) { + require.ErrorContains(t, v1Auth.VerifyP2PBlockSignature(payloadHash, eth.Bytes65{}), "fail") + }) + t.Run("unrecognized key", func(t *testing.T) { + require.ErrorContains(t, v1Auth.VerifyP2PBlockSignature(payloadHash, sigA), "unrecognized") + }) + t.Run("malformed", func(t *testing.T) { + sigB := sigA + sigB[64] = 0xe0 + require.ErrorContains(t, v1Auth.VerifyP2PBlockSignature(payloadHash, sigB), "fail") + }) + t.Run("success", func(t *testing.T) { + v1Auth.Allowed = crypto.PubkeyToAddress(keyA.PublicKey) + require.NoError(t, v1Auth.VerifyP2PBlockSignature(payloadHash, sigA)) + }) +} diff --git a/op-service/signer/blockpayload_args_test.go b/op-service/signer/blockpayload_args_test.go index 302e0d2b60..e6cba4b52e 100644 --- a/op-service/signer/blockpayload_args_test.go +++ b/op-service/signer/blockpayload_args_test.go @@ -50,3 +50,56 @@ func TestBlockPayloadArgs(t *testing.T) { require.NoError(t, err) require.Equal(t, h, h2, "signing hash still the same in V2 API") } + +func TestSigningHash_DifferentDomain(t *testing.T) { + chainID := big.NewInt(100) + + payloadBytes := []byte("arbitraryData") + msg, err := NewBlockPayloadArgs(SigningDomainBlocksV1, chainID, payloadBytes, nil).Message() + require.NoError(t, err, "creating first signing hash") + + msg2, err := NewBlockPayloadArgs([32]byte{3}, chainID, payloadBytes, nil).Message() + require.NoError(t, err, "creating second signing hash") + + hash := msg.ToSigningHash() + hash2 := msg2.ToSigningHash() + require.NotEqual(t, hash, hash2, "signing hash should be different when domain is different") +} + +func TestSigningHash_DifferentChainID(t *testing.T) { + chainID1 := big.NewInt(100) + chainID2 := big.NewInt(101) + + payloadBytes := []byte("arbitraryData") + msg, err := NewBlockPayloadArgs(SigningDomainBlocksV1, chainID1, payloadBytes, nil).Message() + require.NoError(t, err, "creating first signing hash") + + msg2, err := NewBlockPayloadArgs(SigningDomainBlocksV1, chainID2, payloadBytes, nil).Message() + require.NoError(t, err, "creating second signing hash") + + hash := msg.ToSigningHash() + hash2 := msg2.ToSigningHash() + require.NotEqual(t, hash, hash2, "signing hash should be different when chain ID is different") +} + +func TestSigningHash_DifferentPayload(t *testing.T) { + chainID := big.NewInt(100) + + msg, err := NewBlockPayloadArgs(SigningDomainBlocksV1, chainID, []byte("payload1"), nil).Message() + require.NoError(t, err, "creating first signing hash") + + msg2, err := NewBlockPayloadArgs(SigningDomainBlocksV1, chainID, []byte("payload2"), nil).Message() + require.NoError(t, err, "creating second signing hash") + + hash := msg.ToSigningHash() + hash2 := msg2.ToSigningHash() + require.NotEqual(t, hash, hash2, "signing hash should be different when payload is different") +} + +func TestSigningHash_LimitChainID(t *testing.T) { + // ChainID with bitlen 257 + chainID := big.NewInt(1) + chainID = chainID.SetBit(chainID, 256, 1) + _, err := NewBlockPayloadArgs(SigningDomainBlocksV1, chainID, []byte("arbitraryData"), nil).Message() + require.ErrorContains(t, err, "chain_id is too large") +} diff --git a/op-service/signer/client.go b/op-service/signer/client.go index 0a970e4bcb..533eea5489 100644 --- a/op-service/signer/client.go +++ b/op-service/signer/client.go @@ -115,7 +115,7 @@ func (s *SignerClient) SignTransaction(ctx context.Context, chainId *big.Int, fr return &signed, nil } -func (s *SignerClient) SignBlockPayload(ctx context.Context, args *BlockPayloadArgs) ([65]byte, error) { +func (s *SignerClient) SignBlockPayload(ctx context.Context, args *BlockPayloadArgs) (eth.Bytes65, error) { var result hexutil.Bytes if err := s.client.CallContext(ctx, &result, "opsigner_signBlockPayload", args); err != nil { @@ -138,3 +138,7 @@ func (s *SignerClient) SignBlockPayloadV2(ctx context.Context, args *BlockPayloa } return result, nil } + +func (s *SignerClient) Close() { + s.client.Close() +} diff --git a/op-service/signer/local_signer.go b/op-service/signer/local_signer.go new file mode 100644 index 0000000000..eebce1e76a --- /dev/null +++ b/op-service/signer/local_signer.go @@ -0,0 +1,45 @@ +package signer + +import ( + "context" + "crypto/ecdsa" + "errors" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/ethereum-optimism/optimism/op-service/eth" +) + +// LocalSigner is suitable for testing +type LocalSigner struct { + priv *ecdsa.PrivateKey +} + +var _ BlockSigner = (*LocalSigner)(nil) + +func NewLocalSigner(priv *ecdsa.PrivateKey) *LocalSigner { + return &LocalSigner{priv: priv} +} + +func (s *LocalSigner) SignBlockV1(ctx context.Context, chainID eth.ChainID, payloadHash common.Hash) (sig eth.Bytes65, err error) { + if s.priv == nil { + return eth.Bytes65{}, errors.New("signer is closed") + } + msg := BlockSigningMessage{ + Domain: SigningDomainBlocksV1, + ChainID: chainID, + PayloadHash: payloadHash, + } + signingHash := msg.ToSigningHash() + signature, err := crypto.Sign(signingHash[:], s.priv) + if err != nil { + return eth.Bytes65{}, err + } + return eth.Bytes65(signature), nil +} + +func (s *LocalSigner) Close() error { + s.priv = nil + return nil +} diff --git a/op-service/signer/local_signer_test.go b/op-service/signer/local_signer_test.go new file mode 100644 index 0000000000..a519ab7f0a --- /dev/null +++ b/op-service/signer/local_signer_test.go @@ -0,0 +1,32 @@ +package signer + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/crypto" + + "github.com/ethereum-optimism/optimism/op-service/eth" +) + +func TestLocalSigner(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + signer := NewLocalSigner(key) + chainID := eth.ChainIDFromUInt64(123) + payloadHash := crypto.Keccak256Hash([]byte("test")) + sig, err := signer.SignBlockV1(context.Background(), chainID, payloadHash) + require.NoError(t, err) + require.NotEqual(t, eth.Bytes65{}, sig) + + authCtx := &OPStackP2PBlockAuthV1{ + Allowed: crypto.PubkeyToAddress(key.PublicKey), + Chain: chainID, + } + require.NoError(t, authCtx.VerifyP2PBlockSignature(payloadHash, sig)) + + require.NoError(t, signer.Close()) + require.Nil(t, signer.priv) +} diff --git a/op-service/signer/remote_signer.go b/op-service/signer/remote_signer.go new file mode 100644 index 0000000000..a6e9b75e57 --- /dev/null +++ b/op-service/signer/remote_signer.go @@ -0,0 +1,54 @@ +package signer + +import ( + "context" + "errors" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + + "github.com/ethereum-optimism/optimism/op-service/eth" +) + +type RemoteSigner struct { + client *SignerClient + sender *common.Address +} + +var _ BlockSigner = (*RemoteSigner)(nil) + +func NewRemoteSigner(logger log.Logger, config CLIConfig) (*RemoteSigner, error) { + signerClient, err := NewSignerClientFromConfig(logger, config) + if err != nil { + return nil, err + } + senderAddress := common.HexToAddress(config.Address) + return &RemoteSigner{signerClient, &senderAddress}, nil +} + +func (s *RemoteSigner) SignBlockV1(ctx context.Context, chainID eth.ChainID, payloadHash common.Hash) (sig eth.Bytes65, err error) { + if s.client == nil { + return eth.Bytes65{}, errors.New("signer is closed") + } + + // We use API V1 for now, since the server may not support V2 yet + blockPayloadArgs := &BlockPayloadArgs{ + Domain: SigningDomainBlocksV1, + ChainID: chainID.ToBig(), + PayloadHash: payloadHash[:], + SenderAddress: s.sender, + } + signature, err := s.client.SignBlockPayload(ctx, blockPayloadArgs) + if err != nil { + return eth.Bytes65{}, err + } + return signature, nil +} + +func (s *RemoteSigner) Close() error { + if s.client != nil { + s.client.Close() + } + s.client = nil + return nil +} diff --git a/op-service/signer/remote_signer_test.go b/op-service/signer/remote_signer_test.go new file mode 100644 index 0000000000..486138116a --- /dev/null +++ b/op-service/signer/remote_signer_test.go @@ -0,0 +1,129 @@ +package signer + +import ( + "context" + "crypto/ecdsa" + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rpc" + + "github.com/ethereum-optimism/optimism/op-service/eth" + oprpc "github.com/ethereum-optimism/optimism/op-service/rpc" + "github.com/ethereum-optimism/optimism/op-service/testlog" +) + +type mockRemoteSigner struct { + priv *ecdsa.PrivateKey + err error +} + +func (t *mockRemoteSigner) SignBlockPayload(args BlockPayloadArgs) (hexutil.Bytes, error) { + if t.err != nil { + return nil, t.err + } + msg, err := args.Message() + if err != nil { + return nil, err + } + signingHash := msg.ToSigningHash() + signature, err := crypto.Sign(signingHash[:], t.priv) + if err != nil { + return nil, err + } + return signature, nil +} + +func TestRemoteSigner(t *testing.T) { + secret, err := crypto.GenerateKey() + require.NoError(t, err) + + remoteSigner := &mockRemoteSigner{priv: secret, err: nil} + server := oprpc.NewServer( + "127.0.0.1", + 0, + "test", + ) + server.AddAPI(rpc.API{ + Namespace: "opsigner", + Service: remoteSigner, + }) + + require.NoError(t, server.Start()) + defer func() { + _ = server.Stop() + }() + + logger := testlog.Logger(t, log.LevelCrit) + + msg := []byte("any msg") + payloadHash := PayloadHash(msg) + + signerCfg := NewCLIConfig() + signerCfg.Endpoint = fmt.Sprintf("http://%s", server.Endpoint()) + signerCfg.TLSConfig.TLSKey = "" + signerCfg.TLSConfig.TLSCert = "" + signerCfg.TLSConfig.TLSCaCert = "" + signerCfg.TLSConfig.Enabled = false + + must := func(fn func() error) func() { + return func() { + require.NoError(t, fn()) + } + } + addr := crypto.PubkeyToAddress(secret.PublicKey) + chainID := eth.ChainIDFromUInt64(100) + + t.Run("Valid", func(t *testing.T) { + remote, err := NewRemoteSigner(logger, signerCfg) + require.NoError(t, err) + t.Cleanup(must(remote.Close)) + sig, err := remote.SignBlockV1(context.Background(), chainID, payloadHash) + require.NoError(t, err) + authCtx := &OPStackP2PBlockAuthV1{ + Allowed: addr, + Chain: chainID, + } + require.NoError(t, authCtx.VerifyP2PBlockSignature(payloadHash, sig)) + }) + t.Run("RPC err", func(t *testing.T) { + remote, err := NewRemoteSigner(logger, signerCfg) + require.NoError(t, err) + t.Cleanup(must(remote.Close)) + testErr := &rpc.JsonError{Code: -39000, Message: "test error"} + remoteSigner.err = testErr + _, err = remote.SignBlockV1(context.Background(), chainID, payloadHash) + var rpcErr rpc.Error + require.True(t, errors.As(err, &rpcErr)) + require.Equal(t, -39000, rpcErr.ErrorCode()) + remoteSigner.err = nil + }) + + t.Run("RemoteSignerNoTLS", func(t *testing.T) { + signerCfg := NewCLIConfig() + signerCfg.Endpoint = fmt.Sprintf("http://%s", server.Endpoint()) + signerCfg.TLSConfig.TLSKey = "invalid" + signerCfg.TLSConfig.TLSCert = "invalid" + signerCfg.TLSConfig.TLSCaCert = "invalid" + signerCfg.TLSConfig.Enabled = true + + _, err := NewRemoteSigner(logger, signerCfg) + require.Error(t, err) + }) + + t.Run("RemoteSignerInvalidEndpoint", func(t *testing.T) { + signerCfg := NewCLIConfig() + signerCfg.Endpoint = "Invalid" + signerCfg.TLSConfig.TLSKey = "" + signerCfg.TLSConfig.TLSCert = "" + signerCfg.TLSConfig.TLSCaCert = "" + _, err := NewRemoteSigner(logger, signerCfg) + require.Error(t, err) + }) +} diff --git a/op-service/signer/signed_block_payload.go b/op-service/signer/signed_block_payload.go new file mode 100644 index 0000000000..cc65c3eb07 --- /dev/null +++ b/op-service/signer/signed_block_payload.go @@ -0,0 +1,25 @@ +package signer + +import ( + "errors" + + "github.com/ethereum/go-ethereum/common/hexutil" + + "github.com/ethereum-optimism/optimism/op-service/eth" +) + +// SignedP2PBlock represents an untrusted block payload, +// of which the signature can be verified with VerifySignature +type SignedP2PBlock struct { + Raw hexutil.Bytes `json:"raw"` + Signature eth.Bytes65 `json:"signature"` +} + +func (s *SignedP2PBlock) VerifySignature(auth Authenticator) error { + p2pAuth, ok := auth.(OPStackP2PBlockAuth) + if !ok { + return errors.New("expected P2P auth context") + } + payloadHash := PayloadHash(s.Raw) + return p2pAuth.VerifyP2PBlockSignature(payloadHash, s.Signature) +} diff --git a/op-service/signer/signed_block_payload_test.go b/op-service/signer/signed_block_payload_test.go new file mode 100644 index 0000000000..13dfcff01c --- /dev/null +++ b/op-service/signer/signed_block_payload_test.go @@ -0,0 +1,55 @@ +package signer + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/crypto" + + "github.com/ethereum-optimism/optimism/op-service/eth" +) + +func TestSignedP2PBlock(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + addr := crypto.PubkeyToAddress(key.PublicKey) + chainID := eth.ChainIDFromUInt64(42) + v1Auth := &OPStackP2PBlockAuthV1{ + Allowed: addr, + Chain: chainID, + } + payload := []byte("hello") + + sigA, err := NewLocalSigner(key).SignBlockV1(context.Background(), chainID, PayloadHash(payload)) + require.NoError(t, err) + + t.Run("valid", func(t *testing.T) { + bl := SignedP2PBlock{ + Raw: payload, + Signature: sigA, + } + require.NoError(t, bl.VerifySignature(v1Auth)) + }) + + t.Run("invalid msg", func(t *testing.T) { + bl := SignedP2PBlock{ + Raw: []byte("different"), + Signature: sigA, + } + require.Error(t, bl.VerifySignature(v1Auth)) + }) + + keyB, err := crypto.GenerateKey() + require.NoError(t, err) + sigB, err := NewLocalSigner(keyB).SignBlockV1(context.Background(), chainID, PayloadHash(payload)) + require.NoError(t, err) + t.Run("different key", func(t *testing.T) { + bl := SignedP2PBlock{ + Raw: payload, + Signature: sigB, + } + require.Error(t, bl.VerifySignature(v1Auth)) + }) +} diff --git a/op-service/signer/signed_envelope.go b/op-service/signer/signed_envelope.go new file mode 100644 index 0000000000..0cde7ae070 --- /dev/null +++ b/op-service/signer/signed_envelope.go @@ -0,0 +1,35 @@ +package signer + +import ( + "bytes" + "fmt" + + "github.com/ethereum-optimism/optimism/op-service/eth" +) + +type SignedExecutionPayloadEnvelope struct { + Envelope *eth.ExecutionPayloadEnvelope `json:"envelope"` + Signature eth.Bytes65 `json:"signature"` +} + +var _ SignedObject = (*SignedExecutionPayloadEnvelope)(nil) + +func (s *SignedExecutionPayloadEnvelope) ID() eth.BlockID { + return s.Envelope.ExecutionPayload.ID() +} + +func (s *SignedExecutionPayloadEnvelope) String() string { + return fmt.Sprintf("signedEnvelope(%s)", s.ID()) +} + +func (s *SignedExecutionPayloadEnvelope) VerifySignature(auth Authenticator) error { + var buf bytes.Buffer + if _, err := s.Envelope.MarshalSSZ(&buf); err != nil { + return fmt.Errorf("failed to encode execution envelope: %w", err) + } + enc := SignedP2PBlock{ + Raw: buf.Bytes(), + Signature: s.Signature, + } + return enc.VerifySignature(auth) +} diff --git a/op-service/sources/eth_client.go b/op-service/sources/eth_client.go index dfde6e4a5e..5151e95bdd 100644 --- a/op-service/sources/eth_client.go +++ b/op-service/sources/eth_client.go @@ -69,6 +69,25 @@ type EthClientConfig struct { MethodResetDuration time.Duration } +// DefaultEthClientConfig creates a new eth client config, +// with caching of data using the given cache-size (in number of blocks). +func DefaultEthClientConfig(cacheSize int) *EthClientConfig { + return &EthClientConfig{ + // receipts and transactions are cached per block + ReceiptsCacheSize: cacheSize, + TransactionsCacheSize: cacheSize, + HeadersCacheSize: cacheSize, + PayloadsCacheSize: cacheSize, + MaxRequestsPerBatch: 20, + MaxConcurrentRequests: 10, + BlockRefsCacheSize: cacheSize, + TrustRPC: false, + MustBePostMerge: true, + RPCProviderKind: RPCKindStandard, + MethodResetDuration: time.Minute, + } +} + func (c *EthClientConfig) Check() error { if c.ReceiptsCacheSize < 0 { return fmt.Errorf("invalid receipts cache size: %d", c.ReceiptsCacheSize) @@ -324,6 +343,16 @@ func (s *EthClient) FetchReceipts(ctx context.Context, blockHash common.Hash) (e return info, receipts, nil } +// FetchReceipt returns a receipt associated with transaction. +func (s *EthClient) FetchReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { + var r *types.Receipt + err := s.client.CallContext(ctx, &r, "eth_getTransactionReceipt", txHash) + if err == nil && r == nil { + return nil, ethereum.NotFound + } + return r, err +} + // PayloadExecutionWitness generates a block from a payload and returns execution witness data. func (s *EthClient) PayloadExecutionWitness(ctx context.Context, parentHash common.Hash, payloadAttributes eth.PayloadAttributes) (*eth.ExecutionWitness, error) { var witness *eth.ExecutionWitness @@ -442,3 +471,81 @@ func (s *EthClient) BlockRefByHash(ctx context.Context, hash common.Hash) (eth.B s.blockRefsCache.Add(ref.Hash, ref) return ref, nil } + +func ToCallArg(msg ethereum.CallMsg) interface{} { + arg := map[string]interface{}{ + "from": msg.From, + "to": msg.To, + } + if len(msg.Data) > 0 { + arg["data"] = hexutil.Bytes(msg.Data) + } + if msg.Value != nil { + arg["value"] = (*hexutil.Big)(msg.Value) + } + if msg.Gas != 0 { + arg["gas"] = hexutil.Uint64(msg.Gas) + } + if msg.GasPrice != nil { + arg["gasPrice"] = (*hexutil.Big)(msg.GasPrice) + } + return arg +} + +// SuggestGasPrice retrieves the currently suggested gas price to allow a timely +// execution of a transaction. +func (s *EthClient) SuggestGasPrice(ctx context.Context) (*big.Int, error) { + var hex hexutil.Big + if err := s.client.CallContext(ctx, &hex, "eth_gasPrice"); err != nil { + return nil, err + } + return (*big.Int)(&hex), nil +} + +// Call executes a message call transaction but never mined into the blockchain. +func (s *EthClient) Call(ctx context.Context, msg ethereum.CallMsg) ([]byte, error) { + var hex hexutil.Bytes + err := s.client.CallContext(ctx, &hex, "eth_call", ToCallArg(msg), "pending") + if err != nil { + return nil, err + } + return hex, nil +} + +// EstimateGas tries to estimate the gas needed to execute a specific transaction. +func (s *EthClient) EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error) { + var hex hexutil.Uint64 + err := s.client.CallContext(ctx, &hex, "eth_estimateGas", ToCallArg(msg)) + if err != nil { + return 0, err + } + return uint64(hex), nil +} + +// SendTransaction submits a signed transaction. +func (s *EthClient) SendTransaction(ctx context.Context, tx *types.Transaction) error { + data, err := tx.MarshalBinary() + if err != nil { + return err + } + return s.client.CallContext(ctx, nil, "eth_sendRawTransaction", hexutil.Encode(data)) +} + +// PendingNonceAt returns the account nonce of the given account in the pending state. +func (s *EthClient) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) { + var result hexutil.Uint64 + err := s.client.CallContext(ctx, &result, "eth_getTransactionCount", account, "pending") + return uint64(result), err +} + +// BalanceAt returns the wei balance of the given account. +func (s *EthClient) BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) { + var result hexutil.Big + var err error + if blockNumber != nil { + err = s.client.CallContext(ctx, &result, "eth_getBalance", account, blockNumber) + } else { + err = s.client.CallContext(ctx, &result, "eth_getBalance", account, "latest") + } + return (*big.Int)(&result), err +} diff --git a/op-service/sources/l1_beacon_client_test.go b/op-service/sources/l1_beacon_client_test.go index 086cb0f9da..bf54db7b78 100644 --- a/op-service/sources/l1_beacon_client_test.go +++ b/op-service/sources/l1_beacon_client_test.go @@ -215,14 +215,14 @@ func TestBeaconHTTPClient(t *testing.T) { } func TestClientPoolSingle(t *testing.T) { - p := NewClientPool[int](1) + p := NewClientPool(1) for i := 0; i < 10; i++ { require.Equal(t, 1, p.Get()) p.MoveToNext() } } func TestClientPoolSeveral(t *testing.T) { - p := NewClientPool[int](0, 1, 2, 3) + p := NewClientPool(0, 1, 2, 3) for i := 0; i < 25; i++ { require.Equal(t, i%4, p.Get()) p.MoveToNext() diff --git a/op-service/sources/supervisor_client.go b/op-service/sources/supervisor_client.go index a656d9fe85..5641fca3f6 100644 --- a/op-service/sources/supervisor_client.go +++ b/op-service/sources/supervisor_client.go @@ -13,10 +13,33 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" ) +type SupervisorAdminAPI interface { + Start(ctx context.Context) error + Stop(ctx context.Context) error + AddL2RPC(ctx context.Context, rpc string, jwtSecret eth.Bytes32) error +} + +type SupervisorQueryAPI interface { + CheckAccessList(ctx context.Context, inboxEntries []common.Hash, + minSafety types.SafetyLevel, executingDescriptor types.ExecutingDescriptor) error + CrossDerivedToSource(ctx context.Context, chainID eth.ChainID, derived eth.BlockID) (derivedFrom eth.BlockRef, err error) + LocalUnsafe(ctx context.Context, chainID eth.ChainID) (eth.BlockID, error) + CrossSafe(ctx context.Context, chainID eth.ChainID) (types.DerivedIDPair, error) + Finalized(ctx context.Context, chainID eth.ChainID) (eth.BlockID, error) + FinalizedL1(ctx context.Context) (eth.BlockRef, error) + SuperRootAtTimestamp(ctx context.Context, timestamp hexutil.Uint64) (eth.SuperRootResponse, error) + SyncStatus(ctx context.Context) (eth.SupervisorSyncStatus, error) + AllSafeDerivedAt(ctx context.Context, derivedFrom eth.BlockID) (derived map[eth.ChainID]eth.BlockID, err error) +} + type SupervisorClient struct { client client.RPC } +// This type-check keeps the Server API and Client API in sync. +var _ SupervisorQueryAPI = (*SupervisorClient)(nil) +var _ SupervisorAdminAPI = (*SupervisorClient)(nil) + func NewSupervisorClient(client client.RPC) *SupervisorClient { return &SupervisorClient{ client: client, @@ -25,10 +48,7 @@ func NewSupervisorClient(client client.RPC) *SupervisorClient { func (cl *SupervisorClient) Stop(ctx context.Context) error { var result error - err := cl.client.CallContext( - ctx, - &result, - "admin_stop") + err := cl.client.CallContext(ctx, &result, "admin_stop") if err != nil { return fmt.Errorf("failed to stop Supervisor: %w", err) } @@ -37,10 +57,7 @@ func (cl *SupervisorClient) Stop(ctx context.Context) error { func (cl *SupervisorClient) Start(ctx context.Context) error { var result error - err := cl.client.CallContext( - ctx, - &result, - "admin_start") + err := cl.client.CallContext(ctx, &result, "admin_start") if err != nil { return fmt.Errorf("failed to start Supervisor: %w", err) } @@ -49,144 +66,67 @@ func (cl *SupervisorClient) Start(ctx context.Context) error { func (cl *SupervisorClient) AddL2RPC(ctx context.Context, rpc string, auth eth.Bytes32) error { var result error - err := cl.client.CallContext( - ctx, - &result, - "admin_addL2RPC", - rpc, auth) + err := cl.client.CallContext(ctx, &result, "admin_addL2RPC", rpc, auth) if err != nil { return fmt.Errorf("failed to Add L2 to Supervisor (rpc: %s): %w", rpc, err) } return result } -func (cl *SupervisorClient) CheckMessage(ctx context.Context, identifier types.Identifier, logHash common.Hash, executingDescriptor types.ExecutingDescriptor) (types.SafetyLevel, error) { - var result types.SafetyLevel - err := cl.client.CallContext( - ctx, - &result, - "supervisor_checkMessage", - identifier, - logHash, - executingDescriptor) - if err != nil { - return types.Invalid, fmt.Errorf("failed to check message (chain %s), (block %v), (index %v), (logHash %s), (executingTimestamp %v): %w", - identifier.ChainID, - identifier.BlockNumber, - identifier.LogIndex, - logHash, - executingDescriptor.Timestamp, - err) - } - return result, nil +func (cl *SupervisorClient) CheckAccessList(ctx context.Context, inboxEntries []common.Hash, + minSafety types.SafetyLevel, executingDescriptor types.ExecutingDescriptor) error { + return cl.client.CallContext(ctx, nil, "supervisor_checkAccessList", inboxEntries, minSafety, executingDescriptor) } -func (cl *SupervisorClient) UnsafeView(ctx context.Context, chainID eth.ChainID, unsafe types.ReferenceView) (types.ReferenceView, error) { - var result types.ReferenceView - err := cl.client.CallContext( - ctx, - &result, - "supervisor_unsafeView", - chainID, - unsafe) - if err != nil { - return types.ReferenceView{}, fmt.Errorf("failed to share unsafe block view %s (chain %s): %w", unsafe, chainID, err) - } - return result, nil -} - -func (cl *SupervisorClient) SafeView(ctx context.Context, chainID eth.ChainID, safe types.ReferenceView) (types.ReferenceView, error) { - var result types.ReferenceView - err := cl.client.CallContext( - ctx, - &result, - "supervisor_safeView", - chainID, - safe) - if err != nil { - return types.ReferenceView{}, fmt.Errorf("failed to share safe block view %s (chain %s): %w", safe, chainID, err) - } - return result, nil +func (cl *SupervisorClient) CrossDerivedToSource(ctx context.Context, chainID eth.ChainID, derived eth.BlockID) (derivedFrom eth.BlockRef, err error) { + var result eth.BlockRef + err = cl.client.CallContext(ctx, &result, "supervisor_crossDerivedToSource", chainID, derived) + return result, err } func (cl *SupervisorClient) LocalUnsafe(ctx context.Context, chainID eth.ChainID) (eth.BlockID, error) { var result eth.BlockID - err := cl.client.CallContext( - ctx, - &result, - "supervisor_localUnsafe", - chainID) + err := cl.client.CallContext(ctx, &result, "supervisor_localUnsafe", chainID) return result, err } func (cl *SupervisorClient) CrossSafe(ctx context.Context, chainID eth.ChainID) (types.DerivedIDPair, error) { var result types.DerivedIDPair - err := cl.client.CallContext( - ctx, - &result, - "supervisor_crossSafe", - chainID) + err := cl.client.CallContext(ctx, &result, "supervisor_crossSafe", chainID) return result, err } func (cl *SupervisorClient) Finalized(ctx context.Context, chainID eth.ChainID) (eth.BlockID, error) { var result eth.BlockID - err := cl.client.CallContext( - ctx, - &result, - "supervisor_finalized", - chainID) + err := cl.client.CallContext(ctx, &result, "supervisor_finalized", chainID) return result, err } func (cl *SupervisorClient) FinalizedL1(ctx context.Context) (eth.BlockRef, error) { var result eth.BlockRef - err := cl.client.CallContext( - ctx, - &result, - "supervisor_finalizedL1") + err := cl.client.CallContext(ctx, &result, "supervisor_finalizedL1") return result, err } func (cl *SupervisorClient) CrossDerivedFrom(ctx context.Context, chainID eth.ChainID, derived eth.BlockID) (eth.BlockRef, error) { var result eth.BlockRef - err := cl.client.CallContext( - ctx, - &result, - "supervisor_crossDerivedFrom", - chainID, - derived) + err := cl.client.CallContext(ctx, &result, "supervisor_crossDerivedFrom", chainID, derived) return result, err } func (cl *SupervisorClient) UpdateLocalUnsafe(ctx context.Context, chainID eth.ChainID, head eth.BlockRef) error { - return cl.client.CallContext( - ctx, - nil, - "supervisor_updateLocalUnsafe", - chainID, - head) + return cl.client.CallContext(ctx, nil, "supervisor_updateLocalUnsafe", chainID, head) } func (cl *SupervisorClient) UpdateLocalSafe(ctx context.Context, chainID eth.ChainID, derivedFrom eth.L1BlockRef, lastDerived eth.BlockRef) error { - return cl.client.CallContext( - ctx, - nil, - "supervisor_updateLocalSafe", - chainID, - derivedFrom, - lastDerived) + return cl.client.CallContext(ctx, nil, "supervisor_updateLocalSafe", chainID, derivedFrom, lastDerived) } // SuperRootAtTimestamp returns the super root at the specified timestamp. // Returns ethereum.NotFound if one of the chain's has not yet reached the block required for the requested super root. func (cl *SupervisorClient) SuperRootAtTimestamp(ctx context.Context, timestamp hexutil.Uint64) (eth.SuperRootResponse, error) { var result eth.SuperRootResponse - err := cl.client.CallContext( - ctx, - &result, - "supervisor_superRootAtTimestamp", - timestamp) + err := cl.client.CallContext(ctx, &result, "supervisor_superRootAtTimestamp", timestamp) if isNotFound(err) { // Downstream users expect to get a properly typed error message for not found. return result, fmt.Errorf("%w: %v", ethereum.NotFound, err.Error()) @@ -196,20 +136,13 @@ func (cl *SupervisorClient) SuperRootAtTimestamp(ctx context.Context, timestamp func (cl *SupervisorClient) AllSafeDerivedAt(ctx context.Context, derivedFrom eth.BlockID) (map[eth.ChainID]eth.BlockID, error) { var result map[eth.ChainID]eth.BlockID - err := cl.client.CallContext( - ctx, - &result, - "supervisor_allSafeDerivedAt", - derivedFrom) + err := cl.client.CallContext(ctx, &result, "supervisor_allSafeDerivedAt", derivedFrom) return result, err } func (cl *SupervisorClient) SyncStatus(ctx context.Context) (eth.SupervisorSyncStatus, error) { var result eth.SupervisorSyncStatus - err := cl.client.CallContext( - ctx, - &result, - "supervisor_syncStatus") + err := cl.client.CallContext(ctx, &result, "supervisor_syncStatus") return result, err } diff --git a/op-service/testutils/l1info.go b/op-service/testutils/l1info.go index ec5f75e080..cc41669ad0 100644 --- a/op-service/testutils/l1info.go +++ b/op-service/testutils/l1info.go @@ -15,19 +15,20 @@ var _ eth.BlockInfo = &MockBlockInfo{} type MockBlockInfo struct { // Prefixed all fields with "Info" to avoid collisions with the interface method names. - InfoHash common.Hash - InfoParentHash common.Hash - InfoCoinbase common.Address - InfoRoot common.Hash - InfoNum uint64 - InfoTime uint64 - InfoMixDigest [32]byte - InfoBaseFee *big.Int - InfoBlobBaseFee *big.Int - InfoReceiptRoot common.Hash - InfoGasUsed uint64 - InfoGasLimit uint64 - InfoHeaderRLP []byte + InfoHash common.Hash + InfoParentHash common.Hash + InfoCoinbase common.Address + InfoRoot common.Hash + InfoNum uint64 + InfoTime uint64 + InfoMixDigest [32]byte + InfoBaseFee *big.Int + InfoBlobBaseFee *big.Int + InfoExcessBlobGas *uint64 + InfoReceiptRoot common.Hash + InfoGasUsed uint64 + InfoGasLimit uint64 + InfoHeaderRLP []byte InfoParentBeaconRoot *common.Hash InfoWithdrawalsRoot *common.Hash @@ -69,6 +70,10 @@ func (l *MockBlockInfo) BlobBaseFee() *big.Int { return l.InfoBlobBaseFee } +func (l *MockBlockInfo) ExcessBlobGas() *uint64 { + return l.InfoExcessBlobGas +} + func (l *MockBlockInfo) ReceiptHash() common.Hash { return l.InfoReceiptRoot } diff --git a/op-service/testutils/random.go b/op-service/testutils/random.go index a18ad020d1..277a29caeb 100644 --- a/op-service/testutils/random.go +++ b/op-service/testutils/random.go @@ -12,6 +12,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/trie" + "github.com/holiman/uint256" ) func RandomBool(rng *rand.Rand) bool { @@ -140,8 +141,16 @@ func RandomTo(rng *rand.Rand) *common.Address { return &to } +func isIsthmusSigner(signer types.Signer) bool { + isthusSigner := types.NewIsthmusSigner(signer.ChainID()) + return signer.Equal(isthusSigner) +} + func RandomTx(rng *rand.Rand, baseFee *big.Int, signer types.Signer) *types.Transaction { txTypeList := []int{types.LegacyTxType, types.AccessListTxType, types.DynamicFeeTxType} + if isIsthmusSigner(signer) { + txTypeList = append(txTypeList, types.SetCodeTxType) + } txType := txTypeList[rng.Intn(len(txTypeList))] var tx *types.Transaction switch txType { @@ -151,6 +160,8 @@ func RandomTx(rng *rand.Rand, baseFee *big.Int, signer types.Signer) *types.Tran tx = RandomAccessListTx(rng, signer) case types.DynamicFeeTxType: tx = RandomDynamicFeeTxWithBaseFee(rng, baseFee, signer) + case types.SetCodeTxType: + tx = RandomSetCodeTx(rng, signer) default: panic("invalid tx type") } @@ -225,6 +236,49 @@ func RandomDynamicFeeTx(rng *rand.Rand, signer types.Signer) *types.Transaction return RandomDynamicFeeTxWithBaseFee(rng, baseFee, signer) } +func RandomSetCodeAuth(rng *rand.Rand) types.SetCodeAuthorization { + key := InsecureRandomKey(rng) + + auth := types.SetCodeAuthorization{ + ChainID: *uint256.MustFromHex("0x0"), + Address: RandomAddress(rng), + Nonce: rng.Uint64(), + } + + authSigned, err := types.SignSetCode(key, auth) + if err != nil { + panic(err) + } + + return authSigned +} + +func RandomSetCodeTx(rng *rand.Rand, signer types.Signer) *types.Transaction { + baseFee := new(big.Int).SetUint64(rng.Uint64()) + key := InsecureRandomKey(rng) + tip := big.NewInt(rng.Int63n(10 * params.GWei)) + to := RandomAddress(rng) + txData := &types.SetCodeTx{ + ChainID: uint256.MustFromBig(signer.ChainID()), + Nonce: rng.Uint64(), + GasTipCap: uint256.MustFromBig(tip), + GasFeeCap: uint256.MustFromBig(new(big.Int).Add(baseFee, tip)), + Gas: params.TxGas + uint64(rng.Int63n(2_000_000)), + To: to, + Value: uint256.MustFromBig(RandomETH(rng, 10)), + Data: RandomData(rng, rng.Intn(RandomDataSize)), + AccessList: nil, + AuthList: []types.SetCodeAuthorization{ + RandomSetCodeAuth(rng), + }, + } + tx, err := types.SignNewTx(key, signer, txData) + if err != nil { + panic(err) + } + return tx +} + func RandomReceipt(rng *rand.Rand, signer types.Signer, tx *types.Transaction, txIndex uint64, cumulativeGasUsed uint64) *types.Receipt { gasUsed := params.TxGas + uint64(rng.Int63n(int64(tx.Gas()-params.TxGas+1))) logs := make([]*types.Log, rng.Intn(10)) @@ -285,7 +339,7 @@ func RandomBlock(rng *rand.Rand, txCount uint64) (*types.Block, []*types.Receipt func RandomBlockPrependTxsWithTime(rng *rand.Rand, txCount int, t uint64, ptxs ...*types.Transaction) (*types.Block, []*types.Receipt) { header := RandomHeaderWithTime(rng, t) chainID := big.NewInt(rng.Int63n(1000)) - signer := types.NewLondonSigner(chainID) + signer := types.NewIsthmusSigner(chainID) txs := make([]*types.Transaction, 0, txCount+len(ptxs)) txs = append(txs, ptxs...) for i := 0; i < txCount; i++ { diff --git a/op-supervisor/supervisor/backend/backend.go b/op-supervisor/supervisor/backend/backend.go index 33dd3ae092..8438a0a3a9 100644 --- a/op-supervisor/supervisor/backend/backend.go +++ b/op-supervisor/supervisor/backend/backend.go @@ -423,92 +423,82 @@ func (su *SupervisorBackend) DependencySet() depset.DependencySet { // Query methods // ---------------------------- -func (su *SupervisorBackend) CheckMessage(identifier types.Identifier, payloadHash common.Hash, executingDescriptor types.ExecutingDescriptor) (types.SafetyLevel, error) { - logHash := types.PayloadHashToLogHash(payloadHash, identifier.Origin) - chainID := identifier.ChainID - blockNum := identifier.BlockNumber - logIdx := identifier.LogIndex - _, err := su.chainDBs.Contains(chainID, - types.ContainsQuery{ - BlockNum: blockNum, - Timestamp: identifier.Timestamp, - LogIdx: logIdx, - LogHash: logHash, - }) - if errors.Is(err, types.ErrFuture) { - su.logger.Debug("Future message", "identifier", identifier, "payloadHash", payloadHash, "err", err) - return types.LocalUnsafe, nil - } - if errors.Is(err, types.ErrConflict) { - su.logger.Debug("Conflicting message", "identifier", identifier, "payloadHash", payloadHash, "err", err) - return types.Invalid, nil +// checkAccess checks message timestamp invariants and inclusion in the chain. +// If the initiating message exists, the block it is included in is returned. +func (su *SupervisorBackend) checkAccess(acc types.Access, execAt types.ExecutingDescriptor) (eth.BlockID, error) { + // Check if message passes time checks + if err := execAt.AccessCheck(su.depSet.MessageExpiryWindow(), acc.Timestamp); err != nil { + return eth.BlockID{}, err } + + // Check if message exists + bl, err := su.chainDBs.Contains(acc.ChainID, types.ContainsQuery{ + Timestamp: acc.Timestamp, + BlockNum: acc.BlockNumber, + LogIdx: acc.LogIndex, + Checksum: acc.Checksum, + }) if err != nil { - return types.Invalid, fmt.Errorf("failed to check log: %w", err) - } - if identifier.Timestamp+su.depSet.MessageExpiryWindow() < executingDescriptor.Timestamp { - su.logger.Debug("Message expired", "identifier", identifier, "payloadHash", payloadHash, "executingTimestamp", executingDescriptor.Timestamp) - return types.Invalid, nil + return eth.BlockID{}, err } - if identifier.Timestamp > executingDescriptor.Timestamp { - su.logger.Debug("Message timestamp is in the future", "identifier", identifier, "payloadHash", payloadHash, "executingTimestamp", executingDescriptor.Timestamp) - return types.Invalid, nil + return bl.ID(), nil +} + +// checkSafety is a helper method to check if a block has the given safety level. +// It is already assumed to exist in the canonical unsafe chain. +func (su *SupervisorBackend) checkSafety(chainID eth.ChainID, blockID eth.BlockID, safetyLevel types.SafetyLevel) error { + switch safetyLevel { + case types.LocalUnsafe: + return nil // msg exists, nothing more to check + case types.CrossUnsafe: + return su.chainDBs.IsCrossUnsafe(chainID, blockID) + case types.LocalSafe: + return su.chainDBs.IsLocalSafe(chainID, blockID) + case types.CrossSafe: + return su.chainDBs.IsCrossSafe(chainID, blockID) + case types.Finalized: + return su.chainDBs.IsFinalized(chainID, blockID) + default: + return types.ErrConflict } - return su.chainDBs.Safest(chainID, blockNum, logIdx) } -func (su *SupervisorBackend) CheckMessagesV2( - messages []types.Message, - minSafety types.SafetyLevel, - executingDescriptor types.ExecutingDescriptor) error { - su.logger.Debug("Checking messages", "count", len(messages), "minSafety", minSafety, "executingTimestamp", executingDescriptor.Timestamp) +func (su *SupervisorBackend) CheckAccessList(ctx context.Context, inboxEntries []common.Hash, + minSafety types.SafetyLevel, executingDescriptor types.ExecutingDescriptor) error { + switch minSafety { + case types.LocalUnsafe, types.CrossUnsafe, types.LocalSafe, types.CrossSafe, types.Finalized: + // valid safety level + default: + return errors.New("unexpected min-safety level") + } - for _, msg := range messages { - su.logger.Debug("Checking message", - "identifier", msg.Identifier, "payloadHash", msg.PayloadHash.String(), "executingTimestamp", executingDescriptor.Timestamp) - safety, err := su.CheckMessage(msg.Identifier, msg.PayloadHash, executingDescriptor) - if err != nil { - su.logger.Error("Check message failed", "err", err, - "identifier", msg.Identifier, "payloadHash", msg.PayloadHash.String(), "executingTimestamp", executingDescriptor.Timestamp) - return fmt.Errorf("failed to check message: %w", err) + su.logger.Debug("Checking access-list", + "minSafety", minSafety, "length", len(inboxEntries)) + + // TODO(#14800): acquire a rewind-read-lock, so we can ensure the safety of all entries is consistent + + entries := inboxEntries + for len(entries) > 0 { + if err := ctx.Err(); err != nil { + return fmt.Errorf("stopped acces-list check early: %w", err) } - if !safety.AtLeastAsSafe(minSafety) { - su.logger.Error("Message is not sufficiently safe", - "safety", safety, "minSafety", minSafety, - "identifier", msg.Identifier, "payloadHash", msg.PayloadHash.String(), "executingTimestamp", executingDescriptor.Timestamp) - return fmt.Errorf("message %v (safety level: %v) does not meet the minimum safety %v", - msg.Identifier, - safety, - minSafety) + remaining, acc, err := types.ParseAccess(entries) + if err != nil { + return fmt.Errorf("failed to read data: %w", err) } - } - return nil -} - -func (su *SupervisorBackend) CheckMessages( - messages []types.Message, - minSafety types.SafetyLevel) error { - su.logger.Debug("Checking messages", "count", len(messages), "minSafety", minSafety) + entries = remaining - for _, msg := range messages { - su.logger.Debug("Checking message", - "identifier", msg.Identifier, "payloadHash", msg.PayloadHash.String()) - // Guarantee message expiry checks do not fail by setting the executing timestamp to the message timestamp - // This is intentionally done to avoid breaking checkMessagesV1 which doesn't handle message expiry checks - safety, err := su.CheckMessage(msg.Identifier, msg.PayloadHash, types.ExecutingDescriptor{Timestamp: msg.Identifier.Timestamp}) + msgBlock, err := su.checkAccess(acc, executingDescriptor) if err != nil { - su.logger.Error("Check message failed", "err", err, - "identifier", msg.Identifier, "payloadHash", msg.PayloadHash.String()) - return fmt.Errorf("failed to check message: %w", err) + su.logger.Debug("Access-list inclusion check failed", "err", err) + return types.ErrConflict } - if !safety.AtLeastAsSafe(minSafety) { - su.logger.Error("Message is not sufficiently safe", - "safety", safety, "minSafety", minSafety, - "identifier", msg.Identifier, "payloadHash", msg.PayloadHash.String()) - return fmt.Errorf("message %v (safety level: %v) does not meet the minimum safety %v", - msg.Identifier, - safety, - minSafety) + // TODO(#14800) add msgBlock to rewind lock + + // TODO(#14800): this can be deferred to only check the latest block of all access entries + if err := su.checkSafety(acc.ChainID, msgBlock, minSafety); err != nil { + su.logger.Debug("Access-list safety check failed", "err", err) + return types.ErrConflict } } return nil @@ -590,8 +580,12 @@ func (su *SupervisorBackend) Finalized(ctx context.Context, chainID eth.ChainID) return v.ID(), nil } -func (su *SupervisorBackend) FinalizedL1() eth.BlockRef { - return su.chainDBs.FinalizedL1() +func (su *SupervisorBackend) FinalizedL1(ctx context.Context) (eth.BlockRef, error) { + v := su.chainDBs.FinalizedL1() + if v == (eth.BlockRef{}) { + return eth.BlockRef{}, errors.New("finality of L1 is not initialized") + } + return v, nil } func (su *SupervisorBackend) IsLocalUnsafe(ctx context.Context, chainID eth.ChainID, block eth.BlockID) error { @@ -676,7 +670,7 @@ func (su *SupervisorBackend) SuperRootAtTimestamp(ctx context.Context, timestamp }, nil } -func (su *SupervisorBackend) SyncStatus() (eth.SupervisorSyncStatus, error) { +func (su *SupervisorBackend) SyncStatus(ctx context.Context) (eth.SupervisorSyncStatus, error) { return su.statusTracker.SyncStatus() } diff --git a/op-supervisor/supervisor/backend/cross/hazard_set.go b/op-supervisor/supervisor/backend/cross/hazard_set.go index c13e99b2a4..4bc74f8c38 100644 --- a/op-supervisor/supervisor/backend/cross/hazard_set.go +++ b/op-supervisor/supervisor/backend/cross/hazard_set.go @@ -147,13 +147,14 @@ func (h *HazardSet) build(deps HazardDeps, logger log.Logger, chainID eth.ChainI if err := h.checkChainCanInitiate(depSet, srcChainID, candidate, msg); err != nil { return err } - includedIn, err := deps.Contains(srcChainID, - types.ContainsQuery{ - Timestamp: msg.Timestamp, - BlockNum: msg.BlockNum, - LogIdx: msg.LogIdx, - LogHash: msg.Hash, - }) + q := types.ChecksumArgs{ + BlockNumber: msg.BlockNum, + LogIndex: msg.LogIdx, + Timestamp: msg.Timestamp, + ChainID: srcChainID, + LogHash: msg.Hash, + }.Query() + includedIn, err := deps.Contains(srcChainID, q) if err != nil { return fmt.Errorf("executing msg %s failed inclusion check: %w", msg, err) } diff --git a/op-supervisor/supervisor/backend/cross/safe_update.go b/op-supervisor/supervisor/backend/cross/safe_update.go index f942c851c5..2e7c58bcf0 100644 --- a/op-supervisor/supervisor/backend/cross/safe_update.go +++ b/op-supervisor/supervisor/backend/cross/safe_update.go @@ -36,8 +36,6 @@ type CrossSafeDeps interface { func CrossSafeUpdate(logger log.Logger, chainID eth.ChainID, d CrossSafeDeps) error { logger.Debug("Cross-safe update call") - // TODO(#11693): establish L1 reorg-lock of scopeDerivedFrom - // defer unlock once we are done checking the chain candidate, err := scopedCrossSafeUpdate(logger, chainID, d) if err == nil { // if we made progress, and no errors, then there is no need to bump the L1 scope yet. diff --git a/op-supervisor/supervisor/backend/cross/safe_update_test.go b/op-supervisor/supervisor/backend/cross/safe_update_test.go index af783e52c6..891610fe29 100644 --- a/op-supervisor/supervisor/backend/cross/safe_update_test.go +++ b/op-supervisor/supervisor/backend/cross/safe_update_test.go @@ -8,7 +8,6 @@ import ( "github.com/ethereum-optimism/optimism/op-service/testlog" "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/backend/depset" "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" "github.com/stretchr/testify/require" ) @@ -31,7 +30,7 @@ func TestCrossSafeUpdate(t *testing.T) { csd.openBlockFn = func(chainID eth.ChainID, blockNum uint64) (ref eth.BlockRef, logCount uint32, execMsgs map[uint32]*types.ExecutingMessage, err error) { return opened, 10, execs, nil } - csd.checkFn = func(chainID eth.ChainID, blockNum uint64, logIdx uint32, logHash common.Hash) (types.BlockSeal, error) { + csd.checkFn = func(chainID eth.ChainID, blockNum uint64, logIdx uint32, checksum types.MessageChecksum) (types.BlockSeal, error) { return types.BlockSeal{Number: 1, Timestamp: 1}, nil } csd.deps = mockDependencySet{} @@ -126,7 +125,7 @@ func TestCrossSafeUpdate(t *testing.T) { csd.openBlockFn = func(chainID eth.ChainID, blockNum uint64) (ref eth.BlockRef, logCount uint32, execMsgs map[uint32]*types.ExecutingMessage, err error) { return opened, 10, execs, nil } - csd.checkFn = func(chainID eth.ChainID, blockNum uint64, logIdx uint32, logHash common.Hash) (types.BlockSeal, error) { + csd.checkFn = func(chainID eth.ChainID, blockNum uint64, logIdx uint32, checksum types.MessageChecksum) (types.BlockSeal, error) { return types.BlockSeal{Number: 1, Timestamp: 1}, nil } invalidated := false @@ -362,7 +361,7 @@ func TestScopedCrossSafeUpdate(t *testing.T) { csd.openBlockFn = func(chainID eth.ChainID, blockNum uint64) (ref eth.BlockRef, logCount uint32, execMsgs map[uint32]*types.ExecutingMessage, err error) { return opened, 10, execs, nil } - csd.checkFn = func(chainID eth.ChainID, blockNum uint64, logIdx uint32, logHash common.Hash) (types.BlockSeal, error) { + csd.checkFn = func(chainID eth.ChainID, blockNum uint64, logIdx uint32, checksum types.MessageChecksum) (types.BlockSeal, error) { return types.BlockSeal{Number: 1, Timestamp: 1}, nil } count := 0 @@ -401,7 +400,7 @@ func TestScopedCrossSafeUpdate(t *testing.T) { csd.openBlockFn = func(chainID eth.ChainID, blockNum uint64) (ref eth.BlockRef, logCount uint32, execMsgs map[uint32]*types.ExecutingMessage, err error) { return opened, 3, map[uint32]*types.ExecutingMessage{1: em1, 2: em2}, nil } - csd.checkFn = func(chainID eth.ChainID, blockNum uint64, logIdx uint32, logHash common.Hash) (types.BlockSeal, error) { + csd.checkFn = func(chainID eth.ChainID, blockNum uint64, logIdx uint32, checksum types.MessageChecksum) (types.BlockSeal, error) { return types.BlockSeal{Number: 1, Timestamp: 1}, nil } csd.deps = mockDependencySet{} @@ -429,7 +428,7 @@ func TestScopedCrossSafeUpdate(t *testing.T) { csd.openBlockFn = func(chainID eth.ChainID, blockNum uint64) (ref eth.BlockRef, logCount uint32, execMsgs map[uint32]*types.ExecutingMessage, err error) { return opened, 10, execs, nil } - csd.checkFn = func(chainID eth.ChainID, blockNum uint64, logIdx uint32, logHash common.Hash) (types.BlockSeal, error) { + csd.checkFn = func(chainID eth.ChainID, blockNum uint64, logIdx uint32, checksum types.MessageChecksum) (types.BlockSeal, error) { return types.BlockSeal{Number: 1, Timestamp: 1}, nil } csd.deps = mockDependencySet{} @@ -497,7 +496,7 @@ func TestScopedCrossSafeUpdate(t *testing.T) { // when no errors occur, the update is carried out // the used candidate and scope are from CandidateCrossSafe // the candidateScope is returned - csd.checkFn = func(chainID eth.ChainID, blockNum uint64, logIdx uint32, logHash common.Hash) (types.BlockSeal, error) { + csd.checkFn = func(chainID eth.ChainID, blockNum uint64, logIdx uint32, checksum types.MessageChecksum) (types.BlockSeal, error) { return types.BlockSeal{Number: 1, Timestamp: 1}, nil } pair, err := scopedCrossSafeUpdate(logger, chainID, csd) @@ -517,7 +516,7 @@ type mockCrossSafeDeps struct { updateCrossSafeFn func(chain eth.ChainID, l1View eth.BlockRef, lastCrossDerived eth.BlockRef) error nextSourceFn func(chain eth.ChainID, source eth.BlockID) (after eth.BlockRef, err error) previousDerivedFn func(chain eth.ChainID, derived eth.BlockID) (prevDerived types.BlockSeal, err error) - checkFn func(chainID eth.ChainID, blockNum uint64, logIdx uint32, logHash common.Hash) (types.BlockSeal, error) + checkFn func(chainID eth.ChainID, blockNum uint64, logIdx uint32, checksum types.MessageChecksum) (types.BlockSeal, error) invalidateLocalSafeFn func(chainID eth.ChainID, candidate types.DerivedBlockRefPair) error } @@ -547,7 +546,7 @@ func (m *mockCrossSafeDeps) CrossDerivedToSource(chainID eth.ChainID, derived et func (m *mockCrossSafeDeps) Contains(chainID eth.ChainID, q types.ContainsQuery) (types.BlockSeal, error) { if m.checkFn != nil { - return m.checkFn(chainID, q.BlockNum, q.LogIdx, q.LogHash) + return m.checkFn(chainID, q.BlockNum, q.LogIdx, q.Checksum) } return types.BlockSeal{}, nil } diff --git a/op-supervisor/supervisor/backend/cross/unsafe_frontier.go b/op-supervisor/supervisor/backend/cross/unsafe_frontier.go index f7eaecf4c2..8746e13455 100644 --- a/op-supervisor/supervisor/backend/cross/unsafe_frontier.go +++ b/op-supervisor/supervisor/backend/cross/unsafe_frontier.go @@ -10,7 +10,7 @@ import ( ) type UnsafeFrontierCheckDeps interface { - ParentBlock(chainID eth.ChainID, parentOf eth.BlockID) (parent eth.BlockID, err error) + FindBlockID(chainID eth.ChainID, blockNum uint64) (eth.BlockID, error) IsCrossUnsafe(chainID eth.ChainID, block eth.BlockID) error IsLocalUnsafe(chainID eth.ChainID, block eth.BlockID) error @@ -46,7 +46,7 @@ func HazardUnsafeFrontierChecks(d UnsafeFrontierCheckDeps, hazards *HazardSet) e // If it doesn't have a parent block, then there is no prior block required to be cross-safe if hazardBlock.Number > 0 { // Check that parent of hazardBlockID is cross-safe within view - parent, err := d.ParentBlock(hazardChainID, hazardBlock.ID()) + parent, err := d.FindBlockID(hazardChainID, hazardBlock.Number-1) if err != nil { return fmt.Errorf("failed to retrieve parent-block of hazard block %s (chain %s): %w", hazardBlock, hazardChainID, err) } diff --git a/op-supervisor/supervisor/backend/cross/unsafe_frontier_test.go b/op-supervisor/supervisor/backend/cross/unsafe_frontier_test.go index df4080f701..110aafb253 100644 --- a/op-supervisor/supervisor/backend/cross/unsafe_frontier_test.go +++ b/op-supervisor/supervisor/backend/cross/unsafe_frontier_test.go @@ -68,7 +68,7 @@ func TestHazardUnsafeFrontierChecks(t *testing.T) { ufcd := &mockUnsafeFrontierCheckDeps{} hazards := map[types.ChainIndex]types.BlockSeal{types.ChainIndex(0): {Number: 3}} ufcd.isCrossUnsafe = types.ErrFuture - ufcd.parentBlockFn = func() (parent eth.BlockID, err error) { + ufcd.findBlockIDFn = func() (parent eth.BlockID, err error) { return eth.BlockID{}, errors.New("some error") } // when there is one hazard, and IsCrossUnsafe returns an ErrFuture, @@ -81,7 +81,7 @@ func TestHazardUnsafeFrontierChecks(t *testing.T) { ufcd := &mockUnsafeFrontierCheckDeps{} hazards := map[types.ChainIndex]types.BlockSeal{types.ChainIndex(0): {Number: 3}} ufcd.isCrossUnsafe = types.ErrFuture - ufcd.parentBlockFn = func() (parent eth.BlockID, err error) { + ufcd.findBlockIDFn = func() (parent eth.BlockID, err error) { // when getting the parent block, prep isCrossSafe to be err ufcd.isCrossUnsafe = errors.New("not cross unsafe!") return eth.BlockID{}, nil @@ -105,7 +105,7 @@ func TestHazardUnsafeFrontierChecks(t *testing.T) { type mockUnsafeFrontierCheckDeps struct { deps mockDependencySet - parentBlockFn func() (parent eth.BlockID, err error) + findBlockIDFn func() (parent eth.BlockID, err error) isCrossUnsafe error isLocalUnsafe error } @@ -114,9 +114,9 @@ func (m *mockUnsafeFrontierCheckDeps) DependencySet() depset.DependencySet { return m.deps } -func (m *mockUnsafeFrontierCheckDeps) ParentBlock(chainID eth.ChainID, block eth.BlockID) (parent eth.BlockID, err error) { - if m.parentBlockFn != nil { - return m.parentBlockFn() +func (m *mockUnsafeFrontierCheckDeps) FindBlockID(chainID eth.ChainID, num uint64) (parent eth.BlockID, err error) { + if m.findBlockIDFn != nil { + return m.findBlockIDFn() } return eth.BlockID{}, nil } diff --git a/op-supervisor/supervisor/backend/cross/unsafe_update.go b/op-supervisor/supervisor/backend/cross/unsafe_update.go index 2550142acf..47147838fb 100644 --- a/op-supervisor/supervisor/backend/cross/unsafe_update.go +++ b/op-supervisor/supervisor/backend/cross/unsafe_update.go @@ -50,9 +50,6 @@ func CrossUnsafeUpdate(logger log.Logger, chainID eth.ChainID, d CrossUnsafeDeps hazards, err := CrossUnsafeHazards(d, logger, chainID, candidate) if err != nil { - // TODO(#11693): reorgs can be detected by checking if the error is ErrConflict, - // missing data is identified by ErrFuture, - // and other errors (e.g. DB issues) are identifier by remaining error kinds. return fmt.Errorf("failed to check for cross-chain hazards: %w", err) } diff --git a/op-supervisor/supervisor/backend/cross/unsafe_update_test.go b/op-supervisor/supervisor/backend/cross/unsafe_update_test.go index 5c77435736..c1f6fba990 100644 --- a/op-supervisor/supervisor/backend/cross/unsafe_update_test.go +++ b/op-supervisor/supervisor/backend/cross/unsafe_update_test.go @@ -138,7 +138,7 @@ func TestCrossUnsafeUpdate(t *testing.T) { usd.openBlockFn = func(chainID eth.ChainID, blockNum uint64) (ref eth.BlockRef, logCount uint32, execMsgs map[uint32]*types.ExecutingMessage, err error) { return bl, 3, map[uint32]*types.ExecutingMessage{1: em1, 2: em2}, nil } - usd.checkFn = func(chainID eth.ChainID, blockNum uint64, timestamp uint64, logIdx uint32, logHash common.Hash) (types.BlockSeal, error) { + usd.checkFn = func(chainID eth.ChainID, blockNum uint64, timestamp uint64, logIdx uint32, checksum types.MessageChecksum) (types.BlockSeal, error) { return types.BlockSeal{Number: 1, Timestamp: 1}, nil } usd.deps = mockDependencySet{} @@ -167,7 +167,7 @@ func TestCrossUnsafeUpdate(t *testing.T) { Hash: crossUnsafe.Hash, }, 0, nil, nil } - usd.checkFn = func(chainID eth.ChainID, blockNum uint64, timestamp uint64, logIdx uint32, logHash common.Hash) (types.BlockSeal, error) { + usd.checkFn = func(chainID eth.ChainID, blockNum uint64, timestamp uint64, logIdx uint32, checksum types.MessageChecksum) (types.BlockSeal, error) { return crossUnsafe, nil } usd.deps = mockDependencySet{} @@ -193,7 +193,7 @@ type mockCrossUnsafeDeps struct { crossUnsafeFn func(chainID eth.ChainID) (types.BlockSeal, error) openBlockFn func(chainID eth.ChainID, blockNum uint64) (ref eth.BlockRef, logCount uint32, execMsgs map[uint32]*types.ExecutingMessage, err error) updateCrossUnsafeFn func(chain eth.ChainID, crossUnsafe types.BlockSeal) error - checkFn func(chainID eth.ChainID, blockNum uint64, timestamp uint64, logIdx uint32, logHash common.Hash) (types.BlockSeal, error) + checkFn func(chainID eth.ChainID, blockNum uint64, timestamp uint64, logIdx uint32, checksum types.MessageChecksum) (types.BlockSeal, error) } func (m *mockCrossUnsafeDeps) CrossUnsafe(chainID eth.ChainID) (derived types.BlockSeal, err error) { @@ -213,7 +213,7 @@ func (m *mockCrossUnsafeDeps) MessageExpiryWindow() uint64 { func (m *mockCrossUnsafeDeps) Contains(chainID eth.ChainID, q types.ContainsQuery) (types.BlockSeal, error) { if m.checkFn != nil { - return m.checkFn(chainID, q.BlockNum, q.Timestamp, q.LogIdx, q.LogHash) + return m.checkFn(chainID, q.BlockNum, q.Timestamp, q.LogIdx, q.Checksum) } return types.BlockSeal{}, nil } @@ -240,6 +240,6 @@ func (m *mockCrossUnsafeDeps) IsLocalUnsafe(chainID eth.ChainID, blockNum eth.Bl return nil } -func (m *mockCrossUnsafeDeps) ParentBlock(chainID eth.ChainID, blockNum eth.BlockID) (eth.BlockID, error) { +func (m *mockCrossUnsafeDeps) FindBlockID(chainID eth.ChainID, num uint64) (eth.BlockID, error) { return eth.BlockID{}, nil } diff --git a/op-supervisor/supervisor/backend/db/logs/db.go b/op-supervisor/supervisor/backend/db/logs/db.go index 7af29fc132..55f61fe1d8 100644 --- a/op-supervisor/supervisor/backend/db/logs/db.go +++ b/op-supervisor/supervisor/backend/db/logs/db.go @@ -38,22 +38,25 @@ type DB struct { store entrydb.EntryStore[EntryType, Entry] rwLock sync.RWMutex + chainID eth.ChainID + lastEntryContext logContext } -func NewFromFile(logger log.Logger, m Metrics, path string, trimToLastSealed bool) (*DB, error) { +func NewFromFile(logger log.Logger, m Metrics, chainID eth.ChainID, path string, trimToLastSealed bool) (*DB, error) { store, err := entrydb.NewEntryDB[EntryType, Entry, EntryBinary](logger, path) if err != nil { return nil, fmt.Errorf("failed to open DB: %w", err) } - return NewFromEntryStore(logger, m, store, trimToLastSealed) + return NewFromEntryStore(logger, m, chainID, store, trimToLastSealed) } -func NewFromEntryStore(logger log.Logger, m Metrics, store entrydb.EntryStore[EntryType, Entry], trimToLastSealed bool) (*DB, error) { +func NewFromEntryStore(logger log.Logger, m Metrics, chainID eth.ChainID, store entrydb.EntryStore[EntryType, Entry], trimToLastSealed bool) (*DB, error) { db := &DB{ - log: logger, - m: m, - store: store, + log: logger, + m: m, + store: store, + chainID: chainID, } if err := db.init(trimToLastSealed); err != nil { return nil, fmt.Errorf("failed to init database: %w", err) @@ -266,15 +269,6 @@ func (db *DB) LatestSealedBlock() (id eth.BlockID, ok bool) { }, true } -// Get returns the hash of the log at the specified blockNum (of the sealed block) -// and logIdx (of the log after the block), or an error if the log is not found. -func (db *DB) Get(blockNum uint64, logIdx uint32) (common.Hash, error) { - db.rwLock.RLock() - defer db.rwLock.RUnlock() - hash, _, err := db.findLogInfo(blockNum, logIdx) - return hash, err -} - // Contains returns no error iff the specified logHash is recorded in the specified blockNum and logIdx. // If the log is out of reach, then ErrFuture is returned. // If the log is determined to conflict with the canonical chain, then ErrConflict is returned. @@ -283,10 +277,10 @@ func (db *DB) Get(blockNum uint64, logIdx uint32) (common.Hash, error) { // The block-seal of the blockNum block, that the log was included in, is returned. // This seal may be fully zeroed, without error, if the block isn't fully known yet. func (db *DB) Contains(query types.ContainsQuery) (types.BlockSeal, error) { - blockNum, logIdx, logHash, timestamp := query.BlockNum, query.LogIdx, query.LogHash, query.Timestamp + blockNum, logIdx, timestamp := query.BlockNum, query.LogIdx, query.Timestamp db.rwLock.RLock() defer db.rwLock.RUnlock() - db.log.Trace("Checking for log", "blockNum", blockNum, "logIdx", logIdx, "hash", logHash) + db.log.Trace("Checking for log", "blockNum", blockNum, "logIdx", logIdx) // Hot-path: check if we have the block if db.lastEntryContext.hasCompleteBlock() && db.lastEntryContext.blockNum < blockNum { @@ -299,15 +293,11 @@ func (db *DB) Contains(query types.ContainsQuery) (types.BlockSeal, error) { return types.BlockSeal{}, types.ErrFuture } - evtHash, iter, err := db.findLogInfo(blockNum, logIdx) + entryLogHash, iter, err := db.findLogInfo(blockNum, logIdx) if err != nil { return types.BlockSeal{}, err // may be ErrConflict if the block does not have as many logs } - db.log.Trace("Found initiatingEvent", "blockNum", blockNum, "logIdx", logIdx, "hash", evtHash) - // Found the requested block and log index, check if the hash matches - if evtHash != logHash { - return types.BlockSeal{}, fmt.Errorf("payload hash mismatch: expected %s, got %s %w", logHash, evtHash, types.ErrConflict) - } + db.log.Trace("Found initiatingEvent", "blockNum", blockNum, "logIdx", logIdx, "hash", entryLogHash) // Now find the block seal after the log, to identify where the log was included in. err = iter.TraverseConditional(func(state IteratorState) error { _, n, ok := state.SealedBlock() @@ -336,6 +326,17 @@ func (db *DB) Contains(query types.ContainsQuery) (types.BlockSeal, error) { if t != timestamp { return types.BlockSeal{}, fmt.Errorf("timestamp mismatch: expected %d, got %d %w", timestamp, t, types.ErrConflict) } + entryChecksum := types.ChecksumArgs{ + BlockNumber: n, + LogIndex: logIdx, + Timestamp: t, + ChainID: db.chainID, + LogHash: entryLogHash, + }.Checksum() + // Found the requested block and log index, check if the hash matches + if entryChecksum != query.Checksum { + return types.BlockSeal{}, fmt.Errorf("payload hash mismatch: expected %s, got %s %w", query.Checksum, entryChecksum, types.ErrConflict) + } // construct a block seal with the found data now that we know it's correct return types.BlockSeal{ Hash: h, @@ -346,6 +347,9 @@ func (db *DB) Contains(query types.ContainsQuery) (types.BlockSeal, error) { return types.BlockSeal{}, err } +// findLogInfo returns the hash of the log at the specified block number and log index. +// If the log index is out of range we return an ErrFuture if the block is complete, +// or ErrConflict if it's not. func (db *DB) findLogInfo(blockNum uint64, logIdx uint32) (common.Hash, Iterator, error) { if blockNum == 0 { return common.Hash{}, nil, types.ErrConflict // no logs in block 0 @@ -361,6 +365,10 @@ func (db *DB) findLogInfo(blockNum uint64, logIdx uint32) (common.Hash, Iterator return common.Hash{}, nil, err } if err := iter.NextInitMsg(); err != nil { + // if we get an ErrFuture but have a complete block, then we really have a conflict + if errors.Is(err, types.ErrFuture) && db.lastEntryContext.hasCompleteBlock() { + err = types.ErrConflict + } return common.Hash{}, nil, fmt.Errorf("failed to read initiating message %d, on top of block %d: %w", logIdx, blockNum, err) } if _, x, ok := iter.SealedBlock(); !ok { diff --git a/op-supervisor/supervisor/backend/db/logs/db_test.go b/op-supervisor/supervisor/backend/db/logs/db_test.go index 2996804211..89f603fb15 100644 --- a/op-supervisor/supervisor/backend/db/logs/db_test.go +++ b/op-supervisor/supervisor/backend/db/logs/db_test.go @@ -38,7 +38,8 @@ func createHash(i int) common.Hash { func TestErrorOpeningDatabase(t *testing.T) { dir := t.TempDir() - _, err := NewFromFile(testlog.Logger(t, log.LvlInfo), &stubMetrics{}, filepath.Join(dir, "missing-dir", "file.db"), false) + chainID := eth.ChainIDFromUInt64(123) + _, err := NewFromFile(testlog.Logger(t, log.LvlInfo), &stubMetrics{}, chainID, filepath.Join(dir, "missing-dir", "file.db"), false) require.ErrorIs(t, err, os.ErrNotExist) } @@ -47,7 +48,8 @@ func runDBTest(t *testing.T, setup func(t *testing.T, db *DB, m *stubMetrics), a logger := testlog.Logger(t, log.LvlTrace) path := filepath.Join(dir, "test.db") m := &stubMetrics{} - db, err := NewFromFile(logger, m, path, false) + chainID := eth.ChainIDFromUInt64(123) + db, err := NewFromFile(logger, m, chainID, path, false) require.NoError(t, err, "Failed to create database") t.Cleanup(func() { err := db.Close() @@ -852,12 +854,14 @@ func requireContains(t *testing.T, db *DB, blockNum uint64, logIdx uint32, times require.LessOrEqual(t, len(execMsg), 1, "cannot have multiple executing messages for a single log") m, ok := db.m.(*stubMetrics) require.True(t, ok, "Did not get the expected metrics type") - _, err := db.Contains(types.ContainsQuery{ - Timestamp: timestamp, - BlockNum: blockNum, - LogIdx: logIdx, - LogHash: logHash, - }) + q := types.ChecksumArgs{ + BlockNumber: blockNum, + LogIndex: logIdx, + Timestamp: timestamp, + ChainID: db.chainID, + LogHash: logHash, + }.Query() + _, err := db.Contains(q) require.NoErrorf(t, err, "Error searching for log %v in block %v", logIdx, blockNum) require.LessOrEqual(t, m.entriesReadForSearch, int64(searchCheckpointFrequency*2), "Should not need to read more than between two checkpoints") require.NotZero(t, m.entriesReadForSearch, "Must read at least some entries to find the log") @@ -872,12 +876,14 @@ func requireContains(t *testing.T, db *DB, blockNum uint64, logIdx uint32, times func requireConflicts(t *testing.T, db *DB, blockNum uint64, logIdx uint32, timestamp uint64, logHash common.Hash) { m, ok := db.m.(*stubMetrics) require.True(t, ok, "Did not get the expected metrics type") - _, err := db.Contains(types.ContainsQuery{ - Timestamp: timestamp, - BlockNum: blockNum, - LogIdx: logIdx, - LogHash: logHash, - }) + q := types.ChecksumArgs{ + BlockNumber: blockNum, + LogIndex: logIdx, + Timestamp: timestamp, + ChainID: db.chainID, + LogHash: logHash, + }.Query() + _, err := db.Contains(q) require.ErrorIs(t, err, types.ErrConflict, "canonical chain must not include this log") require.LessOrEqual(t, m.entriesReadForSearch, int64(searchCheckpointFrequency*2), "Should not need to read more than between two checkpoints") } @@ -885,12 +891,14 @@ func requireConflicts(t *testing.T, db *DB, blockNum uint64, logIdx uint32, time func requireFuture(t *testing.T, db *DB, blockNum uint64, logIdx uint32, timestamp uint64, logHash common.Hash) { m, ok := db.m.(*stubMetrics) require.True(t, ok, "Did not get the expected metrics type") - _, err := db.Contains(types.ContainsQuery{ - Timestamp: timestamp, - BlockNum: blockNum, - LogIdx: logIdx, - LogHash: logHash, - }) + q := types.ChecksumArgs{ + BlockNumber: blockNum, + LogIndex: logIdx, + Timestamp: timestamp, + ChainID: db.chainID, + LogHash: logHash, + }.Query() + _, err := db.Contains(q) require.ErrorIs(t, err, types.ErrFuture, "canonical chain does not yet include this log") require.LessOrEqual(t, m.entriesReadForSearch, int64(searchCheckpointFrequency*2), "Should not need to read more than between two checkpoints") } @@ -912,10 +920,11 @@ func requireExecutingMessage(t *testing.T, db *DB, blockNum uint64, logIdx uint3 } func TestRecoverOnCreate(t *testing.T) { + chainID := eth.ChainIDFromUInt64(123) createDb := func(t *testing.T, store *entrydb.MemEntryStore[EntryType, Entry]) (*DB, *stubMetrics, error) { logger := testlog.Logger(t, log.LvlInfo) m := &stubMetrics{} - db, err := NewFromEntryStore(logger, m, store, true) + db, err := NewFromEntryStore(logger, m, chainID, store, true) return db, m, err } diff --git a/op-supervisor/supervisor/backend/db/open.go b/op-supervisor/supervisor/backend/db/open.go index 8e6b44edea..4129175202 100644 --- a/op-supervisor/supervisor/backend/db/open.go +++ b/op-supervisor/supervisor/backend/db/open.go @@ -15,7 +15,7 @@ func OpenLogDB(logger log.Logger, chainID eth.ChainID, dataDir string, m logs.Me if err != nil { return nil, fmt.Errorf("failed to create datadir for chain %s: %w", chainID, err) } - logDB, err := logs.NewFromFile(logger, m, path, true) + logDB, err := logs.NewFromFile(logger, m, chainID, path, true) if err != nil { return nil, fmt.Errorf("failed to create logdb for chain %s at %v: %w", chainID, path, err) } diff --git a/op-supervisor/supervisor/backend/db/query.go b/op-supervisor/supervisor/backend/db/query.go index a103ef2f86..400d95ec4d 100644 --- a/op-supervisor/supervisor/backend/db/query.go +++ b/op-supervisor/supervisor/backend/db/query.go @@ -17,6 +17,14 @@ func (db *ChainsDB) FindSealedBlock(chain eth.ChainID, number uint64) (seal type return logDB.FindSealedBlock(number) } +func (db *ChainsDB) FindBlockID(chain eth.ChainID, number uint64) (id eth.BlockID, err error) { + sealed, err := db.FindSealedBlock(chain, number) + if err != nil { + return eth.BlockID{}, err + } + return sealed.ID(), nil +} + // LatestBlockNum returns the latest fully-sealed block number that has been recorded to the logs db // for the given chain. It does not contain safety guarantees. // The block number might not be available (empty database, or non-existent chain). @@ -29,36 +37,34 @@ func (db *ChainsDB) LatestBlockNum(chain eth.ChainID) (num uint64, ok bool) { return bl.Number, ok } +// IsCrossUnsafe checks if the given block is less than the cross-unsafe block number. +// It does not check if the block is actually cross-unsafe (ie, known in the database). func (db *ChainsDB) IsCrossUnsafe(chainID eth.ChainID, block eth.BlockID) error { - v, ok := db.crossUnsafe.Get(chainID) + xU, ok := db.crossUnsafe.Get(chainID) if !ok { return types.ErrUnknownChain } - crossUnsafe := v.Get() + crossUnsafe := xU.Get() if crossUnsafe == (types.BlockSeal{}) { return types.ErrFuture } if block.Number > crossUnsafe.Number { return types.ErrFuture } - // TODO(#11693): make cross-unsafe reorg safe - return nil -} - -func (db *ChainsDB) ParentBlock(chainID eth.ChainID, parentOf eth.BlockID) (parent eth.BlockID, err error) { - logDB, ok := db.logDBs.Get(chainID) + // now we know it's within the cross-unsafe range + // check if it's consistent with unsafe data + lU, ok := db.logDBs.Get(chainID) if !ok { - return eth.BlockID{}, types.ErrUnknownChain - } - if parentOf.Number == 0 { - return eth.BlockID{}, nil + return types.ErrUnknownChain } - // TODO(#11693): make parent-lookup reorg safe - got, err := logDB.FindSealedBlock(parentOf.Number - 1) + unsafeBlock, err := lU.FindSealedBlock(block.Number) if err != nil { - return eth.BlockID{}, err + return fmt.Errorf("failed to find sealed block %d: %w", block.Number, err) + } + if unsafeBlock.ID() != block { + return fmt.Errorf("found %s but was looking for unsafe block %s: %w", unsafeBlock.ID(), block, types.ErrConflict) } - return got.ID(), nil + return nil } func (db *ChainsDB) IsLocalUnsafe(chainID eth.ChainID, block eth.BlockID) error { @@ -92,6 +98,21 @@ func (db *ChainsDB) IsLocalSafe(chainID eth.ChainID, block eth.BlockID) error { return ldb.ContainsDerived(block) } +func (db *ChainsDB) IsFinalized(chainID eth.ChainID, block eth.BlockID) error { + finL1 := db.FinalizedL1() + if finL1 == (eth.BlockRef{}) { + return types.ErrUninitialized + } + source, err := db.CrossDerivedToSource(chainID, block) + if err != nil { + return fmt.Errorf("failed to get cross-safe source: %w", err) + } + if finL1.Number >= source.Number { + return nil + } + return fmt.Errorf("cross-safe source block is not finalized: %w", types.ErrFuture) +} + func (db *ChainsDB) SafeDerivedAt(chainID eth.ChainID, source eth.BlockID) (types.BlockSeal, error) { lDB, ok := db.localDBs.Get(chainID) if !ok { @@ -397,41 +418,6 @@ func (db *ChainsDB) NextSource(chain eth.ChainID, source eth.BlockID) (after eth return v.MustWithParent(source), nil } -// Safest returns the strongest safety level that can be guaranteed for the given log entry. -// it assumes the log entry has already been checked and is valid, this function only checks safety levels. -// Safety levels are assumed to graduate from LocalUnsafe to LocalSafe to CrossUnsafe to CrossSafe, with Finalized as the strongest. -func (db *ChainsDB) Safest(chainID eth.ChainID, blockNum uint64, index uint32) (safest types.SafetyLevel, err error) { - if finalized, err := db.Finalized(chainID); err == nil { - if finalized.Number >= blockNum { - return types.Finalized, nil - } - } - crossSafe, err := db.CrossSafe(chainID) - if err != nil { - return types.Invalid, err - } - if crossSafe.Derived.Number >= blockNum { - return types.CrossSafe, nil - } - crossUnsafe, err := db.CrossUnsafe(chainID) - if err != nil { - return types.Invalid, err - } - // TODO(#12425): API: "index" for in-progress block building shouldn't be exposed from DB. - // For now we're not counting anything cross-safe until the block is sealed. - if blockNum <= crossUnsafe.Number { - return types.CrossUnsafe, nil - } - localSafe, err := db.LocalSafe(chainID) - if err != nil { - return types.Invalid, err - } - if blockNum <= localSafe.Derived.Number { - return types.LocalSafe, nil - } - return types.LocalUnsafe, nil -} - func (db *ChainsDB) IteratorStartingAt(chain eth.ChainID, sealedNum uint64, logIndex uint32) (logs.Iterator, error) { logDB, ok := db.logDBs.Get(chain) if !ok { diff --git a/op-supervisor/supervisor/backend/mock.go b/op-supervisor/supervisor/backend/mock.go index c6de2b91df..98157ddab8 100644 --- a/op-supervisor/supervisor/backend/mock.go +++ b/op-supervisor/supervisor/backend/mock.go @@ -47,15 +47,8 @@ func (m *MockBackend) AddL2RPC(ctx context.Context, rpc string, jwtSecret eth.By return nil } -func (m *MockBackend) CheckMessage(identifier types.Identifier, payloadHash common.Hash, executingDescriptor types.ExecutingDescriptor) (types.SafetyLevel, error) { - return types.CrossUnsafe, nil -} - -func (m *MockBackend) CheckMessages(messages []types.Message, minSafety types.SafetyLevel) error { - return nil -} - -func (m *MockBackend) CheckMessagesV2(messages []types.Message, minSafety types.SafetyLevel, executingDescriptor types.ExecutingDescriptor) error { +func (m *MockBackend) CheckAccessList(ctx context.Context, inboxEntries []common.Hash, + minSafety types.SafetyLevel, executingDescriptor types.ExecutingDescriptor) error { return nil } @@ -71,8 +64,8 @@ func (m *MockBackend) Finalized(ctx context.Context, chainID eth.ChainID) (eth.B return eth.BlockID{}, nil } -func (m *MockBackend) FinalizedL1() eth.BlockRef { - return eth.BlockRef{} +func (m *MockBackend) FinalizedL1(ctx context.Context) (eth.BlockRef, error) { + return eth.BlockRef{}, nil } func (m *MockBackend) CrossDerivedToSource(ctx context.Context, chainID eth.ChainID, derived eth.BlockID) (source eth.BlockRef, err error) { @@ -83,7 +76,7 @@ func (m *MockBackend) SuperRootAtTimestamp(ctx context.Context, timestamp hexuti return eth.SuperRootResponse{}, nil } -func (m *MockBackend) SyncStatus() (eth.SupervisorSyncStatus, error) { +func (m *MockBackend) SyncStatus(ctx context.Context) (eth.SupervisorSyncStatus, error) { return eth.SupervisorSyncStatus{}, nil } diff --git a/op-supervisor/supervisor/backend/rewinder/rewinder_test.go b/op-supervisor/supervisor/backend/rewinder/rewinder_test.go index 19353fe0ca..ab358d13ea 100644 --- a/op-supervisor/supervisor/backend/rewinder/rewinder_test.go +++ b/op-supervisor/supervisor/backend/rewinder/rewinder_test.go @@ -1455,7 +1455,7 @@ func setupTestChains(t *testing.T, chainIDs ...eth.ChainID) *testSetup { require.NoError(t, err) // Create and open the log DB - logDB, err := logs.NewFromFile(logger, &stubMetrics{}, filepath.Join(chainDir, "log.db"), true) + logDB, err := logs.NewFromFile(logger, &stubMetrics{}, chainID, filepath.Join(chainDir, "log.db"), true) require.NoError(t, err) chainsDB.AddLogDB(chainID, logDB) diff --git a/op-supervisor/supervisor/frontend/frontend.go b/op-supervisor/supervisor/frontend/frontend.go index 91dfe3fe29..fce8603048 100644 --- a/op-supervisor/supervisor/frontend/frontend.go +++ b/op-supervisor/supervisor/frontend/frontend.go @@ -3,65 +3,28 @@ package frontend import ( "context" - "github.com/ethereum-optimism/optimism/op-service/eth" - "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" -) -type AdminBackend interface { - Start(ctx context.Context) error - Stop(ctx context.Context) error - AddL2RPC(ctx context.Context, rpc string, jwtSecret eth.Bytes32) error -} - -type QueryBackend interface { - CheckMessage(identifier types.Identifier, payloadHash common.Hash, executingDescriptor types.ExecutingDescriptor) (types.SafetyLevel, error) - CheckMessages(messages []types.Message, minSafety types.SafetyLevel) error - CheckMessagesV2(messages []types.Message, minSafety types.SafetyLevel, executingDescriptor types.ExecutingDescriptor) error - CrossDerivedToSource(ctx context.Context, chainID eth.ChainID, derived eth.BlockID) (derivedFrom eth.BlockRef, err error) - LocalUnsafe(ctx context.Context, chainID eth.ChainID) (eth.BlockID, error) - CrossSafe(ctx context.Context, chainID eth.ChainID) (types.DerivedIDPair, error) - Finalized(ctx context.Context, chainID eth.ChainID) (eth.BlockID, error) - FinalizedL1() eth.BlockRef - SuperRootAtTimestamp(ctx context.Context, timestamp hexutil.Uint64) (eth.SuperRootResponse, error) - SyncStatus() (eth.SupervisorSyncStatus, error) - AllSafeDerivedAt(ctx context.Context, derivedFrom eth.BlockID) (derived map[eth.ChainID]eth.BlockID, err error) -} + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/sources" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" +) type Backend interface { - AdminBackend - QueryBackend + sources.SupervisorAdminAPI + sources.SupervisorQueryAPI } type QueryFrontend struct { - Supervisor QueryBackend -} - -var _ QueryBackend = (*QueryFrontend)(nil) - -// CheckMessage checks the safety-level of an individual message. -// The payloadHash references the hash of the message-payload of the message. -func (q *QueryFrontend) CheckMessage(identifier types.Identifier, payloadHash common.Hash, executingDescriptor types.ExecutingDescriptor) (types.SafetyLevel, error) { - return q.Supervisor.CheckMessage(identifier, payloadHash, executingDescriptor) + Supervisor sources.SupervisorQueryAPI } -// CheckMessagesV2 checks the safety-level of a collection of messages, -// and returns if the minimum safety-level is met for all messages. -func (q *QueryFrontend) CheckMessagesV2( - messages []types.Message, - minSafety types.SafetyLevel, - executingDescriptor types.ExecutingDescriptor) error { - return q.Supervisor.CheckMessagesV2(messages, minSafety, executingDescriptor) -} +var _ sources.SupervisorQueryAPI = (*QueryFrontend)(nil) -// CheckMessages checks the safety-level of a collection of messages, -// and returns if the minimum safety-level is met for all messages. -// Deprecated: This method does not check for message expiry. -func (q *QueryFrontend) CheckMessages( - messages []types.Message, - minSafety types.SafetyLevel) error { - return q.Supervisor.CheckMessages(messages, minSafety) +func (q *QueryFrontend) CheckAccessList(ctx context.Context, inboxEntries []common.Hash, + minSafety types.SafetyLevel, executingDescriptor types.ExecutingDescriptor) error { + return q.Supervisor.CheckAccessList(ctx, inboxEntries, minSafety, executingDescriptor) } func (q *QueryFrontend) LocalUnsafe(ctx context.Context, chainID eth.ChainID) (eth.BlockID, error) { @@ -76,8 +39,8 @@ func (q *QueryFrontend) Finalized(ctx context.Context, chainID eth.ChainID) (eth return q.Supervisor.Finalized(ctx, chainID) } -func (q *QueryFrontend) FinalizedL1() eth.BlockRef { - return q.Supervisor.FinalizedL1() +func (q *QueryFrontend) FinalizedL1(ctx context.Context) (eth.BlockRef, error) { + return q.Supervisor.FinalizedL1(ctx) } // CrossDerivedFrom is deprecated, but remains for backwards compatibility to callers @@ -98,15 +61,15 @@ func (q *QueryFrontend) AllSafeDerivedAt(ctx context.Context, derivedFrom eth.Bl return q.Supervisor.AllSafeDerivedAt(ctx, derivedFrom) } -func (q *QueryFrontend) SyncStatus() (eth.SupervisorSyncStatus, error) { - return q.Supervisor.SyncStatus() +func (q *QueryFrontend) SyncStatus(ctx context.Context) (eth.SupervisorSyncStatus, error) { + return q.Supervisor.SyncStatus(ctx) } type AdminFrontend struct { Supervisor Backend } -var _ AdminBackend = (*AdminFrontend)(nil) +var _ sources.SupervisorAdminAPI = (*AdminFrontend)(nil) // Start starts the service, if it was previously stopped. func (a *AdminFrontend) Start(ctx context.Context) error { diff --git a/op-supervisor/supervisor/service_test.go b/op-supervisor/supervisor/service_test.go index 01003e17f6..b7508ce909 100644 --- a/op-supervisor/supervisor/service_test.go +++ b/op-supervisor/supervisor/service_test.go @@ -65,18 +65,10 @@ func TestSupervisorService(t *testing.T) { cl, err := dial.DialRPCClientWithTimeout(context.Background(), time.Second*5, logger, endpoint) require.NoError(t, err) ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - var dest types.SafetyLevel - err = cl.CallContext(ctx, &dest, "supervisor_checkMessage", - types.Identifier{ - Origin: common.Address{0xaa}, - BlockNumber: 123, - LogIndex: 42, - Timestamp: 1234567, - ChainID: eth.ChainID{0xbb}, - }, common.Hash{0xcc}, types.ExecutingDescriptor{Timestamp: 1234568}) + err = cl.CallContext(ctx, nil, "supervisor_checkAccessList", + []common.Hash{}, types.CrossUnsafe, types.ExecutingDescriptor{Timestamp: 1234568}) cancel() require.NoError(t, err) - require.Equal(t, types.CrossUnsafe, dest, "expecting mock to return cross-unsafe") cl.Close() } require.NoError(t, supervisor.Stop(context.Background()), "stop service") diff --git a/op-supervisor/supervisor/types/types.go b/op-supervisor/supervisor/types/types.go index 6302a7086b..d26b58cb9a 100644 --- a/op-supervisor/supervisor/types/types.go +++ b/op-supervisor/supervisor/types/types.go @@ -1,22 +1,25 @@ package types import ( + "encoding/binary" "encoding/json" "errors" "fmt" "math" "strconv" - ethTypes "github.com/ethereum/go-ethereum/core/types" + "github.com/holiman/uint256" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + ethTypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum-optimism/optimism/op-service/eth" ) // ChainIndex represents the lifetime of a chain in a dependency set. +// Warning: JSON-encoded as string, in base-10. type ChainIndex uint32 func (ci ChainIndex) String() string { @@ -42,7 +45,7 @@ type ContainsQuery struct { Timestamp uint64 BlockNum uint64 LogIdx uint32 - LogHash common.Hash // LogHash commits to the origin-address and the message payload-hash + Checksum MessageChecksum } type ExecutingMessage struct { @@ -50,7 +53,7 @@ type ExecutingMessage struct { BlockNum uint64 LogIdx uint32 Timestamp uint64 - Hash common.Hash + Hash common.Hash // LogHash (hash of msgHash and origin address) } func (s *ExecutingMessage) String() string { @@ -63,6 +66,56 @@ type Message struct { PayloadHash common.Hash `json:"payloadHash"` } +func (m *Message) Checksum() MessageChecksum { + args := ChecksumArgs{ + BlockNumber: m.Identifier.BlockNumber, + LogIndex: m.Identifier.LogIndex, + Timestamp: m.Identifier.Timestamp, + ChainID: m.Identifier.ChainID, + LogHash: PayloadHashToLogHash(m.PayloadHash, m.Identifier.Origin), + } + return args.Checksum() +} + +type ChecksumArgs struct { + BlockNumber uint64 + LogIndex uint32 + Timestamp uint64 + ChainID eth.ChainID + LogHash common.Hash +} + +func (args ChecksumArgs) Checksum() MessageChecksum { + idPacked := make([]byte, 12, 32) // 12 zero bytes, as padding to 32 bytes + idPacked = binary.BigEndian.AppendUint64(idPacked, args.BlockNumber) + idPacked = binary.BigEndian.AppendUint64(idPacked, args.Timestamp) + idPacked = binary.BigEndian.AppendUint32(idPacked, args.LogIndex) + idLogHash := crypto.Keccak256Hash(args.LogHash[:], idPacked) + chainID := args.ChainID.Bytes32() + out := crypto.Keccak256Hash(idLogHash[:], chainID[:]) + out[0] = 0x03 // type/version byte + return MessageChecksum(out) +} + +func (args ChecksumArgs) Access() Access { + return Access{ + BlockNumber: args.BlockNumber, + Timestamp: args.Timestamp, + LogIndex: args.LogIndex, + ChainID: args.ChainID, + Checksum: args.Checksum(), + } +} + +func (args ChecksumArgs) Query() ContainsQuery { + return ContainsQuery{ + BlockNum: args.BlockNumber, + Timestamp: args.Timestamp, + LogIdx: args.LogIndex, + Checksum: args.Checksum(), + } +} + type Identifier struct { Origin common.Address BlockNumber uint64 @@ -105,6 +158,16 @@ func (id *Identifier) UnmarshalJSON(input []byte) error { return nil } +func (id Identifier) ChecksumArgs(msgHash common.Hash) ChecksumArgs { + return ChecksumArgs{ + BlockNumber: id.BlockNumber, + Timestamp: id.Timestamp, + LogIndex: id.LogIndex, + ChainID: id.ChainID, + LogHash: PayloadHashToLogHash(msgHash, id.Origin), + } +} + type SafetyLevel string func (lvl SafetyLevel) String() string { @@ -137,30 +200,6 @@ func (lvl *SafetyLevel) UnmarshalText(text []byte) error { return nil } -// AtLeastAsSafe returns true if the receiver is at least as safe as the other SafetyLevel. -// Safety levels are assumed to graduate from LocalUnsafe to LocalSafe to CrossUnsafe to CrossSafe, with Finalized as the strongest. -func (lvl *SafetyLevel) AtLeastAsSafe(min SafetyLevel) bool { - relativeSafety := map[SafetyLevel]int{ - Invalid: 0, - LocalUnsafe: 1, - LocalSafe: 2, - CrossUnsafe: 3, - CrossSafe: 4, - Finalized: 5, - } - // if either level is not recognized, return false - _, ok := relativeSafety[*lvl] - if !ok { - return false - } - _, ok = relativeSafety[min] - if !ok { - return false - } - // compare the relative safety levels to determine if the receiver is at least as safe as the other - return relativeSafety[*lvl] >= relativeSafety[min] -} - const ( // Finalized is CrossSafe, with the additional constraint that every // dependency is derived only from finalized L1 input data. @@ -187,15 +226,59 @@ const ( type ExecutingDescriptor struct { // Timestamp is the timestamp of the executing message Timestamp uint64 + + // Timeout, requests verification to still hold at Timestamp+Timeout (incl.). Defaults to 0. + // I.e. Timestamp is used as lower-bound validity, and Timeout defines the span to the upper-bound. + Timeout uint64 +} + +func (ed *ExecutingDescriptor) AccessCheck(expiryWindow uint64, initMsgTimestamp uint64) error { + // Check upper-bound invariant, strictly + // (for access-lists we don't afford to check intra-timestamp dependencies) + if ed.Timestamp < initMsgTimestamp { + return fmt.Errorf("message broke timestamp invariant: exec: %d, init: %d, %w", + ed.Timestamp, initMsgTimestamp, ErrConflict) + } + if ed.Timestamp == initMsgTimestamp { + return fmt.Errorf("access-list check does not allow intra-timestamp (%d): %w", ed.Timestamp, ErrConflict) + } + + // Check message expiry + expiryAt := initMsgTimestamp + expiryWindow + if expiryAt < initMsgTimestamp { + return fmt.Errorf("message timestamp too high, overflows: %d, %w", + initMsgTimestamp, ErrConflict) + } + if ed.Timestamp > expiryAt { + return fmt.Errorf("cannot message execute at %d, message expired at %d: %w", + ed.Timestamp, expiryAt, ErrConflict) + } + if ed.Timeout == 0 { + // If no timeout, then just checking the exact execution time was sufficient + return nil + } + + // If a timeout is set, check if executing late is still within the expiry window + if ed.Timestamp+ed.Timeout < ed.Timestamp { + return fmt.Errorf("message timeout too high, overflows: %d, %w", + ed.Timestamp, ErrConflict) + } + if v := ed.Timestamp + ed.Timeout; v > expiryAt { + return fmt.Errorf("cannot execute message at timeout %d, expired at %d: %w", + v, expiryAt, ErrConflict) + } + return nil } type executingDescriptorMarshaling struct { Timestamp hexutil.Uint64 `json:"timestamp"` + Timeout hexutil.Uint64 `json:"timeout,omitempty"` } func (ed ExecutingDescriptor) MarshalJSON() ([]byte, error) { var enc executingDescriptorMarshaling enc.Timestamp = hexutil.Uint64(ed.Timestamp) + enc.Timeout = hexutil.Uint64(ed.Timeout) return json.Marshal(&enc) } @@ -205,6 +288,7 @@ func (ed *ExecutingDescriptor) UnmarshalJSON(input []byte) error { return err } ed.Timestamp = uint64(dec.Timestamp) + ed.Timeout = uint64(dec.Timeout) return nil } @@ -348,3 +432,157 @@ type ManagedEvent struct { ReplaceBlock *BlockReplacement `json:"replaceBlock,omitempty"` DerivationOriginUpdate *eth.BlockRef `json:"derivationOriginUpdate,omitempty"` } + +// MessageChecksum represents a message checksum, as used for access-list checks. +type MessageChecksum common.Hash + +func (mc MessageChecksum) MarshalText() ([]byte, error) { + return common.Hash(mc).MarshalText() +} + +func (mc *MessageChecksum) UnmarshalText(data []byte) error { + return (*common.Hash)(mc).UnmarshalText(data) +} + +func (mc MessageChecksum) String() string { + return common.Hash(mc).String() +} + +// Access represents access to a message, parsed from an access-list +type Access struct { + BlockNumber uint64 + Timestamp uint64 + LogIndex uint32 + ChainID eth.ChainID + Checksum MessageChecksum +} + +// lookupEntry encodes a lookup entry for an access-list +func (acc Access) lookupEntry() common.Hash { + var out common.Hash + out[0] = PrefixLookup + binary.BigEndian.PutUint64(out[4:12], (*uint256.Int)(&acc.ChainID).Uint64()) + binary.BigEndian.PutUint64(out[12:20], acc.BlockNumber) + binary.BigEndian.PutUint64(out[20:28], acc.Timestamp) + binary.BigEndian.PutUint32(out[28:32], acc.LogIndex) + return out +} + +// chainIDExtensionEntry encodes a chainID-extension entry for an access-list +func (acc Access) chainIDExtensionEntry() common.Hash { + var out common.Hash + dat := (*uint256.Int)(&acc.ChainID).Bytes32() + out[0] = PrefixChainIDExtension + copy(out[8:32], dat[0:24]) + return out +} + +type accessMarshaling struct { + BlockNumber hexutil.Uint64 `json:"blockNumber"` + Timestamp hexutil.Uint64 `json:"timestamp"` + LogIndex uint32 `json:"logIndex"` + ChainID eth.ChainID `json:"chainID"` + Checksum MessageChecksum `json:"checksum"` +} + +func (a Access) MarshalJSON() ([]byte, error) { + enc := accessMarshaling{ + BlockNumber: hexutil.Uint64(a.BlockNumber), + Timestamp: hexutil.Uint64(a.Timestamp), + LogIndex: a.LogIndex, + ChainID: a.ChainID, + Checksum: a.Checksum, + } + return json.Marshal(&enc) +} + +func (a *Access) UnmarshalJSON(input []byte) error { + var dec accessMarshaling + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + a.BlockNumber = uint64(dec.BlockNumber) + a.Timestamp = uint64(dec.Timestamp) + a.LogIndex = dec.LogIndex + a.ChainID = dec.ChainID + a.Checksum = dec.Checksum + return nil +} + +const ( + PrefixLookup = 1 + PrefixChainIDExtension = 2 + PrefixChecksum = 3 +) + +var ( + errExpectedEntry = errors.New("expected entry") + errMalformedEntry = errors.New("malformed entry") + errUnexpectedEntryType = errors.New("unexpected entry type") +) + +// ParseAccess parses some access-list entries into an Access, and returns the remaining entries. +// This process can be repeated until no entries are left, to parse an access-list. +func ParseAccess(entries []common.Hash) ([]common.Hash, Access, error) { + if len(entries) == 0 { + return nil, Access{}, errExpectedEntry + } + entry := entries[0] + entries = entries[1:] + if typeByte := entry[0]; typeByte != PrefixLookup { + return nil, Access{}, fmt.Errorf("expected lookup, got entry type %d: %w", + typeByte, errUnexpectedEntryType) + } + if ([3]byte)(entry[1:4]) != ([3]byte{}) { + return nil, Access{}, fmt.Errorf("expected zero bytes: %w", errMalformedEntry) + } + var access Access + access.ChainID = eth.ChainIDFromUInt64(binary.BigEndian.Uint64(entry[4:12])) + access.BlockNumber = binary.BigEndian.Uint64(entry[12:20]) + access.Timestamp = binary.BigEndian.Uint64(entry[20:28]) + access.LogIndex = binary.BigEndian.Uint32(entry[28:32]) + + if len(entries) == 0 { + return nil, Access{}, errExpectedEntry + } + entry = entries[0] + entries = entries[1:] + if typeByte := entry[0]; typeByte == PrefixChainIDExtension { + if ([7]byte)(entry[1:8]) != ([7]byte{}) { + return nil, Access{}, fmt.Errorf("expected zero bytes") + } + // The lower 8 bytes is set to the uint64 in the first entry. + // The upper 24 bytes are set with this extension entry. + chIDBytes32 := access.ChainID.Bytes32() + copy(chIDBytes32[0:24], entry[8:32]) + access.ChainID = eth.ChainIDFromBytes32(chIDBytes32) + if len(entries) == 0 { + return nil, Access{}, errExpectedEntry + } + entry = entries[0] + entries = entries[1:] + } + if typeByte := entry[0]; typeByte != PrefixChecksum { + return nil, Access{}, fmt.Errorf("expected checksum, got entry type %d: %w", + typeByte, errUnexpectedEntryType) + } + access.Checksum = MessageChecksum(entry) + return entries, access, nil +} + +func EncodeAccessList(accesses []Access) []common.Hash { + out := make([]common.Hash, 0, len(accesses)*2) + for _, acc := range accesses { + out = append(out, acc.lookupEntry()) + + if !(*uint256.Int)(&acc.ChainID).IsUint64() { + out = append(out, acc.chainIDExtensionEntry()) + } + + if acc.Checksum[0] != PrefixChecksum { + panic("invalid checksum entry") + } + out = append(out, common.Hash(acc.Checksum)) + } + return out +} diff --git a/op-supervisor/supervisor/types/types_test.go b/op-supervisor/supervisor/types/types_test.go index bc01fa63ef..3c9d018dbc 100644 --- a/op-supervisor/supervisor/types/types_test.go +++ b/op-supervisor/supervisor/types/types_test.go @@ -1,14 +1,22 @@ package types import ( + "encoding/binary" "encoding/json" + "fmt" "math/big" + "math/rand" + "strings" "testing" - "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/stretchr/testify/require" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + ethTypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/ethereum-optimism/optimism/op-service/eth" ) func FuzzRoundtripIdentifierJSONMarshal(f *testing.F) { @@ -38,3 +46,506 @@ func FuzzRoundtripIdentifierJSONMarshal(f *testing.F) { require.Equal(t, id.ChainID, dec.ChainID) }) } + +func TestChainIndex(t *testing.T) { + var x ChainIndex + require.NoError(t, json.Unmarshal([]byte(`"1"`), &x)) + require.Equal(t, ChainIndex(1), x) + data, err := json.Marshal(x) + require.NoError(t, err) + require.Equal(t, `"1"`, string(data)) + + require.NoError(t, json.Unmarshal([]byte(`"4294967295"`), &x)) + require.Equal(t, ChainIndex(0xff_ff_ff_ff), x) + data, err = json.Marshal(x) + require.NoError(t, err) + require.Equal(t, `"4294967295"`, string(data)) + + require.ErrorContains(t, json.Unmarshal([]byte(`"-1"`), &x), "invalid") + require.ErrorContains(t, json.Unmarshal([]byte(`"4294967296"`), &x), "out of range") +} + +func TestHashing(t *testing.T) { + keccak256 := func(name string, parts ...[]byte) (h common.Hash) { + t.Logf("%s = H(", name) + for _, p := range parts { + t.Logf(" %x,", p) + } + t.Logf(")") + h = crypto.Keccak256Hash(parts...) + t.Logf("%s = %s", name, h) + return h + } + id := Identifier{ + Origin: common.HexToAddress("0xe0e1e2e3e4e5e6e7e8e9f0f1f2f3f4f5f6f7f8f9"), + BlockNumber: 0xa1a2_a3a4_a5a6_a7a8, + LogIndex: 0xb1b2_b3b4, + Timestamp: 0xc1c2_c3c4_c5c6_c7c8, + ChainID: eth.ChainIDFromUInt64(0xd1d2_d3d4_d5d6_d7d8), + } + payloadHash := keccak256("payloadHash", []byte("example payload")) // aka msgHash + logHash := keccak256("logHash", id.Origin[:], payloadHash[:]) + x := PayloadHashToLogHash(payloadHash, id.Origin) + require.Equal(t, logHash, x, "check op-supervisor version of log-hashing matches intermediate value") + + var idPacked []byte + idPacked = append(idPacked, make([]byte, 12)...) + idPacked = binary.BigEndian.AppendUint64(idPacked, id.BlockNumber) + idPacked = binary.BigEndian.AppendUint64(idPacked, id.Timestamp) + idPacked = binary.BigEndian.AppendUint32(idPacked, id.LogIndex) + t.Logf("idPacked: %x", idPacked) + + idLogHash := keccak256("idLogHash", logHash[:], idPacked) + chainID := id.ChainID.Bytes32() + bareChecksum := keccak256("bareChecksum", idLogHash[:], chainID[:]) + + checksum := bareChecksum + checksum[0] = 0x03 + t.Logf("Checksum: %s", checksum) +} + +var ( + testOrigin = common.HexToAddress("0xe0e1e2e3e4e5e6e7e8e9f0f1f2f3f4f5f6f7f8f9") + testBlockNumber = uint64(0xa1a2_a3a4_a5a6_a7a8) + testLogIndex = uint32(0xb1b2_b3b4) + testTimestamp = uint64(0xc1c2_c3c4_c5c6_c7c8) + testChainID = eth.ChainIDFromUInt64(0xd1d2_d3d4_d5d6_d7d8) + testPayload = []byte("example payload") + testMsgHash = common.HexToHash("0x8017559a85b12c04b14a1a425d53486d1015f833714a09bd62f04152a7e2ae9b") + testLogHash = common.HexToHash("0xf9ed05990c887d3f86718aabd7e940faaa75d6a5cd44602e89642586ce85f2aa") + testChecksum = MessageChecksum(common.HexToHash("0x03749e87fd7789575de9906569deb05aaf220dc4cfab3d8abbfd34a2e1d7d357")) + testLookupEntry = common.HexToHash("0x01000000d1d2d3d4d5d6d7d8a1a2a3a4a5a6a7a8c1c2c3c4c5c6c7c8b1b2b3b4") +) + +func TestMessage(t *testing.T) { + msg := Message{ + Identifier: Identifier{ + Origin: testOrigin, + BlockNumber: testBlockNumber, + LogIndex: testLogIndex, + Timestamp: testTimestamp, + ChainID: testChainID, + }, + PayloadHash: testMsgHash, + } + t.Run("checksum", func(t *testing.T) { + require.Equal(t, testChecksum, msg.Checksum()) + }) + t.Run("json roundtrip", func(t *testing.T) { + data, err := json.Marshal(msg) + require.NoError(t, err) + var out Message + require.NoError(t, json.Unmarshal(data, &out)) + require.Equal(t, msg, out) + }) +} + +func TestChecksumArgs(t *testing.T) { + args := ChecksumArgs{ + BlockNumber: testBlockNumber, + LogIndex: testLogIndex, + Timestamp: testTimestamp, + ChainID: testChainID, + LogHash: testLogHash, + } + t.Run("checksum", func(t *testing.T) { + require.Equal(t, testChecksum, args.Checksum()) + }) + t.Run("as query", func(t *testing.T) { + q := args.Query() + require.Equal(t, testBlockNumber, q.BlockNum) + require.Equal(t, testTimestamp, q.Timestamp) + require.Equal(t, testLogIndex, q.LogIdx) + require.Equal(t, testChecksum, q.Checksum) + }) + t.Run("as access", func(t *testing.T) { + acc := args.Access() + require.Equal(t, testBlockNumber, acc.BlockNumber) + require.Equal(t, testTimestamp, acc.Timestamp) + require.Equal(t, testLogIndex, acc.LogIndex) + require.Equal(t, testChainID, acc.ChainID) + require.Equal(t, testChecksum, acc.Checksum) + }) +} + +func TestIdentifier(t *testing.T) { + id := Identifier{ + Origin: testOrigin, + BlockNumber: testBlockNumber, + LogIndex: testLogIndex, + Timestamp: testTimestamp, + ChainID: testChainID, + } + t.Run("json roundtrip", func(t *testing.T) { + data, err := json.Marshal(id) + require.NoError(t, err) + var out Identifier + require.NoError(t, json.Unmarshal(data, &out)) + require.Equal(t, id, out) + }) +} + +func TestSafetyLevel(t *testing.T) { + for _, lvl := range []SafetyLevel{ + Finalized, + CrossSafe, + LocalSafe, + CrossUnsafe, + LocalUnsafe, + Invalid, + } { + upper := strings.ToUpper(lvl.String()) + var x SafetyLevel + require.ErrorContains(t, json.Unmarshal([]byte(fmt.Sprintf("%q", upper)), &x), "unrecognized", "case sensitive") + require.NoError(t, json.Unmarshal([]byte(fmt.Sprintf("%q", lvl.String())), &x)) + dat, err := json.Marshal(x) + require.NoError(t, err) + require.Equal(t, fmt.Sprintf("%q", lvl.String()), string(dat)) + } + var x SafetyLevel + require.ErrorContains(t, json.Unmarshal([]byte(`""`), &x), "unrecognized", "empty") + require.ErrorContains(t, json.Unmarshal([]byte(`"foobar"`), &x), "unrecognized", "other") +} + +type execDescrTestCase struct { + name string + ed ExecutingDescriptor + expiryWindow uint64 + initMsgTimestamp uint64 + errStr string // empty if no error +} + +func TestExecutingDescriptorAccessCheck(t *testing.T) { + testCases := []execDescrTestCase{ + { + name: "success", + ed: ExecutingDescriptor{ + Timestamp: 3, + Timeout: 0, + }, + expiryWindow: 10, + initMsgTimestamp: 2, + }, + { + name: "future exec", + ed: ExecutingDescriptor{ + Timestamp: 3, + Timeout: 0, + }, + expiryWindow: 10, + initMsgTimestamp: 4, + errStr: "broke timestamp invariant", + }, + { + name: "access-list checks are extra strict, don't allow intra-timestamp", + ed: ExecutingDescriptor{ + Timestamp: 3, + Timeout: 0, + }, + expiryWindow: 10, + initMsgTimestamp: 3, + errStr: "not allow intra-timestamp", + }, + { + name: "attempt init-msg timestamp overflow", + ed: ExecutingDescriptor{ + Timestamp: (^uint64(0)) - 2, + Timeout: 0, + }, + expiryWindow: 10, + initMsgTimestamp: (^uint64(0)) - 3, + errStr: "overflow", + }, + { + name: "expired", + ed: ExecutingDescriptor{ + Timestamp: 100, + Timeout: 0, + }, + expiryWindow: 10, + initMsgTimestamp: 89, + errStr: "expired", + }, + { + name: "timeout overflow", + ed: ExecutingDescriptor{ + Timestamp: 100, + Timeout: (^uint64(0)) - 3, + }, + expiryWindow: 10, + initMsgTimestamp: 99, + errStr: "overflow", + }, + { + name: "timeout, valid at exec timestamp, but not shortly after", + ed: ExecutingDescriptor{ + Timestamp: 100, + Timeout: 10, //timeout asks for 100+10=110 + }, + expiryWindow: 10, // valid till 95+10 = 105 + initMsgTimestamp: 95, + errStr: "timeout", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.ed.AccessCheck(tc.expiryWindow, tc.initMsgTimestamp) + if tc.errStr == "" { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, tc.errStr) + } + }) + } +} + +func TestPayloadHashToLogHash(t *testing.T) { + logHash := PayloadHashToLogHash(testMsgHash, testOrigin) + require.Equal(t, testLogHash, logHash) +} + +func TestLogToMessagePayload(t *testing.T) { + payload := LogToMessagePayload(ðTypes.Log{ + Data: testPayload, + }) + require.Equal(t, hexutil.Encode(testPayload), hexutil.Encode(payload)) + + t.Run("1 topic", func(t *testing.T) { + v := LogToMessagePayload(ðTypes.Log{ + Data: []byte(`foobar`), + Topics: []common.Hash{ + crypto.Keccak256Hash([]byte(`topic0`)), + }, + }) + expected := make([]byte, 0) + expected = append(expected, crypto.Keccak256([]byte(`topic0`))...) + expected = append(expected, []byte(`foobar`)...) + require.Equal(t, expected, v) + }) + + t.Run("4 topics", func(t *testing.T) { + v := LogToMessagePayload(ðTypes.Log{ + Data: []byte(`foobar`), + Topics: []common.Hash{ + crypto.Keccak256Hash([]byte(`topic0`)), + crypto.Keccak256Hash([]byte(`topic1`)), + crypto.Keccak256Hash([]byte(`topic2`)), + crypto.Keccak256Hash([]byte(`topic3`)), + }, + }) + expected := make([]byte, 0) + expected = append(expected, crypto.Keccak256([]byte(`topic0`))...) + expected = append(expected, crypto.Keccak256([]byte(`topic1`))...) + expected = append(expected, crypto.Keccak256([]byte(`topic2`))...) + expected = append(expected, crypto.Keccak256([]byte(`topic3`))...) + expected = append(expected, []byte(`foobar`)...) + require.Equal(t, expected, v) + }) +} + +func TestAccess(t *testing.T) { + acc := Access{ + BlockNumber: testBlockNumber, + Timestamp: testTimestamp, + LogIndex: testLogIndex, + ChainID: testChainID, + Checksum: MessageChecksum(testChecksum), + } + t.Run("json roundtrip", func(t *testing.T) { + data, err := json.Marshal(acc) + require.NoError(t, err) + var out Access + require.NoError(t, json.Unmarshal(data, &out)) + require.Equal(t, acc, out) + }) +} + +func TestParseAccess(t *testing.T) { + t.Run("empty", func(t *testing.T) { + _, _, err := ParseAccess(nil) + require.ErrorIs(t, err, errExpectedEntry) + }) + t.Run("unexpected 0 type", func(t *testing.T) { + _, _, err := ParseAccess([]common.Hash{ + {0: 0x00}, + }) + require.ErrorIs(t, err, errUnexpectedEntryType) + require.ErrorContains(t, err, "expected lookup") + }) + t.Run("unexpected arbitrary type", func(t *testing.T) { + _, _, err := ParseAccess([]common.Hash{ + {0: 10}, + }) + require.ErrorIs(t, err, errUnexpectedEntryType) + require.ErrorContains(t, err, "expected lookup") + }) + t.Run("unexpected non-zero padding", func(t *testing.T) { + _, _, err := ParseAccess([]common.Hash{ + {0: PrefixLookup, 1: 0x01}, // valid lookup prefix byte, but non-zero value in padding area + }) + require.ErrorIs(t, err, errMalformedEntry) + require.ErrorContains(t, err, "expected zero bytes") + }) + t.Run("incomplete", func(t *testing.T) { + _, _, err := ParseAccess([]common.Hash{ + {0: PrefixLookup}, // valid lookup, but no checksum after + }) + require.ErrorIs(t, err, errExpectedEntry) + }) + t.Run("unexpected 0 type after checksum", func(t *testing.T) { + _, _, err := ParseAccess([]common.Hash{ + {0: PrefixLookup}, + {0: 0}, + }) + require.ErrorIs(t, err, errUnexpectedEntryType) + }) + t.Run("unexpected lookup repeat", func(t *testing.T) { + _, _, err := ParseAccess([]common.Hash{ + {0: PrefixLookup}, + {0: PrefixLookup}, + }) + require.ErrorIs(t, err, errUnexpectedEntryType) + }) + t.Run("unexpected arbitrary type after checksum", func(t *testing.T) { + _, _, err := ParseAccess([]common.Hash{ + {0: PrefixLookup}, + {0: 10}, // unexpected type byte + }) + require.ErrorIs(t, err, errUnexpectedEntryType) + }) + t.Run("valid but zero", func(t *testing.T) { + remaining, acc, err := ParseAccess([]common.Hash{ + {0: PrefixLookup}, // valid lookup entry + {0: PrefixChecksum}, // valid checksum entry + }) + require.NoError(t, err) + require.Equal(t, Access{ + BlockNumber: 0, + Timestamp: 0, + LogIndex: 0, + ChainID: eth.ChainID{}, + Checksum: MessageChecksum{0: PrefixChecksum}, + }, acc) + require.Empty(t, remaining) + }) + t.Run("valid", func(t *testing.T) { + acc := Access{ + BlockNumber: testBlockNumber, + Timestamp: testTimestamp, + LogIndex: testLogIndex, + ChainID: testChainID, + Checksum: MessageChecksum(testChecksum), + } + remaining, parsed, err := ParseAccess([]common.Hash{ + testLookupEntry, + common.Hash(acc.Checksum), + }) + require.NoError(t, err) + require.Equal(t, acc, parsed) + require.Empty(t, remaining) + }) + t.Run("repeat", func(t *testing.T) { + acc := Access{ + BlockNumber: testBlockNumber, + Timestamp: testTimestamp, + LogIndex: testLogIndex, + ChainID: testChainID, + Checksum: MessageChecksum(testChecksum), + } + remaining, parsed, err := ParseAccess([]common.Hash{ + testLookupEntry, + common.Hash(acc.Checksum), + testLookupEntry, + common.Hash(acc.Checksum), + }) + require.NoError(t, err) + require.Equal(t, acc, parsed) + require.Len(t, remaining, 2) + remaining2, parsed2, err := ParseAccess(remaining) + require.NoError(t, err) + require.Equal(t, acc, parsed2) + require.Empty(t, remaining2) + }) + t.Run("with chainID extension", func(t *testing.T) { + acc := Access{ + BlockNumber: testBlockNumber, + Timestamp: testTimestamp, + LogIndex: testLogIndex, + ChainID: eth.ChainIDFromBytes32([32]byte{0: 7, 31: 10}), + Checksum: MessageChecksum(testChecksum), + } + remaining, parsed, err := ParseAccess([]common.Hash{ + acc.lookupEntry(), + acc.chainIDExtensionEntry(), + common.Hash(acc.Checksum), + }) + require.NoError(t, err) + require.Equal(t, acc, parsed) + require.Empty(t, remaining) + }) +} + +func TestEncodeAccessList(t *testing.T) { + acc := Access{ + BlockNumber: testBlockNumber, + Timestamp: testTimestamp, + LogIndex: testLogIndex, + ChainID: testChainID, + Checksum: MessageChecksum(testChecksum), + } + t.Run("valid single", func(t *testing.T) { + accList := EncodeAccessList([]Access{acc}) + require.Len(t, accList, 2) + require.Equal(t, testLookupEntry, accList[0]) + require.Equal(t, common.Hash(testChecksum), accList[1]) + _, result, err := ParseAccess(accList) + require.NoError(t, err) + require.Equal(t, acc, result, "roundtrip") + }) + t.Run("valid repeat", func(t *testing.T) { + accList := EncodeAccessList([]Access{ + acc, + acc, + }) + require.Len(t, accList, 4) + require.Equal(t, testLookupEntry, accList[0]) + require.Equal(t, common.Hash(testChecksum), accList[1]) + require.Equal(t, testLookupEntry, accList[2]) + require.Equal(t, common.Hash(testChecksum), accList[3]) + }) + t.Run("roundtrip", func(t *testing.T) { + accObjects := make([]Access, 0) + rng := rand.New(rand.NewSource(1234)) + randB32 := func() (out [32]byte) { + rng.Read(out[:]) + return + } + // test a big random access-list + count := 200 + for i := 0; i < count; i++ { + chainID := eth.ChainIDFromBytes32(randB32()) + if rng.Intn(5) < 2 { // don't make them all full random bytes32 + chainID = eth.ChainIDFromUInt64(rng.Uint64()) + } + checksum := randB32() + checksum[0] = PrefixChecksum + accObjects = append(accObjects, Access{ + BlockNumber: rng.Uint64(), + Timestamp: rng.Uint64(), + LogIndex: rng.Uint32(), + ChainID: chainID, + Checksum: checksum, + }) + } + list := EncodeAccessList(accObjects) + var result []Access + for i := 0; i < count && len(list) > 0; i++ { + remaining, v, err := ParseAccess(list) + require.NoError(t, err) + result = append(result, v) + list = remaining + } + require.Empty(t, list, "need to exhaust entries, expecting to be done") + require.Equal(t, accObjects, result, "roundtrip of random entries should work") + }) +} diff --git a/op-test-sequencer/sequencer/backend/work/iface.go b/op-test-sequencer/sequencer/backend/work/iface.go index b74e86c343..be51d09883 100644 --- a/op-test-sequencer/sequencer/backend/work/iface.go +++ b/op-test-sequencer/sequencer/backend/work/iface.go @@ -8,6 +8,7 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum-optimism/optimism/op-service/eth" + opsigner "github.com/ethereum-optimism/optimism/op-service/signer" "github.com/ethereum-optimism/optimism/op-test-sequencer/metrics" "github.com/ethereum-optimism/optimism/op-test-sequencer/sequencer/seqtypes" ) @@ -28,7 +29,7 @@ type Block interface { type SignedBlock interface { Block - VerifySignature() error + opsigner.SignedObject } // BuildJob provides access to the building work of a single protocol block. diff --git a/op-test-sequencer/sequencer/backend/work/sequencers/fullseq/sequencer_test.go b/op-test-sequencer/sequencer/backend/work/sequencers/fullseq/sequencer_test.go index d791e16c1a..ae1de1e3ff 100644 --- a/op-test-sequencer/sequencer/backend/work/sequencers/fullseq/sequencer_test.go +++ b/op-test-sequencer/sequencer/backend/work/sequencers/fullseq/sequencer_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "github.com/ethereum-optimism/optimism/op-service/eth" + opsigner "github.com/ethereum-optimism/optimism/op-service/signer" "github.com/ethereum-optimism/optimism/op-service/testlog" "github.com/ethereum-optimism/optimism/op-test-sequencer/metrics" "github.com/ethereum-optimism/optimism/op-test-sequencer/sequencer/backend/work" @@ -40,7 +41,7 @@ func (m *mockSignedBlock) String() string { return "mock signed block" } -func (m *mockSignedBlock) VerifySignature() error { +func (m *mockSignedBlock) VerifySignature(auth opsigner.Authenticator) error { return nil } diff --git a/op-validator/cmd/main.go b/op-validator/cmd/main.go index 8ff7e20501..a4c364bd46 100644 --- a/op-validator/cmd/main.go +++ b/op-validator/cmd/main.go @@ -33,6 +33,7 @@ func main() { Subcommands: []*cli.Command{ versionCmd(validations.VersionV180), versionCmd(validations.VersionV200), + versionCmd(validations.VersionV300), }, }, } diff --git a/op-validator/pkg/validations/addresses.go b/op-validator/pkg/validations/addresses.go index 57f8fef9d9..cda48b65ae 100644 --- a/op-validator/pkg/validations/addresses.go +++ b/op-validator/pkg/validations/addresses.go @@ -9,14 +9,25 @@ import ( const ( VersionV180 = "v1.8.0" VersionV200 = "v2.0.0" + VersionV300 = "v3.0.0" ) var addresses = map[uint64]map[string]common.Address{ + 1: { + // Bootstrapped on 03/07/2025 using OP Deployer. + VersionV180: common.HexToAddress("0x37fb5b21750d0e08a992350574bd1c24f4bcedf9"), + // Bootstrapped on 03/07/2025 using OP Deployer. + VersionV200: common.HexToAddress("0x12a9e38628e5a5b24d18b1956ed68a24fe4e3dc0"), + // Bootstrapped on 03/10/2025 using OP Deployer. + VersionV300: common.HexToAddress("0x1b8f0e76bb49b60dffbe3b847e25d6429ebff3e6"), + }, 11155111: { // Bootstrapped on 03/02/2025 using OP Deployer. VersionV180: common.HexToAddress("0x0a5bf8ebb4b177b2dcc6eba933db726a2e2e2b4d"), // Bootstrapped on 03/02/2025 using OP Deployer. VersionV200: common.HexToAddress("0x37739a6b0a3f1e7429499a4ec4a0685439daff5c"), + // Bootstrapped on 03/10/2025 using OP Deployer. + VersionV300: common.HexToAddress("0x5fd6dc82a0cbc0db40d18963d812487772113216"), }, } diff --git a/op-validator/pkg/validations/addresses_test.go b/op-validator/pkg/validations/addresses_test.go index d3967e348f..952b20371b 100644 --- a/op-validator/pkg/validations/addresses_test.go +++ b/op-validator/pkg/validations/addresses_test.go @@ -1,10 +1,25 @@ package validations import ( + "context" + "fmt" + "os" + "regexp" + "slices" "testing" + "time" + + op_e2e "github.com/ethereum-optimism/optimism/op-e2e" + + "github.com/ethereum-optimism/superchain-registry/validation" + "github.com/ethereum/go-ethereum/rpc" + "github.com/lmittmann/w3" + "github.com/lmittmann/w3/module/eth" + "github.com/lmittmann/w3/w3types" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" + "golang.org/x/exp/maps" ) func TestValidatorAddress(t *testing.T) { @@ -39,7 +54,7 @@ func TestValidatorAddress(t *testing.T) { { name: "Invalid Version", chainID: 11155111, - version: "v3.0.0", + version: "v99.0.0", want: common.Address{}, expectError: true, }, @@ -58,3 +73,143 @@ func TestValidatorAddress(t *testing.T) { }) } } + +func TestAddressValidDeployment(t *testing.T) { + op_e2e.InitParallel(t) + + for _, network := range []string{"mainnet", "sepolia"} { + t.Run(network, func(t *testing.T) { + op_e2e.InitParallel(t) + testStandardVersionNetwork(t, network) + }) + } +} + +// Regex to match version strings, removing op-contracts/ prefix and -rc.* suffix +var cleanVersionRegex = regexp.MustCompile(`^(?:op-contracts/)?(v\d+\.\d+\.\d+)(?:-rc\.\d+)?$`) + +func testStandardVersionNetwork(t *testing.T, network string) { + var rpcURL string + var versions validation.Versions + var chainID uint64 + if network == "mainnet" { + rpcURL = os.Getenv("MAINNET_RPC_URL") + versions = validation.StandardVersionsMainnet + chainID = 1 + } else if network == "sepolia" { + rpcURL = os.Getenv("SEPOLIA_RPC_URL") + versions = validation.StandardVersionsSepolia + chainID = 11155111 + } else { + t.Fatalf("Invalid network: %s", network) + } + + require.NotEmpty(t, rpcURL, "RPC URL is empty") + + // Use maps.keys to ensure the versions are sorted in descending order. + sortedKeys := maps.Keys(versions) + slices.Sort(sortedKeys) + slices.Reverse(sortedKeys) + + for _, semver := range sortedKeys { + // Versions are in descending order, to stop at all versions prior to v1.8.0 since + // they don't have validators. + if string(semver) == "op-contracts/v1.6.0" { + break + } + + version := versions[semver] + + matches := cleanVersionRegex.FindStringSubmatch(string(semver)) + require.Len(t, matches, 2, "Invalid version format: %s", semver) + cleanVersion := matches[1] + + address, err := ValidatorAddress(chainID, cleanVersion) + require.NoError(t, err) + + rpcClient, err := rpc.Dial(rpcURL) + require.NoError(t, err) + + t.Run(string(semver), func(t *testing.T) { + testStandardVersion(t, address, rpcClient, version) + }) + } +} + +func testStandardVersion(t *testing.T, address common.Address, rpcClient *rpc.Client, version validation.VersionConfig) { + type fieldDef struct { + getter string + semver string + } + fields := []fieldDef{ + { + "systemConfigVersion", + version.SystemConfig.Version, + }, + { + "mipsVersion", + version.Mips.Version, + }, + { + "optimismPortalVersion", + version.OptimismPortal.Version, + }, + { + "anchorStateRegistryVersion", + version.AnchorStateRegistry.Version, + }, + { + "delayedWETHVersion", + version.DelayedWeth.Version, + }, + { + "disputeGameFactoryVersion", + version.DisputeGameFactory.Version, + }, + { + "preimageOracleVersion", + version.PreimageOracle.Version, + }, + { + "l1CrossDomainMessengerVersion", + version.L1CrossDomainMessenger.Version, + }, + { + "l1ERC721BridgeVersion", + version.L1ERC721Bridge.Version, + }, + { + "l1StandardBridgeVersion", + version.L1StandardBridge.Version, + }, + { + "optimismMintableERC20FactoryVersion", + version.OptimismMintableERC20Factory.Version, + }, + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + w3c := w3.NewClient(rpcClient) + for _, field := range fields { + fn := w3.MustNewFunc(fmt.Sprintf("%s()", field.getter), "string") + var outBytes []byte + require.NoError( + t, + w3c.CallCtx( + ctx, + eth.Call(&w3types.Message{ + To: &address, + Func: fn, + }, nil, nil).Returns(&outBytes), + ), + "failed to call %s", + field.getter, + ) + + var outVersion string + require.NoError(t, fn.DecodeReturns(outBytes, &outVersion)) + require.Equal(t, field.semver, outVersion) + } +} diff --git a/packages/contracts-bedrock/foundry.toml b/packages/contracts-bedrock/foundry.toml index aafcc6c22b..6526bae095 100644 --- a/packages/contracts-bedrock/foundry.toml +++ b/packages/contracts-bedrock/foundry.toml @@ -12,6 +12,7 @@ snapshots = 'notarealpath' # workaround for foundry#9477 optimizer = true optimizer_runs = 999999 +use_literal_content = true # IMPORTANT: # When adding any new compiler profiles or compilation restrictions, you must @@ -28,6 +29,7 @@ compilation_restrictions = [ { paths = "src/dispute/SuperPermissionedDisputeGame.sol", optimizer_runs = 5000 }, { paths = "src/L1/OPContractsManager.sol", optimizer_runs = 5000 }, { paths = "src/L1/StandardValidator.sol", optimizer_runs = 5000 }, + { paths = "src/L1/OptimismPortal2.sol", optimizer_runs = 5000 } ] extra_output = ['devdoc', 'userdoc', 'metadata', 'storageLayout'] @@ -62,6 +64,7 @@ fs_permissions = [ { access='read', path='./kout-deployment' }, { access='read', path='./test/fixtures' }, { access='read', path='./lib/superchain-registry/superchain/configs/' }, + { access='read', path='./lib/superchain-registry/validation/standard/' }, ] # 5159 error code is selfdestruct error code @@ -138,8 +141,11 @@ additional_compiler_profiles = [ compilation_restrictions = [ { paths = "src/dispute/FaultDisputeGame.sol", optimizer_runs = 0 }, { paths = "src/dispute/PermissionedDisputeGame.sol", optimizer_runs = 0 }, + { paths = "src/dispute/SuperFaultDisputeGame.sol", optimizer_runs = 0 }, + { paths = "src/dispute/SuperPermissionedDisputeGame.sol", optimizer_runs = 0 }, { paths = "src/L1/OPContractsManager.sol", optimizer_runs = 0 }, { paths = "src/L1/StandardValidator.sol", optimizer_runs = 0 }, + { paths = "src/L1/OptimismPortal2.sol", optimizer_runs = 0 }, ] ################################################################ diff --git a/packages/contracts-bedrock/interfaces/L1/IETHLockbox.sol b/packages/contracts-bedrock/interfaces/L1/IETHLockbox.sol new file mode 100644 index 0000000000..7f9f5c2d0d --- /dev/null +++ b/packages/contracts-bedrock/interfaces/L1/IETHLockbox.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { ISemver } from "interfaces/universal/ISemver.sol"; +import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; +import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; +import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; + +interface IETHLockbox is IProxyAdminOwnedBase, ISemver { + error ETHLockbox_Unauthorized(); + error ETHLockbox_Paused(); + error ETHLockbox_InsufficientBalance(); + error ETHLockbox_NoWithdrawalTransactions(); + error ETHLockbox_DifferentProxyAdminOwner(); + error ETHLockbox_DifferentSuperchainConfig(); + + event Initialized(uint8 version); + event ETHLocked(IOptimismPortal2 indexed portal, uint256 amount); + event ETHUnlocked(IOptimismPortal2 indexed portal, uint256 amount); + event PortalAuthorized(IOptimismPortal2 indexed portal); + event LockboxAuthorized(IETHLockbox indexed lockbox); + event LiquidityMigrated(IETHLockbox indexed lockbox, uint256 amount); + event LiquidityReceived(IETHLockbox indexed lockbox, uint256 amount); + + function initialize(ISuperchainConfig _superchainConfig, IOptimismPortal2[] calldata _portals) external; + function superchainConfig() external view returns (ISuperchainConfig); + function paused() external view returns (bool); + function authorizedPortals(IOptimismPortal2) external view returns (bool); + function authorizedLockboxes(IETHLockbox) external view returns (bool); + function receiveLiquidity() external payable; + function lockETH() external payable; + function unlockETH(uint256 _value) external; + function authorizePortal(IOptimismPortal2 _portal) external; + function authorizeLockbox(IETHLockbox _lockbox) external; + function migrateLiquidity(IETHLockbox _lockbox) external; + + function __constructor__() external; +} diff --git a/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol b/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol index 37701010a8..0982120868 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol @@ -21,6 +21,7 @@ import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.s import { IL1ERC721Bridge } from "interfaces/L1/IL1ERC721Bridge.sol"; import { IL1StandardBridge } from "interfaces/L1/IL1StandardBridge.sol"; import { IOptimismMintableERC20Factory } from "interfaces/universal/IOptimismMintableERC20Factory.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; interface IOPContractsManagerContractsContainer { function __constructor__( @@ -38,16 +39,20 @@ interface IOPContractsManagerGameTypeAdder { uint256 indexed l2ChainId, GameType indexed gameType, address newDisputeGame, address oldDisputeGame ); - function __constructor__( - IOPContractsManagerContractsContainer _contractsContainer - ) - external; + function __constructor__(IOPContractsManagerContractsContainer _contractsContainer) external; - function addGameType(IOPContractsManager.AddGameInput[] memory _gameConfigs, address _superchainConfig) + function addGameType( + IOPContractsManager.AddGameInput[] memory _gameConfigs, + address _superchainConfig + ) external returns (IOPContractsManager.AddGameOutput[] memory); - function updatePrestate(IOPContractsManager.OpChainConfig[] memory _prestateUpdateInputs, address _superchainConfig) external; + function updatePrestate( + IOPContractsManager.OpChainConfig[] memory _prestateUpdateInputs, + address _superchainConfig + ) + external; function contractsContainer() external view returns (IOPContractsManagerContractsContainer); } @@ -55,12 +60,13 @@ interface IOPContractsManagerGameTypeAdder { interface IOPContractsManagerDeployer { event Deployed(uint256 indexed l2ChainId, address indexed deployer, bytes deployOutput); - function __constructor__( - IOPContractsManagerContractsContainer _contractsContainer - ) - external; + function __constructor__(IOPContractsManagerContractsContainer _contractsContainer) external; - function deploy(IOPContractsManager.DeployInput memory _input, address _superchainConfig, address _deployer) + function deploy( + IOPContractsManager.DeployInput memory _input, + address _superchainConfig, + address _deployer + ) external returns (IOPContractsManager.DeployOutput memory); @@ -70,17 +76,13 @@ interface IOPContractsManagerDeployer { interface IOPContractsManagerUpgrader { event Upgraded(uint256 indexed l2ChainId, address indexed systemConfig, address indexed upgrader); - function __constructor__( - IOPContractsManagerContractsContainer _contractsContainer - ) - external; + function __constructor__(IOPContractsManagerContractsContainer _contractsContainer) external; function upgrade(IOPContractsManager.OpChainConfig[] memory _opChainConfigs) external; function contractsContainer() external view returns (IOPContractsManagerContractsContainer); } - interface IOPContractsManager { // -------- Structs -------- @@ -123,6 +125,7 @@ interface IOPContractsManager { IOptimismMintableERC20Factory optimismMintableERC20FactoryProxy; IL1StandardBridge l1StandardBridgeProxy; IL1CrossDomainMessenger l1CrossDomainMessengerProxy; + IETHLockbox ethLockboxProxy; // Fault proof contracts below. IOptimismPortal2 optimismPortalProxy; IDisputeGameFactory disputeGameFactoryProxy; @@ -156,6 +159,7 @@ interface IOPContractsManager { address protocolVersionsImpl; address l1ERC721BridgeImpl; address optimismPortalImpl; + address ethLockboxImpl; address systemConfigImpl; address optimismMintableERC20FactoryImpl; address l1CrossDomainMessengerImpl; @@ -303,3 +307,27 @@ interface IOPContractsManager { function setRC(bool _isRC) external; } + +/// @notice Minimal interface only used for calling `implementations()` method but without retrieving the ETHLockbox +/// on it, since the OPCM contracts already deployed on mainnet don't have it. +/// @dev Only used for testing. +interface IOPCMImplementationsWithoutLockbox { + /// @notice The implementation contracts for the OP Stack, without the newly added ETHLockbox. + struct Implementations { + address superchainConfigImpl; + address protocolVersionsImpl; + address l1ERC721BridgeImpl; + address optimismPortalImpl; + address systemConfigImpl; + address optimismMintableERC20FactoryImpl; + address l1CrossDomainMessengerImpl; + address l1StandardBridgeImpl; + address disputeGameFactoryImpl; + address anchorStateRegistryImpl; + address delayedWETHImpl; + address mipsImpl; + } + + /// @notice Returns the implementation contracts without the ETHLockbox. + function implementations() external view returns (Implementations memory); +} diff --git a/packages/contracts-bedrock/interfaces/L1/IOPPrestateUpdater.sol b/packages/contracts-bedrock/interfaces/L1/IOPPrestateUpdater.sol deleted file mode 100644 index 7456fed523..0000000000 --- a/packages/contracts-bedrock/interfaces/L1/IOPPrestateUpdater.sol +++ /dev/null @@ -1,142 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -// Libraries -import { GameType } from "src/dispute/lib/Types.sol"; - -// Interfaces -import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; -import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; -import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; -import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; -import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; - -interface IOPPrestateUpdater { - // -------- Constants and Variables -------- - - function version() external pure returns (string memory); - - /// @notice Address of the SuperchainConfig contract shared by all chains. - function superchainConfig() external view returns (ISuperchainConfig); - - /// @notice Address of the ProtocolVersions contract shared by all chains. - function protocolVersions() external view returns (IProtocolVersions); - - /// @notice Address of the ProxyAdmin contract shared by all chains. - function superchainProxyAdmin() external view returns (IProxyAdmin); - - /// @notice L1 smart contracts release deployed by this version of OPCM. This is used in opcm to signal which - /// version of the L1 smart contracts is deployed. It takes the format of `op-contracts/vX.Y.Z`. - function l1ContractsRelease() external view returns (string memory); - - // -------- Events -------- - - /// @notice Emitted when a new OP Stack chain is deployed. - /// @param l2ChainId Chain ID of the new chain. - /// @param deployer Address that deployed the chain. - /// @param deployOutput ABI-encoded output of the deployment. - event Deployed(uint256 indexed l2ChainId, address indexed deployer, bytes deployOutput); - - /// @notice Emitted when a chain is upgraded - /// @param systemConfig Address of the chain's SystemConfig contract - /// @param upgrader Address that initiated the upgrade - event Upgraded(uint256 indexed l2ChainId, ISystemConfig indexed systemConfig, address indexed upgrader); - - /// @notice Emitted when a new game type is added to a chain - /// @param l2ChainId Chain ID of the chain - /// @param gameType Type of the game being added - /// @param newDisputeGame Address of the deployed dispute game - /// @param oldDisputeGame Address of the old dispute game - event GameTypeAdded(uint256 indexed l2ChainId, GameType indexed gameType, IDisputeGame newDisputeGame, IDisputeGame oldDisputeGame); - - // -------- Errors -------- - - error BytesArrayTooLong(); - error DeploymentFailed(); - error EmptyInitcode(); - error IdentityPrecompileCallFailed(); - error NotABlueprint(); - error ReservedBitsSet(); - error UnexpectedPreambleData(bytes data); - error UnsupportedERCVersion(uint8 version); - error OnlyUpgradeController(); - error PrestateNotSet(); - - /// @notice Thrown when an address is the zero address. - error AddressNotFound(address who); - - /// @notice Throw when a contract address has no code. - error AddressHasNoCode(address who); - - /// @notice Thrown when a release version is already set. - error AlreadyReleased(); - - /// @notice Thrown when an invalid `l2ChainId` is provided to `deploy`. - error InvalidChainId(); - - /// @notice Thrown when a role's address is not valid. - error InvalidRoleAddress(string role); - - /// @notice Thrown when the latest release is not set upon initialization. - error LatestReleaseNotSet(); - - /// @notice Thrown when the starting anchor root is not provided. - error InvalidStartingAnchorRoot(); - - /// @notice Thrown when certain methods are called outside of a DELEGATECALL. - error OnlyDelegatecall(); - - /// @notice Thrown when game configs passed to addGameType are invalid. - error InvalidGameConfigs(); - - /// @notice Thrown when the SuperchainConfig of the chain does not match the SuperchainConfig of this OPCM. - error SuperchainConfigMismatch(ISystemConfig systemConfig); - - error SuperchainProxyAdminMismatch(); - - /// @notice Thrown when a function from the parent (OPCM) is not implemented. - error NotImplemented(); - - /// @notice Thrown when the prestate of a permissioned disputed game is 0. - error PrestateRequired(); - - // -------- Methods -------- - - function __constructor__( - ISuperchainConfig _superchainConfig, - IProtocolVersions _protocolVersions, - IOPContractsManager.Blueprints memory _blueprints - ) - external; - - function deploy(IOPContractsManager.DeployInput calldata _input) external returns (IOPContractsManager.DeployOutput memory); - - /// @notice Upgrades the implementation of all proxies in the specified chains - /// @param _opChainConfigs The chains to upgrade - function upgrade(IOPContractsManager.OpChainConfig[] memory _opChainConfigs) external; - - /// @notice addGameType deploys a new dispute game and links it to the DisputeGameFactory. The inputted _gameConfigs - /// must be added in ascending GameType order. - function addGameType(IOPContractsManager.AddGameInput[] memory _gameConfigs) external returns (IOPContractsManager.AddGameOutput[] memory); - - /// @notice Maps an L2 chain ID to an L1 batch inbox address as defined by the standard - /// configuration's convention. This convention is `versionByte || keccak256(bytes32(chainId))[:19]`, - /// where || denotes concatenation`, versionByte is 0x00, and chainId is a uint256. - /// https://specs.optimism.io/protocol/configurability.html#consensus-parameters - function chainIdToBatchInboxAddress(uint256 _l2ChainId) external pure returns (address); - - /// @notice Returns the blueprint contract addresses. - function blueprints() external view returns (IOPContractsManager.Blueprints memory); - - /// @notice Returns the implementation contract addresses. - function implementations() external view returns (IOPContractsManager.Implementations memory); - - function upgradeController() external view returns (address); - - function isRC() external view returns (bool); - - function setRC(bool _isRC) external; - - function updatePrestate(IOPContractsManager.OpChainConfig[] memory _prestateUpdateInputs) external; -} diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol index bbf12c3fce..673b3c778f 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol @@ -2,48 +2,60 @@ pragma solidity ^0.8.0; import { Types } from "src/libraries/Types.sol"; -import { GameType, Timestamp } from "src/dispute/lib/LibUDT.sol"; +import { GameType } from "src/dispute/lib/LibUDT.sol"; import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; +import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; +import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; -interface IOptimismPortal2 { - error AlreadyFinalized(); - error BadTarget(); - error Blacklisted(); - error CallPaused(); +interface IOptimismPortal2 is IProxyAdminOwnedBase { + error OptimismPortal_Unauthorized(); error ContentLengthMismatch(); error EmptyItem(); - error GasEstimation(); error InvalidDataRemainder(); - error InvalidDisputeGame(); - error InvalidGameType(); error InvalidHeader(); - error InvalidMerkleProof(); - error InvalidProof(); - error LargeCalldata(); - error NonReentrant(); + error ReinitializableBase_ZeroInitVersion(); + error OptimismPortal_AlreadyFinalized(); + error OptimismPortal_BadTarget(); + error OptimismPortal_CallPaused(); + error OptimismPortal_CalldataTooLarge(); + error OptimismPortal_GasEstimation(); + error OptimismPortal_GasLimitTooLow(); + error OptimismPortal_ImproperDisputeGame(); + error OptimismPortal_InvalidDisputeGame(); + error OptimismPortal_InvalidMerkleProof(); + error OptimismPortal_InvalidOutputRootProof(); + error OptimismPortal_InvalidProofTimestamp(); + error OptimismPortal_InvalidRootClaim(); + error OptimismPortal_NoReentrancy(); + error OptimismPortal_ProofNotOldEnough(); + error OptimismPortal_Unproven(); + error OptimismPortal_InvalidOutputRootIndex(); + error OptimismPortal_InvalidSuperRootProof(); + error OptimismPortal_InvalidOutputRootChainId(); + error OptimismPortal_WrongProofMethod(); + error OptimismPortal_MigratingToSameRegistry(); + error Encoding_EmptySuperRoot(); + error Encoding_InvalidSuperRootVersion(); error OutOfGas(); - error ProposalNotValidated(); - error SmallGasLimit(); - error Unauthorized(); error UnexpectedList(); error UnexpectedString(); - error Unproven(); - error LegacyGame(); - event DisputeGameBlacklisted(IDisputeGame indexed disputeGame); event Initialized(uint8 version); - event RespectedGameTypeSet(GameType indexed newGameType, Timestamp indexed updatedAt); event TransactionDeposited(address indexed from, address indexed to, uint256 indexed version, bytes opaqueData); event WithdrawalFinalized(bytes32 indexed withdrawalHash, bool success); event WithdrawalProven(bytes32 indexed withdrawalHash, address indexed from, address indexed to); event WithdrawalProvenExtension1(bytes32 indexed withdrawalHash, address indexed proofSubmitter); + event ETHMigrated(address indexed lockbox, uint256 ethBalance); + event PortalMigrated(IETHLockbox oldLockbox, IETHLockbox newLockbox, IAnchorStateRegistry oldAnchorStateRegistry, IAnchorStateRegistry newAnchorStateRegistry); receive() external payable; - function blacklistDisputeGame(IDisputeGame _disputeGame) external; + function anchorStateRegistry() external view returns (IAnchorStateRegistry); + function ethLockbox() external view returns (IETHLockbox); function checkWithdrawal(bytes32 _withdrawalHash, address _proofSubmitter) external view; function depositTransaction( address _to, @@ -54,10 +66,10 @@ interface IOptimismPortal2 { ) external payable; - function disputeGameBlacklist(IDisputeGame) external view returns (bool); function disputeGameFactory() external view returns (IDisputeGameFactory); function disputeGameFinalityDelaySeconds() external view returns (uint256); function donateETH() external payable; + function migrateToSuperRoots(IETHLockbox _newLockbox, IAnchorStateRegistry _newAnchorStateRegistry) external; function finalizeWithdrawalTransaction(Types.WithdrawalTransaction memory _tx) external; function finalizeWithdrawalTransactionExternalProof( Types.WithdrawalTransaction memory _tx, @@ -67,12 +79,13 @@ interface IOptimismPortal2 { function finalizedWithdrawals(bytes32) external view returns (bool); function guardian() external view returns (address); function initialize( - IDisputeGameFactory _disputeGameFactory, ISystemConfig _systemConfig, ISuperchainConfig _superchainConfig, - GameType _initialRespectedGameType + IAnchorStateRegistry _anchorStateRegistry, + IETHLockbox _ethLockbox ) external; + function initVersion() external view returns (uint8); function l2Sender() external view returns (address); function minimumGasLimit(uint64 _byteCount) external pure returns (uint64); function numProofSubmitters(bytes32 _withdrawalHash) external view returns (uint256); @@ -87,19 +100,34 @@ interface IOptimismPortal2 { bytes[] memory _withdrawalProof ) external; + function proveWithdrawalTransaction( + Types.WithdrawalTransaction memory _tx, + IDisputeGame _disputeGameProxy, + uint256 _outputRootIndex, + Types.SuperRootProof memory _superRootProof, + Types.OutputRootProof memory _outputRootProof, + bytes[] memory _withdrawalProof + ) + external; function provenWithdrawals( bytes32, address ) external view - returns (IDisputeGame disputeGameProxy, uint64 timestamp); // nosemgrep + returns (IDisputeGame disputeGameProxy, uint64 timestamp); function respectedGameType() external view returns (GameType); function respectedGameTypeUpdatedAt() external view returns (uint64); - function setRespectedGameType(GameType _gameType) external; function superchainConfig() external view returns (ISuperchainConfig); + function superRootsActive() external view returns (bool); function systemConfig() external view returns (ISystemConfig); + function upgrade( + IAnchorStateRegistry _anchorStateRegistry, + IETHLockbox _ethLockbox + ) + external; function version() external pure returns (string memory); + function migrateLiquidity() external; - function __constructor__(uint256 _proofMaturityDelaySeconds, uint256 _disputeGameFinalityDelaySeconds) external; + function __constructor__(uint256 _proofMaturityDelaySeconds) external; } diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol deleted file mode 100644 index 6dec13a833..0000000000 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol +++ /dev/null @@ -1,107 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import { Types } from "src/libraries/Types.sol"; -import { GameType, Timestamp } from "src/dispute/lib/LibUDT.sol"; -import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; -import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; -import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; -import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import { ConfigType } from "interfaces/L2/IL1BlockInterop.sol"; - -interface IOptimismPortalInterop { - error AlreadyFinalized(); - error BadTarget(); - error Blacklisted(); - error CallPaused(); - error ContentLengthMismatch(); - error EmptyItem(); - error GasEstimation(); - error InvalidDataRemainder(); - error InvalidDisputeGame(); - error InvalidGameType(); - error InvalidHeader(); - error InvalidMerkleProof(); - error InvalidProof(); - error LargeCalldata(); - error NonReentrant(); - error OutOfGas(); - error ProposalNotValidated(); - error SmallGasLimit(); - error Unauthorized(); - error UnexpectedList(); - error UnexpectedString(); - error Unproven(); - error LegacyGame(); - - event DisputeGameBlacklisted(IDisputeGame indexed disputeGame); - event Initialized(uint8 version); - event RespectedGameTypeSet(GameType indexed newGameType, Timestamp indexed updatedAt); - event TransactionDeposited(address indexed from, address indexed to, uint256 indexed version, bytes opaqueData); - event WithdrawalFinalized(bytes32 indexed withdrawalHash, bool success); - event WithdrawalProven(bytes32 indexed withdrawalHash, address indexed from, address indexed to); - event WithdrawalProvenExtension1(bytes32 indexed withdrawalHash, address indexed proofSubmitter); - - receive() external payable; - - function blacklistDisputeGame(IDisputeGame _disputeGame) external; - function checkWithdrawal(bytes32 _withdrawalHash, address _proofSubmitter) external view; - function depositTransaction( - address _to, - uint256 _value, - uint64 _gasLimit, - bool _isCreation, - bytes memory _data - ) - external - payable; - function disputeGameBlacklist(IDisputeGame) external view returns (bool); - function disputeGameFactory() external view returns (IDisputeGameFactory); - function disputeGameFinalityDelaySeconds() external view returns (uint256); - function donateETH() external payable; - function finalizeWithdrawalTransaction(Types.WithdrawalTransaction memory _tx) external; - function finalizeWithdrawalTransactionExternalProof( - Types.WithdrawalTransaction memory _tx, - address _proofSubmitter - ) - external; - function finalizedWithdrawals(bytes32) external view returns (bool); - function guardian() external view returns (address); - function initialize( - IDisputeGameFactory _disputeGameFactory, - ISystemConfig _systemConfig, - ISuperchainConfig _superchainConfig, - GameType _initialRespectedGameType - ) - external; - function l2Sender() external view returns (address); - function minimumGasLimit(uint64 _byteCount) external pure returns (uint64); - function numProofSubmitters(bytes32 _withdrawalHash) external view returns (uint256); - function params() external view returns (uint128 prevBaseFee, uint64 prevBoughtGas, uint64 prevBlockNum); // nosemgrep - function paused() external view returns (bool); - function proofMaturityDelaySeconds() external view returns (uint256); - function proofSubmitters(bytes32, uint256) external view returns (address); - function proveWithdrawalTransaction( - Types.WithdrawalTransaction memory _tx, - uint256 _disputeGameIndex, - Types.OutputRootProof memory _outputRootProof, - bytes[] memory _withdrawalProof - ) - external; - function provenWithdrawals( - bytes32, - address - ) - external - view - returns (IDisputeGame disputeGameProxy, uint64 timestamp); // nosemgrep - function respectedGameType() external view returns (GameType); - function respectedGameTypeUpdatedAt() external view returns (uint64); - function setConfig(ConfigType _type, bytes memory _value) external; - function setRespectedGameType(GameType _gameType) external; - function superchainConfig() external view returns (ISuperchainConfig); - function systemConfig() external view returns (ISystemConfig); - function version() external pure returns (string memory); - - function __constructor__(uint256 _proofMaturityDelaySeconds, uint256 _disputeGameFinalityDelaySeconds) external; -} diff --git a/packages/contracts-bedrock/interfaces/L1/IProxyAdminOwnedBase.sol b/packages/contracts-bedrock/interfaces/L1/IProxyAdminOwnedBase.sol new file mode 100644 index 0000000000..73346bff11 --- /dev/null +++ b/packages/contracts-bedrock/interfaces/L1/IProxyAdminOwnedBase.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IProxyAdminOwnedBase { + function proxyAdminOwner() external view returns (address); +} diff --git a/packages/contracts-bedrock/interfaces/L1/IStandardValidator.sol b/packages/contracts-bedrock/interfaces/L1/IStandardValidator.sol index e1c5b8b44d..1f3dfed5ab 100644 --- a/packages/contracts-bedrock/interfaces/L1/IStandardValidator.sol +++ b/packages/contracts-bedrock/interfaces/L1/IStandardValidator.sol @@ -88,3 +88,22 @@ interface IStandardValidatorV200 is IStandardValidatorBase { address _challenger ) external; } + +interface IStandardValidatorV300 is IStandardValidatorBase { + struct InputV300 { + address proxyAdmin; + address sysCfg; + bytes32 absolutePrestate; + uint256 l2ChainID; + } + + function validate(InputV300 memory _input, bool _allowFailure) external view returns (string memory); + + function __constructor__( + IStandardValidatorBase.ImplementationsBase memory _implementations, + ISuperchainConfig _superchainConfig, + address _l1PAOMultisig, + address _mips, + address _challenger + ) external; +} diff --git a/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol b/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol index e5677516fb..a295e63a10 100644 --- a/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol +++ b/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol @@ -17,17 +17,17 @@ interface ISystemConfig { address l1CrossDomainMessenger; address l1ERC721Bridge; address l1StandardBridge; - address disputeGameFactory; address optimismPortal; address optimismMintableERC20Factory; } + error ReinitializableBase_ZeroInitVersion(); + event ConfigUpdate(uint256 indexed version, UpdateType indexed updateType, bytes data); event Initialized(uint8 version); event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); function BATCH_INBOX_SLOT() external view returns (bytes32); - function DISPUTE_GAME_FACTORY_SLOT() external view returns (bytes32); function L1_CROSS_DOMAIN_MESSENGER_SLOT() external view returns (bytes32); function L1_ERC_721_BRIDGE_SLOT() external view returns (bytes32); function L1_STANDARD_BRIDGE_SLOT() external view returns (bytes32); @@ -54,12 +54,15 @@ interface ISystemConfig { address _unsafeBlockSigner, IResourceMetering.ResourceConfig memory _config, address _batchInbox, - Addresses memory _addresses + Addresses memory _addresses, + uint256 _l2ChainId ) external; + function initVersion() external view returns (uint8); function l1CrossDomainMessenger() external view returns (address addr_); function l1ERC721Bridge() external view returns (address addr_); function l1StandardBridge() external view returns (address addr_); + function l2ChainId() external view returns (uint256); function maximumGasLimit() external pure returns (uint64); function minimumGasLimit() external view returns (uint64); function operatorFeeConstant() external view returns (uint64); @@ -81,6 +84,7 @@ interface ISystemConfig { function startBlock() external view returns (uint256 startBlock_); function transferOwnership(address newOwner) external; // nosemgrep function unsafeBlockSigner() external view returns (address addr_); + function upgrade(uint256 _l2ChainId) external; function version() external pure returns (string memory); function __constructor__() external; diff --git a/packages/contracts-bedrock/interfaces/L1/ISystemConfigInterop.sol b/packages/contracts-bedrock/interfaces/L1/ISystemConfigInterop.sol deleted file mode 100644 index 19cac2b415..0000000000 --- a/packages/contracts-bedrock/interfaces/L1/ISystemConfigInterop.sol +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; -import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; - -interface ISystemConfigInterop { - event ConfigUpdate(uint256 indexed version, ISystemConfig.UpdateType indexed updateType, bytes data); - event Initialized(uint8 version); - event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); - - function BATCH_INBOX_SLOT() external view returns (bytes32); - function DISPUTE_GAME_FACTORY_SLOT() external view returns (bytes32); - function L1_CROSS_DOMAIN_MESSENGER_SLOT() external view returns (bytes32); - function L1_ERC_721_BRIDGE_SLOT() external view returns (bytes32); - function L1_STANDARD_BRIDGE_SLOT() external view returns (bytes32); - function OPTIMISM_MINTABLE_ERC20_FACTORY_SLOT() external view returns (bytes32); - function OPTIMISM_PORTAL_SLOT() external view returns (bytes32); - function START_BLOCK_SLOT() external view returns (bytes32); - function UNSAFE_BLOCK_SIGNER_SLOT() external view returns (bytes32); - function VERSION() external view returns (uint256); - function basefeeScalar() external view returns (uint32); - function batchInbox() external view returns (address addr_); - function batcherHash() external view returns (bytes32); - function blobbasefeeScalar() external view returns (uint32); - function disputeGameFactory() external view returns (address addr_); - function gasLimit() external view returns (uint64); - function eip1559Denominator() external view returns (uint32); - function eip1559Elasticity() external view returns (uint32); - function l1CrossDomainMessenger() external view returns (address addr_); - function l1ERC721Bridge() external view returns (address addr_); - function l1StandardBridge() external view returns (address addr_); - function maximumGasLimit() external pure returns (uint64); - function minimumGasLimit() external view returns (uint64); - function operatorFeeConstant() external view returns (uint64); - function operatorFeeScalar() external view returns (uint32); - function optimismMintableERC20Factory() external view returns (address addr_); - function optimismPortal() external view returns (address addr_); - function overhead() external view returns (uint256); - function owner() external view returns (address); - function renounceOwnership() external; - function resourceConfig() external view returns (IResourceMetering.ResourceConfig memory); - function scalar() external view returns (uint256); - function setBatcherHash(bytes32 _batcherHash) external; - function setGasConfig(uint256 _overhead, uint256 _scalar) external; - function setGasConfigEcotone(uint32 _basefeeScalar, uint32 _blobbasefeeScalar) external; - function setGasLimit(uint64 _gasLimit) external; - function setUnsafeBlockSigner(address _unsafeBlockSigner) external; - function setEIP1559Params(uint32 _denominator, uint32 _elasticity) external; - function setOperatorFeeScalars(uint32 _operatorFeeScalar, uint64 _operatorFeeConstant) external; - function startBlock() external view returns (uint256 startBlock_); - function transferOwnership(address newOwner) external; // nosemgrep - function unsafeBlockSigner() external view returns (address addr_); - - function addDependency(uint256 _chainId) external; - function removeDependency(uint256 _chainId) external; - function dependencyManager() external view returns (address); - function initialize( - address _owner, - uint32 _basefeeScalar, - uint32 _blobbasefeeScalar, - bytes32 _batcherHash, - uint64 _gasLimit, - address _unsafeBlockSigner, - IResourceMetering.ResourceConfig memory _config, - address _batchInbox, - ISystemConfig.Addresses memory _addresses, - address _dependencyManager - ) - external; - function version() external pure returns (string memory); - - function __constructor__() external; -} diff --git a/packages/contracts-bedrock/interfaces/L2/ICrossL2Inbox.sol b/packages/contracts-bedrock/interfaces/L2/ICrossL2Inbox.sol index 8b486cc707..4c6237752c 100644 --- a/packages/contracts-bedrock/interfaces/L2/ICrossL2Inbox.sol +++ b/packages/contracts-bedrock/interfaces/L2/ICrossL2Inbox.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +/// @notice Identifier of a cross chain message. + struct Identifier { address origin; uint256 blockNumber; @@ -9,53 +11,16 @@ struct Identifier { uint256 chainId; } -/// @title ICrossL2Inbox -/// @notice Interface for the CrossL2Inbox contract. interface ICrossL2Inbox { - error ReentrantCall(); - - /// @notice Thrown when the caller is not DEPOSITOR_ACCOUNT when calling `setInteropStart()` - error NotDepositor(); - - /// @notice Thrown when attempting to set interop start when it's already set. - error InteropStartAlreadySet(); - - /// @notice Thrown when a non-written transient storage slot is attempted to be read from. - error NotEntered(); - - /// @notice Thrown when trying to execute a cross chain message on a deposit transaction. error NoExecutingDeposits(); + error NotInAccessList(); + error BlockNumberTooHigh(); + error TimestampTooHigh(); + error LogIndexTooHigh(); event ExecutingMessage(bytes32 indexed msgHash, Identifier id); function version() external view returns (string memory); - /// @notice Returns the interop start timestamp. - /// @return interopStart_ interop start timestamp. - function interopStart() external view returns (uint256 interopStart_); - - /// @notice Returns the origin address of the Identifier. - function origin() external view returns (address); - - /// @notice Returns the block number of the Identifier. - function blockNumber() external view returns (uint256); - - /// @notice Returns the log index of the Identifier. - function logIndex() external view returns (uint256); - - /// @notice Returns the timestamp of the Identifier. - function timestamp() external view returns (uint256); - - /// @notice Returns the chain ID of the Identifier. - function chainId() external view returns (uint256); - - function setInteropStart() external; - - /// @notice Validates a cross chain message on the destination chain - /// and emits an ExecutingMessage event. This function is useful - /// for applications that understand the schema of the _message payload and want to - /// process it in a custom way. - /// @param _id Identifier of the message. - /// @param _msgHash Hash of the message payload to call target with. function validateMessage(Identifier calldata _id, bytes32 _msgHash) external; } diff --git a/packages/contracts-bedrock/interfaces/L2/IDependencySet.sol b/packages/contracts-bedrock/interfaces/L2/IDependencySet.sol deleted file mode 100644 index fb294bee8d..0000000000 --- a/packages/contracts-bedrock/interfaces/L2/IDependencySet.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -/// @title IDependencySet -/// @notice Interface for L1Block with only `isInDependencySet(uint256)` method. -interface IDependencySet { - /// @notice Returns true if the chain associated with input chain ID is in the interop dependency set. - /// Every chain is in the interop dependency set of itself. - /// @param _chainId Input chain ID. - /// @return True if the input chain ID corresponds to a chain in the interop dependency set, and false otherwise. - function isInDependencySet(uint256 _chainId) external view returns (bool); -} diff --git a/packages/contracts-bedrock/interfaces/L2/IL1BlockInterop.sol b/packages/contracts-bedrock/interfaces/L2/IL1BlockInterop.sol deleted file mode 100644 index 53cfc890ca..0000000000 --- a/packages/contracts-bedrock/interfaces/L2/IL1BlockInterop.sol +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -enum ConfigType { - ADD_DEPENDENCY, - REMOVE_DEPENDENCY -} - -interface IL1BlockInterop { - error AlreadyDependency(); - error CantRemovedDependency(); - error DependencySetSizeTooLarge(); - error NotCrossL2Inbox(); - error NotDependency(); - error NotDepositor(); - - event DependencyAdded(uint256 indexed chainId); - event DependencyRemoved(uint256 indexed chainId); - - function DEPOSITOR_ACCOUNT() external pure returns (address addr_); - function baseFeeScalar() external view returns (uint32); - function basefee() external view returns (uint256); - function batcherHash() external view returns (bytes32); - function blobBaseFee() external view returns (uint256); - function blobBaseFeeScalar() external view returns (uint32); - function dependencySetSize() external view returns (uint8); - function depositsComplete() external; - function gasPayingToken() external pure returns (address addr_, uint8 decimals_); - function gasPayingTokenName() external pure returns (string memory name_); - function gasPayingTokenSymbol() external pure returns (string memory symbol_); - function hash() external view returns (bytes32); - function isCustomGasToken() external pure returns (bool is_); - function isDeposit() external view returns (bool isDeposit_); - function isInDependencySet(uint256 _chainId) external view returns (bool); - function l1FeeOverhead() external view returns (uint256); - function l1FeeScalar() external view returns (uint256); - function number() external view returns (uint64); - function operatorFeeScalar() external view returns (uint32); - function operatorFeeConstant() external view returns (uint64); - function sequenceNumber() external view returns (uint64); - function setConfig(ConfigType _type, bytes memory _value) external; - function setL1BlockValues( - uint64 _number, - uint64 _timestamp, - uint256 _basefee, - bytes32 _hash, - uint64 _sequenceNumber, - bytes32 _batcherHash, - uint256 _l1FeeOverhead, - uint256 _l1FeeScalar - ) - external; - function setL1BlockValuesEcotone() external; - function setL1BlockValuesInterop() external; - function setL1BlockValuesIsthmus() external; - function timestamp() external view returns (uint64); - function version() external pure returns (string memory); - - function __constructor__() external; -} diff --git a/packages/contracts-bedrock/interfaces/L2/IL2ToL2CrossDomainMessenger.sol b/packages/contracts-bedrock/interfaces/L2/IL2ToL2CrossDomainMessenger.sol index 58f07a9c92..9bc158d46c 100644 --- a/packages/contracts-bedrock/interfaces/L2/IL2ToL2CrossDomainMessenger.sol +++ b/packages/contracts-bedrock/interfaces/L2/IL2ToL2CrossDomainMessenger.sol @@ -27,9 +27,6 @@ interface IL2ToL2CrossDomainMessenger { /// @notice Thrown when attempting to relay a message whose destination chain is not the chain relaying it. error MessageDestinationNotRelayChain(); - /// @notice Thrown when attempting to relay a message whose target is CrossL2Inbox. - error MessageTargetCrossL2Inbox(); - /// @notice Thrown when attempting to relay a message whose target is L2ToL2CrossDomainMessenger. error MessageTargetL2ToL2CrossDomainMessenger(); @@ -39,12 +36,6 @@ interface IL2ToL2CrossDomainMessenger { /// @notice Thrown when a reentrant call is detected. error ReentrantCall(); - /// @notice Thrown when a call to the target contract during message relay fails. - error TargetCallFailed(); - - /// @notice Thrown when attempting to use a chain ID that is not in the dependency set. - error InvalidChainId(); - /// @notice Emitted whenever a message is sent to a destination /// @param destination Chain ID of the destination chain. /// @param target Target contract or wallet address. diff --git a/packages/contracts-bedrock/interfaces/L2/ISuperchainWETH.sol b/packages/contracts-bedrock/interfaces/L2/ISuperchainWETH.sol index e646807b8d..883252c69a 100644 --- a/packages/contracts-bedrock/interfaces/L2/ISuperchainWETH.sol +++ b/packages/contracts-bedrock/interfaces/L2/ISuperchainWETH.sol @@ -9,6 +9,7 @@ interface ISuperchainWETH is IWETH98, IERC7802, ISemver { error Unauthorized(); error InvalidCrossDomainSender(); error ZeroAddress(); + error Permit2AllowanceIsFixedAtInfinity(); event SendETH(address indexed from, address indexed to, uint256 amount, uint256 destination); diff --git a/packages/contracts-bedrock/interfaces/dispute/IAnchorStateRegistry.sol b/packages/contracts-bedrock/interfaces/dispute/IAnchorStateRegistry.sol index a6f7d3a45c..14a745f5dc 100644 --- a/packages/contracts-bedrock/interfaces/dispute/IAnchorStateRegistry.sol +++ b/packages/contracts-bedrock/interfaces/dispute/IAnchorStateRegistry.sol @@ -5,30 +5,33 @@ import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; -import { GameType, Hash, OutputRoot } from "src/dispute/lib/Types.sol"; +import { GameType, Hash, Proposal } from "src/dispute/lib/Types.sol"; interface IAnchorStateRegistry { - error AnchorStateRegistry_Unauthorized(); - error AnchorStateRegistry_InvalidAnchorGame(); error AnchorStateRegistry_AnchorGameBlacklisted(); + error AnchorStateRegistry_InvalidAnchorGame(); + error AnchorStateRegistry_Unauthorized(); - event AnchorNotUpdated(IFaultDisputeGame indexed game); event AnchorUpdated(IFaultDisputeGame indexed game); + event DisputeGameBlacklisted(IDisputeGame indexed disputeGame); event Initialized(uint8 version); + event RespectedGameTypeSet(GameType gameType); + event RetirementTimestampSet(uint256 timestamp); function anchorGame() external view returns (IFaultDisputeGame); function anchors(GameType) external view returns (Hash, uint256); + function blacklistDisputeGame(IDisputeGame _disputeGame) external; + function disputeGameBlacklist(IDisputeGame) external view returns (bool); function getAnchorRoot() external view returns (Hash, uint256); + function disputeGameFinalityDelaySeconds() external view returns (uint256); function disputeGameFactory() external view returns (IDisputeGameFactory); function initialize( ISuperchainConfig _superchainConfig, IDisputeGameFactory _disputeGameFactory, - IOptimismPortal2 _portal, - OutputRoot memory _startingAnchorRoot + Proposal memory _startingAnchorRoot, + GameType _startingRespectedGameType ) external; - function isGameBlacklisted(IDisputeGame _game) external view returns (bool); function isGameProper(IDisputeGame _game) external view returns (bool); function isGameRegistered(IDisputeGame _game) external view returns (bool); @@ -37,11 +40,16 @@ interface IAnchorStateRegistry { function isGameRetired(IDisputeGame _game) external view returns (bool); function isGameFinalized(IDisputeGame _game) external view returns (bool); function isGameClaimValid(IDisputeGame _game) external view returns (bool); - function portal() external view returns (IOptimismPortal2); + function paused() external view returns (bool); function respectedGameType() external view returns (GameType); + function retirementTimestamp() external view returns (uint64); function setAnchorState(IDisputeGame _game) external; + function setRespectedGameType(GameType _gameType) external; function superchainConfig() external view returns (ISuperchainConfig); + function updateRetirementTimestamp() external; function version() external view returns (string memory); - function __constructor__() external; + function __constructor__( + uint256 _disputeGameFinalityDelaySeconds + ) external; } diff --git a/packages/contracts-bedrock/interfaces/dispute/IDisputeGame.sol b/packages/contracts-bedrock/interfaces/dispute/IDisputeGame.sol index 3765375987..85bb61606d 100644 --- a/packages/contracts-bedrock/interfaces/dispute/IDisputeGame.sol +++ b/packages/contracts-bedrock/interfaces/dispute/IDisputeGame.sol @@ -14,7 +14,7 @@ interface IDisputeGame is IInitializable { function gameCreator() external pure returns (address creator_); function rootClaim() external pure returns (Claim rootClaim_); function l1Head() external pure returns (Hash l1Head_); - function l2BlockNumber() external pure returns (uint256 l2BlockNumber_); + function l2SequenceNumber() external pure returns (uint256 l2SequenceNumber_); function extraData() external pure returns (bytes memory extraData_); function resolve() external returns (GameStatus status_); function gameData() external view returns (GameType gameType_, Claim rootClaim_, bytes memory extraData_); diff --git a/packages/contracts-bedrock/interfaces/dispute/IFaultDisputeGame.sol b/packages/contracts-bedrock/interfaces/dispute/IFaultDisputeGame.sol index 8cf4996019..9beccc00a5 100644 --- a/packages/contracts-bedrock/interfaces/dispute/IFaultDisputeGame.sol +++ b/packages/contracts-bedrock/interfaces/dispute/IFaultDisputeGame.sol @@ -128,7 +128,7 @@ interface IFaultDisputeGame is IDisputeGame { function resolvedSubgames(uint256) external view returns (bool); function splitDepth() external view returns (uint256 splitDepth_); function startingBlockNumber() external view returns (uint256 startingBlockNumber_); - function startingOutputRoot() external view returns (Hash root, uint256 l2BlockNumber); // nosemgrep + function startingOutputRoot() external view returns (Hash root, uint256 l2SequenceNumber); // nosemgrep function startingRootHash() external view returns (Hash startingRootHash_); function step(uint256 _claimIndex, bool _isAttack, bytes memory _stateData, bytes memory _proof) external; function subgames(uint256, uint256) external view returns (uint256); diff --git a/packages/contracts-bedrock/interfaces/dispute/IPermissionedDisputeGame.sol b/packages/contracts-bedrock/interfaces/dispute/IPermissionedDisputeGame.sol index ca235c3453..902f4dfec5 100644 --- a/packages/contracts-bedrock/interfaces/dispute/IPermissionedDisputeGame.sol +++ b/packages/contracts-bedrock/interfaces/dispute/IPermissionedDisputeGame.sol @@ -118,7 +118,7 @@ interface IPermissionedDisputeGame is IDisputeGame { function resolvedSubgames(uint256) external view returns (bool); function splitDepth() external view returns (uint256 splitDepth_); function startingBlockNumber() external view returns (uint256 startingBlockNumber_); - function startingOutputRoot() external view returns (Hash root, uint256 l2BlockNumber); // nosemgrep + function startingOutputRoot() external view returns (Hash root, uint256 l2SequenceNumber); // nosemgrep function startingRootHash() external view returns (Hash startingRootHash_); function step(uint256 _claimIndex, bool _isAttack, bytes memory _stateData, bytes memory _proof) external; function subgames(uint256, uint256) external view returns (uint256); diff --git a/packages/contracts-bedrock/interfaces/dispute/ISuperFaultDisputeGame.sol b/packages/contracts-bedrock/interfaces/dispute/ISuperFaultDisputeGame.sol new file mode 100644 index 0000000000..c379b84251 --- /dev/null +++ b/packages/contracts-bedrock/interfaces/dispute/ISuperFaultDisputeGame.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; +import { IDelayedWETH } from "interfaces/dispute/IDelayedWETH.sol"; +import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; +import { IBigStepper } from "interfaces/dispute/IBigStepper.sol"; +import { GameType, Claim, Position, Clock, Hash, Duration, BondDistributionMode } from "src/dispute/lib/Types.sol"; + +interface ISuperFaultDisputeGame is IDisputeGame { + struct ClaimData { + uint32 parentIndex; + address counteredBy; + address claimant; + uint128 bond; + Claim claim; + Position position; + Clock clock; + } + + struct ResolutionCheckpoint { + bool initialCheckpointComplete; + uint32 subgameIndex; + Position leftmostPosition; + address counteredBy; + } + + struct GameConstructorParams { + GameType gameType; + Claim absolutePrestate; + uint256 maxGameDepth; + uint256 splitDepth; + Duration clockExtension; + Duration maxClockDuration; + IBigStepper vm; + IDelayedWETH weth; + IAnchorStateRegistry anchorStateRegistry; + uint256 l2ChainId; + } + + error AlreadyInitialized(); + error AnchorRootNotFound(); + error BondTransferFailed(); + error CannotDefendRootClaim(); + error ClaimAboveSplit(); + error ClaimAlreadyExists(); + error ClaimAlreadyResolved(); + error ClockNotExpired(); + error ClockTimeExceeded(); + error DuplicateStep(); + error GameDepthExceeded(); + error GameNotInProgress(); + error IncorrectBondAmount(); + error InvalidChallengePeriod(); + error InvalidClockExtension(); + error InvalidDisputedClaimIndex(); + error InvalidLocalIdent(); + error InvalidParent(); + error InvalidPrestate(); + error InvalidSplitDepth(); + error MaxDepthTooLarge(); + error NoCreditToClaim(); + error NoChainIdNeeded(); + error OutOfOrderResolution(); + error SuperFaultDisputeGameInvalidRootClaim(); + error UnexpectedRootClaim(Claim rootClaim); + error ValidStep(); + error InvalidBondDistributionMode(); + error GameNotFinalized(); + error GameNotResolved(); + error ReservedGameType(); + + event Move(uint256 indexed parentIndex, Claim indexed claim, address indexed claimant); + event GameClosed(BondDistributionMode bondDistributionMode); + + function absolutePrestate() external view returns (Claim absolutePrestate_); + function addLocalData(uint256 _ident, uint256 _execLeafIdx, uint256 _partOffset) external; + function anchorStateRegistry() external view returns (IAnchorStateRegistry registry_); + function attack(Claim _disputed, uint256 _parentIndex, Claim _claim) external payable; + function bondDistributionMode() external view returns (BondDistributionMode); + function claimCredit(address _recipient) external; + function claimData(uint256) + external + view // nosemgrep + returns ( + uint32 parentIndex, + address counteredBy, + address claimant, + uint128 bond, + Claim claim, + Position position, + Clock clock + ); + function claimDataLen() external view returns (uint256 len_); + function claims(Hash) external view returns (bool); + function clockExtension() external view returns (Duration clockExtension_); + function closeGame() external; + function credit(address _recipient) external view returns (uint256 credit_); + function defend(Claim _disputed, uint256 _parentIndex, Claim _claim) external payable; + function getChallengerDuration(uint256 _claimIndex) external view returns (Duration duration_); + function getNumToResolve(uint256 _claimIndex) external view returns (uint256 numRemainingChildren_); + function getRequiredBond(Position _position) external view returns (uint256 requiredBond_); + function hasUnlockedCredit(address) external view returns (bool); + function maxClockDuration() external view returns (Duration maxClockDuration_); + function maxGameDepth() external view returns (uint256 maxGameDepth_); + function move(Claim _disputed, uint256 _challengeIndex, Claim _claim, bool _isAttack) external payable; + function normalModeCredit(address) external view returns (uint256); + function l2SequenceNumber() external pure returns (uint256 l2SequenceNumber_); + function refundModeCredit(address) external view returns (uint256); + function resolutionCheckpoints(uint256) + external + view + returns (bool initialCheckpointComplete, uint32 subgameIndex, Position leftmostPosition, address counteredBy); // nosemgrep + function resolveClaim(uint256 _claimIndex, uint256 _numToResolve) external; + function resolvedSubgames(uint256) external view returns (bool); + function splitDepth() external view returns (uint256 splitDepth_); + function startingSequenceNumber() external view returns (uint256 startingSequenceNumber_); + function startingProposal() external view returns (Hash root, uint256 l2SequenceNumber); // nosemgrep + function startingRootHash() external view returns (Hash startingRootHash_); + function step(uint256 _claimIndex, bool _isAttack, bytes memory _stateData, bytes memory _proof) external; + function subgames(uint256, uint256) external view returns (uint256); + function version() external pure returns (string memory); + function vm() external view returns (IBigStepper vm_); + function wasRespectedGameTypeWhenCreated() external view returns (bool); + function weth() external view returns (IDelayedWETH weth_); + + function __constructor__(GameConstructorParams memory _params) external; +} diff --git a/packages/contracts-bedrock/interfaces/safe/IDeputyGuardianModule.sol b/packages/contracts-bedrock/interfaces/safe/IDeputyGuardianModule.sol index a5c0e33130..d4d76649a5 100644 --- a/packages/contracts-bedrock/interfaces/safe/IDeputyGuardianModule.sol +++ b/packages/contracts-bedrock/interfaces/safe/IDeputyGuardianModule.sol @@ -2,22 +2,21 @@ pragma solidity ^0.8.0; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; -import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; import { GameType, Timestamp } from "src/dispute/lib/Types.sol"; import { GnosisSafe as Safe } from "safe-contracts/GnosisSafe.sol"; interface IDeputyGuardianModule is ISemver { - error ExecutionFailed(string); - error Unauthorized(); + error DeputyGuardianModule_ExecutionFailed(string); + error DeputyGuardianModule_Unauthorized(); event Paused(string identifier); event Unpaused(); event DisputeGameBlacklisted(IDisputeGame indexed game); event RespectedGameTypeSet(GameType indexed gameType, Timestamp indexed updatedAt); + event RetirementTimestampUpdated(Timestamp indexed updatedAt); function version() external view returns (string memory); function __constructor__(Safe _safe, ISuperchainConfig _superchainConfig, address _deputyGuardian) external; @@ -26,7 +25,7 @@ interface IDeputyGuardianModule is ISemver { function deputyGuardian() external view returns (address deputyGuardian_); function pause() external; function unpause() external; - function setAnchorState(IAnchorStateRegistry _registry, IFaultDisputeGame _game) external; - function blacklistDisputeGame(IOptimismPortal2 _portal, IDisputeGame _game) external; - function setRespectedGameType(IOptimismPortal2 _portal, GameType _gameType) external; + function blacklistDisputeGame(IAnchorStateRegistry _anchorStateRegistry, IDisputeGame _game) external; + function setRespectedGameType(IAnchorStateRegistry _anchorStateRegistry, GameType _gameType) external; + function updateRetirementTimestamp(IAnchorStateRegistry _anchorStateRegistry) external; } diff --git a/packages/contracts-bedrock/interfaces/universal/IReinitializableBase.sol b/packages/contracts-bedrock/interfaces/universal/IReinitializableBase.sol new file mode 100644 index 0000000000..d6d096a743 --- /dev/null +++ b/packages/contracts-bedrock/interfaces/universal/IReinitializableBase.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IReinitializableBase { + error ReinitializableBase_ZeroInitVersion(); + + function initVersion() external view returns (uint8); + + // ReinitializerBase is abstract, so it has no constructor in its interface. + function __constructor__() external; +} diff --git a/packages/contracts-bedrock/justfile b/packages/contracts-bedrock/justfile index 09c0f1f8a2..82337e5e88 100644 --- a/packages/contracts-bedrock/justfile +++ b/packages/contracts-bedrock/justfile @@ -27,6 +27,10 @@ forge-build-dev *ARGS: build-source: forge build --skip "/**/test/**" --skip "/**/scripts/**" +# Builds source contracts and scripts, skipping tests. +build-no-tests: + forge build --skip "/**/test/**" + # Builds the contracts. build *ARGS: lint-fix-no-fail just forge-build {{ARGS}} @@ -65,7 +69,7 @@ test-dev *ARGS: build-go-ffi # Default block number for the forked upgrade path. export sepoliaBlockNumber := "7701807" -export mainnetBlockNumber := "21971446" +export mainnetBlockNumber := "21983965" export pinnedBlockNumber := if env_var_or_default("FORK_BASE_CHAIN", "") == "mainnet" { mainnetBlockNumber @@ -303,6 +307,7 @@ check: semver-diff-check-no-build \ validate-deploy-configs \ validate-spacers-no-build \ + reinitializer-check-no-build \ interfaces-check-no-build \ lint-forge-tests-check-no-build @@ -352,6 +357,18 @@ pre-pr *ARGS: cp -r "$TEMP_BUILD_DIR/cache" ./ fi +twrap: + #!/usr/bin/env sh + if [ -z "$WRAP_DISABLED" ]; then + tput rmam + export WRAP_DISABLED=1 + echo "Terminal line wrapping disabled" + else + tput smam + unset WRAP_DISABLED + echo "Terminal line wrapping enabled" + fi + # Fixes linting errors. lint-fix: forge fmt diff --git a/packages/contracts-bedrock/lib/superchain-registry b/packages/contracts-bedrock/lib/superchain-registry index b9383cc1d0..84bce73573 160000 --- a/packages/contracts-bedrock/lib/superchain-registry +++ b/packages/contracts-bedrock/lib/superchain-registry @@ -1 +1 @@ -Subproject commit b9383cc1d0e97c6699e78dda06465aca182647c5 +Subproject commit 84bce73573f130008d84bae6e924163bab589a11 diff --git a/packages/contracts-bedrock/scripts/L2Genesis.s.sol b/packages/contracts-bedrock/scripts/L2Genesis.s.sol index 7ab4e9d6ef..e8463c0dec 100644 --- a/packages/contracts-bedrock/scripts/L2Genesis.s.sol +++ b/packages/contracts-bedrock/scripts/L2Genesis.s.sol @@ -395,16 +395,9 @@ contract L2Genesis is Deployer { /// @notice This predeploy is following the safety invariant #1. function setL1Block() public { - if (cfg.useInterop()) { - string memory cname = "L1BlockInterop"; - address impl = Predeploys.predeployToCodeNamespace(Predeploys.L1_BLOCK_ATTRIBUTES); - console.log("Setting %s implementation at: %s", cname, impl); - vm.etch(impl, vm.getDeployedCode(string.concat(cname, ".sol:", cname))); - } else { - _setImplementationCode(Predeploys.L1_BLOCK_ATTRIBUTES); - // Note: L1 block attributes are set to 0. - // Before the first user-tx the state is overwritten with actual L1 attributes. - } + // Note: L1 block attributes are set to 0. + // Before the first user-tx the state is overwritten with actual L1 attributes. + _setImplementationCode(Predeploys.L1_BLOCK_ATTRIBUTES); } /// @notice This predeploy is following the safety invariant #1. @@ -562,13 +555,13 @@ contract L2Genesis is Deployer { vm.resetNonce(address(eas)); } - /// @notice This predeploy is following the safety invariant #2. + /// @notice This predeploy is following the safety invariant #1. /// This contract has no initializer. function setCrossL2Inbox() internal { _setImplementationCode(Predeploys.CROSS_L2_INBOX); } - /// @notice This predeploy is following the safety invariant #2. + /// @notice This predeploy is following the safety invariant #1. /// This contract has no initializer. function setL2ToL2CrossDomainMessenger() internal { _setImplementationCode(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); diff --git a/packages/contracts-bedrock/scripts/autogen/generate-semver-lock/main.go b/packages/contracts-bedrock/scripts/autogen/generate-semver-lock/main.go index 4e826c8145..4a9e03b15a 100644 --- a/packages/contracts-bedrock/scripts/autogen/generate-semver-lock/main.go +++ b/packages/contracts-bedrock/scripts/autogen/generate-semver-lock/main.go @@ -5,10 +5,10 @@ import ( "encoding/json" "fmt" "os" - "regexp" "sort" "strings" + "github.com/ethereum-optimism/optimism/op-chain-ops/solc" "github.com/ethereum-optimism/optimism/packages/contracts-bedrock/scripts/checks/common" "github.com/ethereum/go-ethereum/crypto" ) @@ -21,8 +21,8 @@ type SemverLockOutput struct { } type SemverLockResult struct { - SemverLockOutput - SourceFilePath string + ContractKey string + SemverLockOutput SemverLockOutput } func main() { @@ -32,7 +32,7 @@ func main() { processFile, ) if err != nil { - fmt.Printf("error: %v\n", err) + fmt.Printf("Failed to generate semver lock: %v\n", err) os.Exit(1) } @@ -42,7 +42,7 @@ func main() { if result == nil { continue } - output[result.SourceFilePath] = result.SemverLockOutput + output[result.ContractKey] = result.SemverLockOutput } // Get and sort the keys @@ -76,16 +76,60 @@ func processFile(file string) (*SemverLockResult, []error) { return nil, []error{fmt.Errorf("failed to read artifact: %w", err)} } + var sourceFilePath, contractName, contractKey string + for path, name := range artifact.Metadata.Settings.CompilationTarget { + sourceFilePath = path + contractName = name + contractKey = sourceFilePath + ":" + name + break + } + // Only apply to files in the src directory. - sourceFilePath := artifact.Ast.AbsolutePath if !strings.HasPrefix(sourceFilePath, "src/") { return nil, nil } - // Check if the contract uses semver. - semverRegex := regexp.MustCompile(`custom:semver`) - semver := semverRegex.FindStringSubmatch(artifact.RawMetadata) - if len(semver) == 0 { + // Check if the contract has a version function or variable with @custom:semver tag + hasSemverTag := false + for _, node := range artifact.Ast.Nodes { + if node.NodeType != "ContractDefinition" || node.Name != contractName { + continue + } + // Check each node inside the contract + for _, subNode := range node.Nodes { + // Skip nodes that aren't version functions or variables + if (subNode.NodeType != "FunctionDefinition" && + subNode.NodeType != "VariableDeclaration") || + subNode.Name != "version" { + continue + } + if subNode.Documentation == nil { + continue + } + // Handle documentation based on its actual type + var docText string + switch doc := subNode.Documentation.(type) { + case string: + docText = doc + case map[string]interface{}: + if text, ok := doc["text"].(string); ok { + docText = text + } + case solc.AstDocumentation: + docText = doc.Text + case *solc.AstDocumentation: + docText = doc.Text + } + if strings.Contains(docText, "@custom:semver") { + hasSemverTag = true + break + } + } + if hasSemverTag { + break + } + } + if !hasSemverTag { return nil, nil } @@ -101,13 +145,12 @@ func processFile(file string) (*SemverLockResult, []error) { return nil, []error{fmt.Errorf("failed to read source file: %w", err)} } - // Calculate hashes using Keccak256 trimmedSourceCode := []byte(strings.TrimSuffix(string(sourceCode), "\n")) initCodeHash := fmt.Sprintf("0x%x", crypto.Keccak256Hash(initCodeBytes)) sourceCodeHash := fmt.Sprintf("0x%x", crypto.Keccak256Hash(trimmedSourceCode)) return &SemverLockResult{ - SourceFilePath: sourceFilePath, + ContractKey: contractKey, SemverLockOutput: SemverLockOutput{ InitCodeHash: initCodeHash, SourceCodeHash: sourceCodeHash, diff --git a/packages/contracts-bedrock/scripts/checks/check-frozen-files.sh b/packages/contracts-bedrock/scripts/checks/check-frozen-files.sh index 77eec5660b..07704e390d 100755 --- a/packages/contracts-bedrock/scripts/checks/check-frozen-files.sh +++ b/packages/contracts-bedrock/scripts/checks/check-frozen-files.sh @@ -48,66 +48,62 @@ changed_contracts=$(jq -r ' # In order to prevent a file from being modified, comment it out. Do not delete it. # All files in semver-lock.json should be in this list. ALLOWED_FILES=( - "src/L1/DataAvailabilityChallenge.sol" - "src/L1/L1CrossDomainMessenger.sol" - "src/L1/L1ERC721Bridge.sol" - "src/L1/L1StandardBridge.sol" - "src/L1/OPContractsManager.sol" - "src/L1/OPContractsManagerInterop.sol" - "src/L1/OPPrestateUpdater.sol" - "src/L1/OptimismPortal2.sol" - "src/L1/OptimismPortalInterop.sol" - "src/L1/ProtocolVersions.sol" - "src/L1/SuperchainConfig.sol" - "src/L1/SystemConfig.sol" - "src/L1/SystemConfigInterop.sol" - "src/L2/BaseFeeVault.sol" - "src/L2/CrossL2Inbox.sol" - "src/L2/ETHLiquidity.sol" - "src/L2/GasPriceOracle.sol" - "src/L2/L1Block.sol" - "src/L2/L1BlockInterop.sol" - "src/L2/L1FeeVault.sol" - "src/L2/L2CrossDomainMessenger.sol" - "src/L2/L2ERC721Bridge.sol" - "src/L2/L2StandardBridge.sol" - "src/L2/L2StandardBridgeInterop.sol" - "src/L2/L2ToL1MessagePasser.sol" - "src/L2/L2ToL2CrossDomainMessenger.sol" - "src/L2/OptimismMintableERC721.sol" - "src/L2/OptimismMintableERC721Factory.sol" - "src/L2/OptimismSuperchainERC20.sol" - "src/L2/OptimismSuperchainERC20Beacon.sol" - "src/L2/OptimismSuperchainERC20Factory.sol" - "src/L2/SequencerFeeVault.sol" - "src/L2/SuperchainERC20.sol" - "src/L2/SuperchainTokenBridge.sol" - "src/L2/SuperchainWETH.sol" - "src/L2/WETH.sol" - # "src/cannon/MIPS.sol" - # "src/cannon/MIPS2.sol" - # "src/cannon/MIPS64.sol" - "src/cannon/PreimageOracle.sol" - "src/dispute/AnchorStateRegistry.sol" - "src/dispute/DelayedWETH.sol" - "src/dispute/DisputeGameFactory.sol" - "src/dispute/FaultDisputeGame.sol" - "src/dispute/PermissionedDisputeGame.sol" - "src/dispute/SuperFaultDisputeGame.sol" - "src/dispute/SuperPermissionedDisputeGame.sol" - "src/legacy/DeployerWhitelist.sol" - "src/legacy/L1BlockNumber.sol" - "src/legacy/LegacyMessagePasser.sol" - "src/safe/DeputyGuardianModule.sol" - "src/safe/DeputyPauseModule.sol" - "src/safe/LivenessGuard.sol" - "src/safe/LivenessModule.sol" - "src/universal/OptimismMintableERC20.sol" - "src/universal/OptimismMintableERC20Factory.sol" - "src/universal/StorageSetter.sol" - "src/vendor/asterisc/RISCV.sol" - "src/vendor/eas/EAS.sol" - "src/vendor/eas/SchemaRegistry.sol" + "src/L1/DataAvailabilityChallenge.sol:DataAvailabilityChallenge" + # "src/L1/ETHLockbox.sol:ETHLockbox" + "src/L1/L1CrossDomainMessenger.sol:L1CrossDomainMessenger" + "src/L1/L1ERC721Bridge.sol:L1ERC721Bridge" + "src/L1/L1StandardBridge.sol:L1StandardBridge" + "src/L1/OPContractsManager.sol:OPContractsManager" + # "src/L1/OptimismPortal2.sol:OptimismPortal2" + "src/L1/ProtocolVersions.sol:ProtocolVersions" + "src/L1/SuperchainConfig.sol:SuperchainConfig" + "src/L1/SystemConfig.sol:SystemConfig" + "src/L2/BaseFeeVault.sol:BaseFeeVault" + "src/L2/CrossL2Inbox.sol:CrossL2Inbox" + "src/L2/ETHLiquidity.sol:ETHLiquidity" + "src/L2/GasPriceOracle.sol:GasPriceOracle" + "src/L2/L1Block.sol:L1Block" + "src/L2/L1FeeVault.sol:L1FeeVault" + "src/L2/L2CrossDomainMessenger.sol:L2CrossDomainMessenger" + "src/L2/L2ERC721Bridge.sol:L2ERC721Bridge" + "src/L2/L2StandardBridge.sol:L2StandardBridge" + "src/L2/L2StandardBridgeInterop.sol:L2StandardBridgeInterop" + "src/L2/L2ToL1MessagePasser.sol:L2ToL1MessagePasser" + "src/L2/L2ToL2CrossDomainMessenger.sol:L2ToL2CrossDomainMessenger" + "src/L2/OptimismMintableERC721.sol:OptimismMintableERC721" + "src/L2/OptimismMintableERC721Factory.sol:OptimismMintableERC721Factory" + "src/L2/OptimismSuperchainERC20.sol:OptimismSuperchainERC20" + "src/L2/OptimismSuperchainERC20Beacon.sol:OptimismSuperchainERC20Beacon" + "src/L2/OptimismSuperchainERC20Factory.sol:OptimismSuperchainERC20Factory" + "src/L2/SequencerFeeVault.sol:SequencerFeeVault" + "src/L2/SuperchainERC20.sol:SuperchainERC20" + "src/L2/SuperchainTokenBridge.sol:SuperchainTokenBridge" + "src/L2/SuperchainWETH.sol:SuperchainWETH" + "src/L2/WETH.sol:WETH" + # "src/cannon/MIPS.sol:MIPS" + # "src/cannon/MIPS2.sol:MIPS2" + # "src/cannon/MIPS64.sol:MIPS64" + "src/cannon/PreimageOracle.sol:PreimageOracle" + # "src/dispute/AnchorStateRegistry.sol:AnchorStateRegistry" + "src/dispute/DelayedWETH.sol:DelayedWETH" + # "src/dispute/DisputeGameFactory.sol:DisputeGameFactory" + "src/dispute/FaultDisputeGame.sol:FaultDisputeGame" + "src/dispute/PermissionedDisputeGame.sol:PermissionedDisputeGame" + "src/dispute/SuperFaultDisputeGame.sol:SuperFaultDisputeGame" + "src/dispute/SuperPermissionedDisputeGame.sol:SuperPermissionedDisputeGame" + "src/legacy/DeployerWhitelist.sol:DeployerWhitelist" + "src/legacy/L1BlockNumber.sol:L1BlockNumber" + "src/legacy/LegacyMessagePasser.sol:LegacyMessagePasser" + "src/safe/DeputyGuardianModule.sol:DeputyGuardianModule" + "src/safe/DeputyPauseModule.sol:DeputyPauseModule" + "src/safe/LivenessGuard.sol:LivenessGuard" + "src/safe/LivenessModule.sol:LivenessModule" + "src/universal/OptimismMintableERC20.sol:OptimismMintableERC20" + "src/universal/OptimismMintableERC20Factory.sol:OptimismMintableERC20Factory" + "src/universal/StorageSetter.sol:StorageSetter" + "src/vendor/asterisc/RISCV.sol:RISCV" + "src/vendor/eas/EAS.sol:EAS" + "src/vendor/eas/SchemaRegistry.sol:SchemaRegistry" ) MATCHED_FILES=() diff --git a/packages/contracts-bedrock/scripts/checks/check-semver-diff.sh b/packages/contracts-bedrock/scripts/checks/check-semver-diff.sh index 08540879a9..18f698f911 100755 --- a/packages/contracts-bedrock/scripts/checks/check-semver-diff.sh +++ b/packages/contracts-bedrock/scripts/checks/check-semver-diff.sh @@ -56,7 +56,7 @@ changed_contracts=$(jq -r ' .key as $key | .value != $upstream[$key] ) - ) | map(.key); + ) | map(.key | split(":")[0]); changes[] ' "$temp_dir/local_semver_lock.json" "$temp_dir/upstream_semver_lock.json") diff --git a/packages/contracts-bedrock/scripts/checks/interfaces/main.go b/packages/contracts-bedrock/scripts/checks/interfaces/main.go index 211730d3ad..4dbbf9bd31 100644 --- a/packages/contracts-bedrock/scripts/checks/interfaces/main.go +++ b/packages/contracts-bedrock/scripts/checks/interfaces/main.go @@ -26,9 +26,12 @@ var excludeContracts = []string{ // EAS "IEAS", "ISchemaResolver", "ISchemaRegistry", + // Misc stuff that can be ignored + "IOPContractsManagerLegacyUpgrade", + // TODO: Interfaces that need to be fixed "IInitializable", "IOptimismMintableERC20", "ILegacyMintableERC20", - "KontrolCheatsBase", "ISystemConfigInterop", "IResolvedDelegateProxy", + "KontrolCheatsBase", "IResolvedDelegateProxy", } type ContractDefinition struct { diff --git a/packages/contracts-bedrock/scripts/checks/reinitializer/main.go b/packages/contracts-bedrock/scripts/checks/reinitializer/main.go index c41e403b98..e44c955d03 100644 --- a/packages/contracts-bedrock/scripts/checks/reinitializer/main.go +++ b/packages/contracts-bedrock/scripts/checks/reinitializer/main.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "math" "os" "strconv" "strings" @@ -103,15 +104,23 @@ func getReinitializerValue(node *solc.AstNode) (uint64, error) { for _, modifier := range node.Modifiers { if modifier.ModifierName.Name == "reinitializer" { - valStr, ok := modifier.Arguments[0].Value.(string) - if !ok { - return 0, fmt.Errorf("reinitializer value is not a string") + if modifier.Arguments[0].Kind == "functionCall" { + if modifier.Arguments[0].Expression.Name == "initVersion" { + return math.MaxUint64, nil // uint64 max representing initVersion call + } else { + return 0, fmt.Errorf("reinitializer value is not a call to initVersion") + } + } else { + valStr, ok := modifier.Arguments[0].Value.(string) + if !ok { + return 0, fmt.Errorf("reinitializer value is not a string") + } + val, err := strconv.Atoi(valStr) + if err != nil { + return 0, fmt.Errorf("reinitializer value is not an integer") + } + return uint64(val), nil } - val, err := strconv.Atoi(valStr) - if err != nil { - return 0, fmt.Errorf("reinitializer value is not an integer") - } - return uint64(val), nil } } diff --git a/packages/contracts-bedrock/scripts/checks/reinitializer/main_test.go b/packages/contracts-bedrock/scripts/checks/reinitializer/main_test.go index 93ca35173b..0f9fc382e2 100644 --- a/packages/contracts-bedrock/scripts/checks/reinitializer/main_test.go +++ b/packages/contracts-bedrock/scripts/checks/reinitializer/main_test.go @@ -1,6 +1,7 @@ package main import ( + "math" "testing" "github.com/ethereum-optimism/optimism/op-chain-ops/solc" @@ -155,6 +156,38 @@ func TestGetReinitializerValue(t *testing.T) { want: 0, wantErr: true, }, + { + name: "Valid reinitializer with initVersion call", + node: &solc.AstNode{ + Modifiers: []solc.AstNode{ + { + ModifierName: &solc.Expression{Name: "reinitializer"}, + Arguments: []solc.Expression{{ + Kind: "functionCall", + Expression: &solc.Expression{Name: "initVersion"}, + }}, + }, + }, + }, + want: math.MaxUint64, + wantErr: false, + }, + { + name: "Invalid function call - not initVersion", + node: &solc.AstNode{ + Modifiers: []solc.AstNode{ + { + ModifierName: &solc.Expression{Name: "reinitializer"}, + Arguments: []solc.Expression{{ + Kind: "functionCall", + Expression: &solc.Expression{Name: "someOtherFunction"}, + }}, + }, + }, + }, + want: 0, + wantErr: true, + }, } for _, tt := range tests { @@ -484,6 +517,85 @@ func TestCheckArtifact(t *testing.T) { }, wantErr: false, // Should return nil without error }, + { + name: "Matching reinitializer values with initVersion", + artifact: &solc.ForgeArtifact{ + Ast: solc.Ast{ + Nodes: []solc.AstNode{ + { + NodeType: "ContractDefinition", + Nodes: []solc.AstNode{ + { + NodeType: "FunctionDefinition", + Name: "initialize", + Modifiers: []solc.AstNode{ + { + ModifierName: &solc.Expression{Name: "reinitializer"}, + Arguments: []solc.Expression{{ + Kind: "functionCall", + Expression: &solc.Expression{Name: "initVersion"}, + }}, + }, + }, + }, + { + NodeType: "FunctionDefinition", + Name: "upgrade", + Modifiers: []solc.AstNode{ + { + ModifierName: &solc.Expression{Name: "reinitializer"}, + Arguments: []solc.Expression{{ + Kind: "functionCall", + Expression: &solc.Expression{Name: "initVersion"}, + }}, + }, + }, + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "Mismatched reinitializer values - one with initVersion, one with constant", + artifact: &solc.ForgeArtifact{ + Ast: solc.Ast{ + Nodes: []solc.AstNode{ + { + NodeType: "ContractDefinition", + Nodes: []solc.AstNode{ + { + NodeType: "FunctionDefinition", + Name: "initialize", + Modifiers: []solc.AstNode{ + { + ModifierName: &solc.Expression{Name: "reinitializer"}, + Arguments: []solc.Expression{{ + Kind: "functionCall", + Expression: &solc.Expression{Name: "initVersion"}, + }}, + }, + }, + }, + { + NodeType: "FunctionDefinition", + Name: "upgrade", + Modifiers: []solc.AstNode{ + { + ModifierName: &solc.Expression{Name: "reinitializer"}, + Arguments: []solc.Expression{{Value: "2"}}, + }, + }, + }, + }, + }, + }, + }, + }, + wantErr: true, + }, } for _, tt := range tests { diff --git a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol index 026cdb178f..34b8c8d747 100644 --- a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol +++ b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol @@ -7,7 +7,6 @@ import { console2 as console } from "forge-std/console2.sol"; // Scripts import { DeployConfig } from "scripts/deploy/DeployConfig.s.sol"; -import { ISystemConfigInterop } from "interfaces/L1/ISystemConfigInterop.sol"; import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; // Libraries @@ -22,7 +21,7 @@ import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; +import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPortal2.sol"; import { IL1ERC721Bridge } from "interfaces/L1/IL1ERC721Bridge.sol"; import { IL1StandardBridge } from "interfaces/L1/IL1StandardBridge.sol"; import { ProtocolVersion, IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; @@ -32,6 +31,7 @@ import { IOptimismMintableERC20Factory } from "interfaces/universal/IOptimismMin import { IPreimageOracle } from "interfaces/cannon/IPreimageOracle.sol"; import { IMIPS } from "interfaces/cannon/IMIPS.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; library ChainAssertions { Vm internal constant vm = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); @@ -91,7 +91,6 @@ library ChainAssertions { require(config.l1CrossDomainMessenger() == _contracts.L1CrossDomainMessenger, "CHECK-SCFG-160"); require(config.l1ERC721Bridge() == _contracts.L1ERC721Bridge, "CHECK-SCFG-170"); require(config.l1StandardBridge() == _contracts.L1StandardBridge, "CHECK-SCFG-180"); - require(config.disputeGameFactory() == _contracts.DisputeGameFactory, "CHECK-SCFG-190"); require(config.optimismPortal() == _contracts.OptimismPortal, "CHECK-SCFG-200"); require(config.optimismMintableERC20Factory() == _contracts.OptimismMintableERC20Factory, "CHECK-SCFG-210"); } else { @@ -116,38 +115,11 @@ library ChainAssertions { require(config.l1CrossDomainMessenger() == address(0), "CHECK-SCFG-380"); require(config.l1ERC721Bridge() == address(0), "CHECK-SCFG-390"); require(config.l1StandardBridge() == address(0), "CHECK-SCFG-400"); - require(config.disputeGameFactory() == address(0), "CHECK-SCFG-410"); require(config.optimismPortal() == address(0), "CHECK-SCFG-420"); require(config.optimismMintableERC20Factory() == address(0), "CHECK-SCFG-430"); } } - /// @notice Asserts that the SystemConfigInterop is setup correctly - function checkSystemConfigInterop( - Types.ContractSet memory _contracts, - DeployConfig _cfg, - bool _isProxy - ) - internal - view - { - ISystemConfigInterop config = ISystemConfigInterop(_contracts.SystemConfig); - console.log( - "Running chain assertions on the SystemConfigInterop %s at %s", - _isProxy ? "proxy" : "implementation", - address(config) - ); - - checkSystemConfig(_contracts, _cfg, _isProxy); - if (_isProxy) { - // TODO: this is not being set in the deployment, nor is a config value. - // Update this when it has an entry in hardhat.json - require(config.dependencyManager() == address(0), "CHECK-SCFGI-10"); - } else { - require(config.dependencyManager() == address(0), "CHECK-SCFGI-20"); - } - } - /// @notice Asserts that the L1CrossDomainMessenger is setup correctly function checkL1CrossDomainMessenger(Types.ContractSet memory _contracts, Vm _vm, bool _isProxy) internal view { IL1CrossDomainMessenger messenger = IL1CrossDomainMessenger(_contracts.L1CrossDomainMessenger); @@ -367,7 +339,7 @@ library ChainAssertions { internal view { - IOptimismPortal2 portal = IOptimismPortal2(payable(_contracts.OptimismPortal)); + IOptimismPortal portal = IOptimismPortal(payable(_contracts.OptimismPortal)); console.log( "Running chain assertions on the OptimismPortal2 %s at %s", _isProxy ? "proxy" : "implementation", @@ -385,20 +357,52 @@ library ChainAssertions { if (_isProxy) { require(address(portal.disputeGameFactory()) == _contracts.DisputeGameFactory, "CHECK-OP2-20"); + require(address(portal.anchorStateRegistry()) == _contracts.AnchorStateRegistry, "CHECK-OP2-25"); require(address(portal.systemConfig()) == _contracts.SystemConfig, "CHECK-OP2-30"); require(portal.guardian() == guardian, "CHECK-OP2-40"); require(address(portal.superchainConfig()) == address(_contracts.SuperchainConfig), "CHECK-OP2-50"); require(portal.paused() == ISuperchainConfig(_contracts.SuperchainConfig).paused(), "CHECK-OP2-60"); require(portal.l2Sender() == Constants.DEFAULT_L2_SENDER, "CHECK-OP2-70"); + require(address(portal.ethLockbox()) == _contracts.ETHLockbox, "CHECK-OP2-80"); } else { - require(address(portal.disputeGameFactory()) == address(0), "CHECK-OP2-80"); + require(address(portal.anchorStateRegistry()) == address(0), "CHECK-OP2-80"); require(address(portal.systemConfig()) == address(0), "CHECK-OP2-90"); require(address(portal.superchainConfig()) == address(0), "CHECK-OP2-100"); require(portal.l2Sender() == address(0), "CHECK-OP2-110"); + require(address(portal.ethLockbox()) == address(0), "CHECK-OP2-120"); } // This slot is the custom gas token _balance and this check ensures // that it stays unset for forwards compatibility with custom gas token. - require(vm.load(address(portal), bytes32(uint256(61))) == bytes32(0), "CHECK-OP2-120"); + require(vm.load(address(portal), bytes32(uint256(61))) == bytes32(0), "CHECK-OP2-130"); + } + + /// @notice Asserts that the ETHLockbox is setup correctly + function checkETHLockbox(Types.ContractSet memory _contracts, DeployConfig _cfg, bool _isProxy) internal view { + IETHLockbox ethLockbox = IETHLockbox(_contracts.ETHLockbox); + ISuperchainConfig superchainConfig = ISuperchainConfig(_contracts.SuperchainConfig); + + console.log( + "Running chain assertions on the ETHLockbox %s at %s", + _isProxy ? "proxy" : "implementation", + address(ethLockbox) + ); + + require(address(ethLockbox) != address(0), "CHECK-ELB-10"); + + // Check that the contract is initialized + DeployUtils.assertInitialized({ _contractAddress: address(ethLockbox), _isProxy: _isProxy, _slot: 0, _offset: 0 }); + + if (_isProxy) { + require(ethLockbox.superchainConfig() == superchainConfig, "CHECK-ELB-20"); + require(ethLockbox.authorizedPortals(IOptimismPortal(payable(_contracts.OptimismPortal))), "CHECK-ELB-30"); + require(ethLockbox.proxyAdminOwner() == _cfg.finalSystemOwner(), "CHECK-ELB-40"); + } else { + require(address(ethLockbox.superchainConfig()) == address(0), "CHECK-ELB-50"); + require( + ethLockbox.authorizedPortals(IOptimismPortal(payable(_contracts.OptimismPortal))) == false, + "CHECK-ELB-60" + ); + } } /// @notice Asserts that the ProtocolVersions is setup correctly diff --git a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol index f4638d2080..b71b6dd3f3 100644 --- a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol @@ -20,7 +20,6 @@ import { DeploySuperchainInput, DeploySuperchain, DeploySuperchainOutput } from import { DeployImplementationsInput, DeployImplementations, - DeployImplementationsInterop, DeployImplementationsOutput } from "scripts/deploy/DeployImplementations.s.sol"; @@ -29,13 +28,12 @@ import { Constants } from "src/libraries/Constants.sol"; import { Types } from "scripts/libraries/Types.sol"; import { Duration } from "src/dispute/lib/LibUDT.sol"; import { StorageSlot, ForgeArtifacts } from "scripts/libraries/ForgeArtifacts.sol"; -import { GameType, Claim, GameTypes, OutputRoot, Hash } from "src/dispute/lib/Types.sol"; +import { GameType, Claim, GameTypes, Proposal, Hash } from "src/dispute/lib/Types.sol"; // Interfaces import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IDataAvailabilityChallenge } from "interfaces/L1/IDataAvailabilityChallenge.sol"; @@ -119,6 +117,7 @@ contract Deploy is Deployer { AnchorStateRegistry: artifacts.getAddress("AnchorStateRegistryProxy"), OptimismMintableERC20Factory: artifacts.getAddress("OptimismMintableERC20FactoryProxy"), OptimismPortal: artifacts.getAddress("OptimismPortalProxy"), + ETHLockbox: artifacts.getAddress("ETHLockboxProxy"), SystemConfig: artifacts.getAddress("SystemConfigProxy"), L1ERC721Bridge: artifacts.getAddress("L1ERC721BridgeProxy"), ProtocolVersions: artifacts.getAddress("ProtocolVersionsProxy"), @@ -138,6 +137,7 @@ contract Deploy is Deployer { AnchorStateRegistry: artifacts.getAddress("AnchorStateRegistryImpl"), OptimismMintableERC20Factory: artifacts.getAddress("OptimismMintableERC20FactoryImpl"), OptimismPortal: artifacts.getAddress("OptimismPortal2Impl"), + ETHLockbox: artifacts.getAddress("ETHLockboxImpl"), SystemConfig: artifacts.getAddress("SystemConfigImpl"), L1ERC721Bridge: artifacts.getAddress("L1ERC721BridgeImpl"), ProtocolVersions: artifacts.getAddress("ProtocolVersionsImpl"), @@ -206,7 +206,7 @@ contract Deploy is Deployer { // Set the respected game type according to the deploy config vm.startPrank(ISuperchainConfig(artifacts.mustGetAddress("SuperchainConfigProxy")).guardian()); - IOptimismPortal2(artifacts.mustGetAddress("OptimismPortalProxy")).setRespectedGameType( + IAnchorStateRegistry(artifacts.mustGetAddress("AnchorStateRegistryProxy")).setRespectedGameType( GameType.wrap(uint32(cfg.respectedGameType())) ); vm.stopPrank(); @@ -296,9 +296,6 @@ contract Deploy is Deployer { // I think this was a bug dii.set(dii.upgradeController.selector, superchainProxyAdmin.owner()); - if (_isInterop) { - di = DeployImplementations(new DeployImplementationsInterop()); - } di.run(dii, dio); // Save the implementation addresses which are needed outside of this function or script. @@ -318,6 +315,7 @@ contract Deploy is Deployer { AnchorStateRegistry: address(0), OptimismMintableERC20Factory: address(dio.optimismMintableERC20FactoryImpl()), OptimismPortal: address(dio.optimismPortalImpl()), + ETHLockbox: address(dio.ethLockboxImpl()), SystemConfig: address(dio.systemConfigImpl()), L1ERC721Bridge: address(dio.l1ERC721BridgeImpl()), ProtocolVersions: address(dio.protocolVersionsImpl()), @@ -328,6 +326,7 @@ contract Deploy is Deployer { ChainAssertions.checkL1StandardBridge({ _contracts: impls, _isProxy: false }); ChainAssertions.checkL1ERC721Bridge({ _contracts: impls, _isProxy: false }); ChainAssertions.checkOptimismPortal2({ _contracts: impls, _cfg: cfg, _isProxy: false }); + ChainAssertions.checkETHLockbox({ _contracts: impls, _cfg: cfg, _isProxy: false }); ChainAssertions.checkOptimismMintableERC20Factory({ _contracts: impls, _isProxy: false }); ChainAssertions.checkDisputeGameFactory({ _contracts: impls, _expectedOwner: address(0), _isProxy: false }); ChainAssertions.checkDelayedWETH({ _contracts: impls, _cfg: cfg, _isProxy: false, _expectedOwner: address(0) }); @@ -346,11 +345,7 @@ contract Deploy is Deployer { _mips: IMIPS(address(dio.mipsSingleton())), _superchainProxyAdmin: superchainProxyAdmin }); - if (_isInterop) { - ChainAssertions.checkSystemConfigInterop({ _contracts: impls, _cfg: cfg, _isProxy: false }); - } else { - ChainAssertions.checkSystemConfig({ _contracts: impls, _cfg: cfg, _isProxy: false }); - } + ChainAssertions.checkSystemConfig({ _contracts: impls, _cfg: cfg, _isProxy: false }); } /// @notice Deploy all of the OP Chain specific contracts @@ -372,6 +367,7 @@ contract Deploy is Deployer { artifacts.save("OptimismMintableERC20FactoryProxy", address(deployOutput.optimismMintableERC20FactoryProxy)); artifacts.save("L1StandardBridgeProxy", address(deployOutput.l1StandardBridgeProxy)); artifacts.save("L1CrossDomainMessengerProxy", address(deployOutput.l1CrossDomainMessengerProxy)); + artifacts.save("ETHLockboxProxy", address(deployOutput.ethLockboxProxy)); // Fault Proof contracts artifacts.save("DisputeGameFactoryProxy", address(deployOutput.disputeGameFactoryProxy)); @@ -380,7 +376,6 @@ contract Deploy is Deployer { artifacts.save("PermissionedDisputeGame", address(deployOutput.permissionedDisputeGame)); artifacts.save("OptimismPortalProxy", address(deployOutput.optimismPortalProxy)); artifacts.save("OptimismPortal2Proxy", address(deployOutput.optimismPortalProxy)); - // Check if the permissionless game implementation is already set IDisputeGameFactory factory = IDisputeGameFactory(artifacts.mustGetAddress("DisputeGameFactoryProxy")); address permissionlessGameImpl = address(factory.gameImpls(GameTypes.CANNON)); @@ -521,10 +516,10 @@ contract Deploy is Deployer { l1CrossDomainMessenger: artifacts.mustGetAddress("L1CrossDomainMessengerProxy"), l1ERC721Bridge: artifacts.mustGetAddress("L1ERC721BridgeProxy"), l1StandardBridge: artifacts.mustGetAddress("L1StandardBridgeProxy"), - disputeGameFactory: artifacts.mustGetAddress("DisputeGameFactoryProxy"), optimismPortal: artifacts.mustGetAddress("OptimismPortalProxy"), optimismMintableERC20Factory: artifacts.mustGetAddress("OptimismMintableERC20FactoryProxy") - }) + }), + cfg.l2ChainID() ) ) }); @@ -961,7 +956,7 @@ contract Deploy is Deployer { blobBasefeeScalar: cfg.blobbasefeeScalar(), l2ChainId: cfg.l2ChainID(), startingAnchorRoot: abi.encode( - OutputRoot({ root: Hash.wrap(cfg.faultGameGenesisOutputRoot()), l2BlockNumber: cfg.faultGameGenesisBlock() }) + Proposal({ root: Hash.wrap(cfg.faultGameGenesisOutputRoot()), l2SequenceNumber: cfg.faultGameGenesisBlock() }) ), saltMixer: saltMixer, gasLimit: uint64(cfg.l2GenesisBlockGasLimit()), diff --git a/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol index 5cd0414734..0596ffae98 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol @@ -24,14 +24,13 @@ import { IOPContractsManagerUpgrader, IOPContractsManagerContractsContainer } from "interfaces/L1/IOPContractsManager.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; +import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPortal2.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.sol"; import { IL1ERC721Bridge } from "interfaces/L1/IL1ERC721Bridge.sol"; import { IL1StandardBridge } from "interfaces/L1/IL1StandardBridge.sol"; import { IOptimismMintableERC20Factory } from "interfaces/universal/IOptimismMintableERC20Factory.sol"; -import { IOptimismPortalInterop } from "interfaces/L1/IOptimismPortalInterop.sol"; -import { ISystemConfigInterop } from "interfaces/L1/ISystemConfigInterop.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; import { Solarray } from "scripts/libraries/Solarray.sol"; @@ -158,7 +157,8 @@ contract DeployImplementationsOutput is BaseDeployIO { IOPContractsManagerDeployer internal _opcmDeployer; IOPContractsManagerUpgrader internal _opcmUpgrader; IDelayedWETH internal _delayedWETHImpl; - IOptimismPortal2 internal _optimismPortalImpl; + IOptimismPortal internal _optimismPortalImpl; + IETHLockbox internal _ethLockboxImpl; IPreimageOracle internal _preimageOracleSingleton; IMIPS internal _mipsSingleton; ISystemConfig internal _systemConfigImpl; @@ -182,7 +182,8 @@ contract DeployImplementationsOutput is BaseDeployIO { else if (_sel == this.opcmUpgrader.selector) _opcmUpgrader = IOPContractsManagerUpgrader(_addr); else if (_sel == this.superchainConfigImpl.selector) _superchainConfigImpl = ISuperchainConfig(_addr); else if (_sel == this.protocolVersionsImpl.selector) _protocolVersionsImpl = IProtocolVersions(_addr); - else if (_sel == this.optimismPortalImpl.selector) _optimismPortalImpl = IOptimismPortal2(payable(_addr)); + else if (_sel == this.optimismPortalImpl.selector) _optimismPortalImpl = IOptimismPortal(payable(_addr)); + else if (_sel == this.ethLockboxImpl.selector) _ethLockboxImpl = IETHLockbox(payable(_addr)); else if (_sel == this.delayedWETHImpl.selector) _delayedWETHImpl = IDelayedWETH(payable(_addr)); else if (_sel == this.preimageOracleSingleton.selector) _preimageOracleSingleton = IPreimageOracle(_addr); else if (_sel == this.mipsSingleton.selector) _mipsSingleton = IMIPS(_addr); @@ -217,7 +218,8 @@ contract DeployImplementationsOutput is BaseDeployIO { address(this.l1StandardBridgeImpl()), address(this.optimismMintableERC20FactoryImpl()), address(this.disputeGameFactoryImpl()), - address(this.anchorStateRegistryImpl()) + address(this.anchorStateRegistryImpl()), + address(this.ethLockboxImpl()) ); DeployUtils.assertValidContractAddresses(Solarray.extend(addrs1, addrs2)); @@ -260,11 +262,16 @@ contract DeployImplementationsOutput is BaseDeployIO { return _protocolVersionsImpl; } - function optimismPortalImpl() public view returns (IOptimismPortal2) { + function optimismPortalImpl() public view returns (IOptimismPortal) { DeployUtils.assertValidContractAddress(address(_optimismPortalImpl)); return _optimismPortalImpl; } + function ethLockboxImpl() public view returns (IETHLockbox) { + DeployUtils.assertValidContractAddress(address(_ethLockboxImpl)); + return _ethLockboxImpl; + } + function delayedWETHImpl() public view returns (IDelayedWETH) { DeployUtils.assertValidContractAddress(address(_delayedWETHImpl)); return _delayedWETHImpl; @@ -327,6 +334,7 @@ contract DeployImplementationsOutput is BaseDeployIO { assertValidOpcm(_dii); assertValidOptimismMintableERC20FactoryImpl(_dii); assertValidOptimismPortalImpl(_dii); + assertValidETHLockboxImpl(_dii); assertValidPreimageOracleSingleton(_dii); assertValidSystemConfigImpl(_dii); } @@ -339,11 +347,11 @@ contract DeployImplementationsOutput is BaseDeployIO { } function assertValidOptimismPortalImpl(DeployImplementationsInput) internal view { - IOptimismPortal2 portal = optimismPortalImpl(); + IOptimismPortal portal = optimismPortalImpl(); DeployUtils.assertInitialized({ _contractAddress: address(portal), _isProxy: false, _slot: 0, _offset: 0 }); - require(address(portal.disputeGameFactory()) == address(0), "PORTAL-10"); + require(address(portal.anchorStateRegistry()) == address(0), "PORTAL-10"); require(address(portal.systemConfig()) == address(0), "PORTAL-20"); require(address(portal.superchainConfig()) == address(0), "PORTAL-30"); require(portal.l2Sender() == address(0), "PORTAL-40"); @@ -351,6 +359,17 @@ contract DeployImplementationsOutput is BaseDeployIO { // This slot is the custom gas token _balance and this check ensures // that it stays unset for forwards compatibility with custom gas token. require(vm.load(address(portal), bytes32(uint256(61))) == bytes32(0), "PORTAL-50"); + + require(address(portal.ethLockbox()) == address(0), "PORTAL-60"); + } + + function assertValidETHLockboxImpl(DeployImplementationsInput) internal view { + IETHLockbox lockbox = ethLockboxImpl(); + + DeployUtils.assertInitialized({ _contractAddress: address(lockbox), _isProxy: false, _slot: 0, _offset: 0 }); + + require(address(lockbox.superchainConfig()) == address(0), "ELB-10"); + require(lockbox.authorizedPortals(optimismPortalImpl()) == false, "ELB-20"); } function assertValidDelayedWETHImpl(DeployImplementationsInput _dii) internal view { @@ -402,9 +421,8 @@ contract DeployImplementationsOutput is BaseDeployIO { require(systemConfig.l1CrossDomainMessenger() == address(0), "SYSCON-170"); require(systemConfig.l1ERC721Bridge() == address(0), "SYSCON-180"); require(systemConfig.l1StandardBridge() == address(0), "SYSCON-190"); - require(systemConfig.disputeGameFactory() == address(0), "SYSCON-200"); - require(systemConfig.optimismPortal() == address(0), "SYSCON-210"); - require(systemConfig.optimismMintableERC20Factory() == address(0), "SYSCON-220"); + require(systemConfig.optimismPortal() == address(0), "SYSCON-200"); + require(systemConfig.optimismMintableERC20Factory() == address(0), "SYSCON-210"); } function assertValidL1CrossDomainMessengerImpl(DeployImplementationsInput) internal view { @@ -485,11 +503,12 @@ contract DeployImplementations is Script { deployL1StandardBridgeImpl(_dio); deployOptimismMintableERC20FactoryImpl(_dio); deployOptimismPortalImpl(_dii, _dio); + deployETHLockboxImpl(_dio); deployDelayedWETHImpl(_dii, _dio); deployPreimageOracleSingleton(_dii, _dio); deployMipsSingleton(_dii, _dio); deployDisputeGameFactoryImpl(_dio); - deployAnchorStateRegistryImpl(_dio); + deployAnchorStateRegistryImpl(_dii, _dio); // Deploy the OP Contracts Manager with the new implementations set. deployOPContractsManager(_dii, _dio); @@ -516,6 +535,7 @@ contract DeployImplementations is Script { protocolVersionsImpl: address(_dio.protocolVersionsImpl()), l1ERC721BridgeImpl: address(_dio.l1ERC721BridgeImpl()), optimismPortalImpl: address(_dio.optimismPortalImpl()), + ethLockboxImpl: address(_dio.ethLockboxImpl()), systemConfigImpl: address(_dio.systemConfigImpl()), optimismMintableERC20FactoryImpl: address(_dio.optimismMintableERC20FactoryImpl()), l1CrossDomainMessengerImpl: address(_dio.l1CrossDomainMessengerImpl()), @@ -680,6 +700,18 @@ contract DeployImplementations is Script { _dio.set(_dio.optimismMintableERC20FactoryImpl.selector, address(impl)); } + function deployETHLockboxImpl(DeployImplementationsOutput _dio) public virtual { + IETHLockbox impl = IETHLockbox( + DeployUtils.createDeterministic({ + _name: "ETHLockbox", + _args: DeployUtils.encodeConstructor(abi.encodeCall(IETHLockbox.__constructor__, ())), + _salt: _salt + }) + ); + vm.label(address(impl), "ETHLockboxImpl"); + _dio.set(_dio.ethLockboxImpl.selector, address(impl)); + } + // --- Fault Proofs Contracts --- // The fault proofs contracts are configured as follows: @@ -725,14 +757,11 @@ contract DeployImplementations is Script { virtual { uint256 proofMaturityDelaySeconds = _dii.proofMaturityDelaySeconds(); - uint256 disputeGameFinalityDelaySeconds = _dii.disputeGameFinalityDelaySeconds(); - IOptimismPortal2 impl = IOptimismPortal2( + IOptimismPortal impl = IOptimismPortal( DeployUtils.createDeterministic({ _name: "OptimismPortal2", _args: DeployUtils.encodeConstructor( - abi.encodeCall( - IOptimismPortal2.__constructor__, (proofMaturityDelaySeconds, disputeGameFinalityDelaySeconds) - ) + abi.encodeCall(IOptimismPortal.__constructor__, (proofMaturityDelaySeconds)) ), _salt: _salt }) @@ -810,11 +839,20 @@ contract DeployImplementations is Script { _dio.set(_dio.disputeGameFactoryImpl.selector, address(impl)); } - function deployAnchorStateRegistryImpl(DeployImplementationsOutput _dio) public virtual { + function deployAnchorStateRegistryImpl( + DeployImplementationsInput _dii, + DeployImplementationsOutput _dio + ) + public + virtual + { + uint256 disputeGameFinalityDelaySeconds = _dii.disputeGameFinalityDelaySeconds(); IAnchorStateRegistry impl = IAnchorStateRegistry( DeployUtils.createDeterministic({ _name: "AnchorStateRegistry", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IAnchorStateRegistry.__constructor__, ())), + _args: DeployUtils.encodeConstructor( + abi.encodeCall(IAnchorStateRegistry.__constructor__, (disputeGameFinalityDelaySeconds)) + ), _salt: _salt }) ); @@ -907,88 +945,3 @@ contract DeployImplementations is Script { dio_ = DeployImplementationsOutput(DeployUtils.toIOAddress(msg.sender, "optimism.DeployImplementationsOutput")); } } - -// Similar to how DeploySuperchain.s.sol contains a lot of comments to thoroughly document the script -// architecture, this comment block documents how to update the deploy scripts to support new features. -// -// Using the base scripts and contracts (DeploySuperchain, DeployImplementations, DeployOPChain, and -// the corresponding OPContractsManager) deploys a standard chain. For nonstandard and in-development -// features we need to modify some or all of those contracts, and we do that via inheritance. Using -// interop as an example, they've made the following changes to L1 contracts: -// - `OptimismPortalInterop is OptimismPortal`: A different portal implementation is used, and -// it's ABI is the same. -// - `SystemConfigInterop is SystemConfig`: A different system config implementation is used, and -// it's initializer has a different signature. This signature is different because there is a -// new input parameter, the `dependencyManager`. -// - Because of the different system config initializer, there is a new input parameter (dependencyManager). -// -// Similar to how inheritance was used to develop the new portal and system config contracts, we use -// inheritance to modify up to all of the deployer contracts. For this interop example, what this -// means is we need: -// - An `OPContractsManagerInterop is OPContractsManager` that knows how to encode the calldata for the -// new system config initializer. -// - A `DeployImplementationsInterop is DeployImplementations` that: -// - Deploys OptimismPortalInterop instead of OptimismPortal. -// - Deploys SystemConfigInterop instead of SystemConfig. -// - Deploys OPContractsManagerInterop instead of OPContractsManager, which contains the updated logic -// for encoding the SystemConfig initializer. -// - Updates the OPCM release setter logic to use the updated initializer. -// - A `DeployOPChainInterop is DeployOPChain` that allows the updated input parameter to be passed. -// -// Most of the complexity in the above flow comes from the the new input for the updated SystemConfig -// initializer. If all function signatures were the same, all we'd have to change is the contract -// implementations that are deployed then set in the OPCM. For now, to simplify things until we -// resolve https://github.com/ethereum-optimism/optimism/issues/11783, we just assume this new role -// is the same as the proxy admin owner. -contract DeployImplementationsInterop is DeployImplementations { - function deployOPCMDeployer(DeployImplementationsOutput _dio) public override { - IOPContractsManagerDeployer impl = IOPContractsManagerDeployer( - DeployUtils.createDeterministic({ - _name: "OPContractsManager.sol:OPContractsManagerDeployerInterop", - _args: DeployUtils.encodeConstructor( - abi.encodeCall(IOPContractsManagerDeployer.__constructor__, (_dio.opcmContractsContainer())) - ), - _salt: _salt - }) - ); - vm.label(address(impl), "OPContractsManagerDeployerImpl"); - _dio.set(_dio.opcmDeployer.selector, address(impl)); - } - - function deployOptimismPortalImpl( - DeployImplementationsInput _dii, - DeployImplementationsOutput _dio - ) - public - override - { - uint256 proofMaturityDelaySeconds = _dii.proofMaturityDelaySeconds(); - uint256 disputeGameFinalityDelaySeconds = _dii.disputeGameFinalityDelaySeconds(); - IOptimismPortalInterop impl = IOptimismPortalInterop( - DeployUtils.createDeterministic({ - _name: "OptimismPortalInterop", - _args: DeployUtils.encodeConstructor( - abi.encodeCall( - IOptimismPortalInterop.__constructor__, (proofMaturityDelaySeconds, disputeGameFinalityDelaySeconds) - ) - ), - _salt: _salt - }) - ); - - vm.label(address(impl), "OptimismPortalImpl"); - _dio.set(_dio.optimismPortalImpl.selector, address(impl)); - } - - function deploySystemConfigImpl(DeployImplementationsOutput _dio) public override { - ISystemConfigInterop impl = ISystemConfigInterop( - DeployUtils.createDeterministic({ - _name: "SystemConfigInterop", - _args: DeployUtils.encodeConstructor(abi.encodeCall(ISystemConfigInterop.__constructor__, ())), - _salt: _salt - }) - ); - vm.label(address(impl), "SystemConfigImpl"); - _dio.set(_dio.systemConfigImpl.selector, address(impl)); - } -} diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index 9541db0bba..fb2c104c1f 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -27,12 +27,13 @@ import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { IPermissionedDisputeGame } from "interfaces/dispute/IPermissionedDisputeGame.sol"; import { Claim, Duration, GameType, GameTypes, Hash } from "src/dispute/lib/Types.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; +import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPortal2.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.sol"; import { IL1ERC721Bridge } from "interfaces/L1/IL1ERC721Bridge.sol"; import { IL1StandardBridge } from "interfaces/L1/IL1StandardBridge.sol"; import { IOptimismMintableERC20Factory } from "interfaces/universal/IOptimismMintableERC20Factory.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; contract DeployOPChainInput is BaseDeployIO { address internal _opChainProxyAdminOwner; @@ -239,7 +240,8 @@ contract DeployOPChainOutput is BaseDeployIO { IOptimismMintableERC20Factory internal _optimismMintableERC20FactoryProxy; IL1StandardBridge internal _l1StandardBridgeProxy; IL1CrossDomainMessenger internal _l1CrossDomainMessengerProxy; - IOptimismPortal2 internal _optimismPortalProxy; + IOptimismPortal internal _optimismPortalProxy; + IETHLockbox internal _ethLockboxProxy; IDisputeGameFactory internal _disputeGameFactoryProxy; IAnchorStateRegistry internal _anchorStateRegistryProxy; IFaultDisputeGame internal _faultDisputeGame; @@ -257,7 +259,8 @@ contract DeployOPChainOutput is BaseDeployIO { else if (_sel == this.optimismMintableERC20FactoryProxy.selector) _optimismMintableERC20FactoryProxy = IOptimismMintableERC20Factory(_addr) ; else if (_sel == this.l1StandardBridgeProxy.selector) _l1StandardBridgeProxy = IL1StandardBridge(payable(_addr)) ; else if (_sel == this.l1CrossDomainMessengerProxy.selector) _l1CrossDomainMessengerProxy = IL1CrossDomainMessenger(_addr) ; - else if (_sel == this.optimismPortalProxy.selector) _optimismPortalProxy = IOptimismPortal2(payable(_addr)) ; + else if (_sel == this.optimismPortalProxy.selector) _optimismPortalProxy = IOptimismPortal(payable(_addr)) ; + else if (_sel == this.ethLockboxProxy.selector) _ethLockboxProxy = IETHLockbox(payable(_addr)) ; else if (_sel == this.disputeGameFactoryProxy.selector) _disputeGameFactoryProxy = IDisputeGameFactory(_addr) ; else if (_sel == this.anchorStateRegistryProxy.selector) _anchorStateRegistryProxy = IAnchorStateRegistry(_addr) ; else if (_sel == this.faultDisputeGame.selector) _faultDisputeGame = IFaultDisputeGame(_addr) ; @@ -308,12 +311,18 @@ contract DeployOPChainOutput is BaseDeployIO { return _l1CrossDomainMessengerProxy; } - function optimismPortalProxy() public returns (IOptimismPortal2) { + function optimismPortalProxy() public returns (IOptimismPortal) { DeployUtils.assertValidContractAddress(address(_optimismPortalProxy)); DeployUtils.assertERC1967ImplementationSet(address(_optimismPortalProxy)); return _optimismPortalProxy; } + function ethLockboxProxy() public returns (IETHLockbox) { + DeployUtils.assertValidContractAddress(address(_ethLockboxProxy)); + DeployUtils.assertERC1967ImplementationSet(address(_ethLockboxProxy)); + return _ethLockboxProxy; + } + function disputeGameFactoryProxy() public returns (IDisputeGameFactory) { DeployUtils.assertValidContractAddress(address(_disputeGameFactoryProxy)); DeployUtils.assertERC1967ImplementationSet(address(_disputeGameFactoryProxy)); @@ -390,6 +399,7 @@ contract DeployOPChain is Script { vm.label(address(deployOutput.l1StandardBridgeProxy), "l1StandardBridgeProxy"); vm.label(address(deployOutput.l1CrossDomainMessengerProxy), "l1CrossDomainMessengerProxy"); vm.label(address(deployOutput.optimismPortalProxy), "optimismPortalProxy"); + vm.label(address(deployOutput.ethLockboxProxy), "ethLockboxProxy"); vm.label(address(deployOutput.disputeGameFactoryProxy), "disputeGameFactoryProxy"); vm.label(address(deployOutput.anchorStateRegistryProxy), "anchorStateRegistryProxy"); // vm.label(address(deployOutput.faultDisputeGame), "faultDisputeGame"); @@ -408,6 +418,7 @@ contract DeployOPChain is Script { _doo.set(_doo.l1StandardBridgeProxy.selector, address(deployOutput.l1StandardBridgeProxy)); _doo.set(_doo.l1CrossDomainMessengerProxy.selector, address(deployOutput.l1CrossDomainMessengerProxy)); _doo.set(_doo.optimismPortalProxy.selector, address(deployOutput.optimismPortalProxy)); + _doo.set(_doo.ethLockboxProxy.selector, address(deployOutput.ethLockboxProxy)); _doo.set(_doo.disputeGameFactoryProxy.selector, address(deployOutput.disputeGameFactoryProxy)); _doo.set(_doo.anchorStateRegistryProxy.selector, address(deployOutput.anchorStateRegistryProxy)); // _doo.set(_doo.faultDisputeGame.selector, address(deployOutput.faultDisputeGame)); @@ -440,7 +451,8 @@ contract DeployOPChain is Script { address(_doo.anchorStateRegistryProxy()), address(_doo.permissionedDisputeGame()), // address(_doo.faultDisputeGame()), - address(_doo.delayedWETHPermissionedGameProxy()) + address(_doo.delayedWETHPermissionedGameProxy()), + address(_doo.ethLockboxProxy()) ); // TODO: Eventually switch from Permissioned to Permissionless. Add this address back in. // address(_delayedWETHPermissionlessGameProxy) @@ -459,6 +471,7 @@ contract DeployOPChain is Script { assertValidL1StandardBridge(_doi, _doo); assertValidOptimismMintableERC20Factory(_doi, _doo); assertValidOptimismPortal(_doi, _doo); + assertValidETHLockbox(_doi, _doo); assertValidPermissionedDisputeGame(_doi, _doo); assertValidSystemConfig(_doi, _doo); assertValidAddressManager(_doi, _doo); @@ -549,11 +562,10 @@ contract DeployOPChain is Script { require(systemConfig.l1CrossDomainMessenger() == address(_doo.l1CrossDomainMessengerProxy()), "SYSCON-160"); require(systemConfig.l1ERC721Bridge() == address(_doo.l1ERC721BridgeProxy()), "SYSCON-170"); require(systemConfig.l1StandardBridge() == address(_doo.l1StandardBridgeProxy()), "SYSCON-180"); - require(systemConfig.disputeGameFactory() == address(_doo.disputeGameFactoryProxy()), "SYSCON-190"); - require(systemConfig.optimismPortal() == address(_doo.optimismPortalProxy()), "SYSCON-200"); + require(systemConfig.optimismPortal() == address(_doo.optimismPortalProxy()), "SYSCON-190"); require( systemConfig.optimismMintableERC20Factory() == address(_doo.optimismMintableERC20FactoryProxy()), - "SYSCON-210" + "SYSCON-200" ); } @@ -609,19 +621,32 @@ contract DeployOPChain is Script { } function assertValidOptimismPortal(DeployOPChainInput _doi, DeployOPChainOutput _doo) internal { - IOptimismPortal2 portal = _doo.optimismPortalProxy(); + IOptimismPortal portal = _doo.optimismPortalProxy(); ISuperchainConfig superchainConfig = ISuperchainConfig(address(_doi.opcm().superchainConfig())); - require(address(portal.disputeGameFactory()) == address(_doo.disputeGameFactoryProxy()), "PORTAL-10"); - require(address(portal.systemConfig()) == address(_doo.systemConfigProxy()), "PORTAL-20"); - require(address(portal.superchainConfig()) == address(superchainConfig), "PORTAL-30"); - require(portal.guardian() == superchainConfig.guardian(), "PORTAL-40"); - require(portal.paused() == superchainConfig.paused(), "PORTAL-50"); - require(portal.l2Sender() == Constants.DEFAULT_L2_SENDER, "PORTAL-60"); + require(address(portal.anchorStateRegistry()) == address(_doo.anchorStateRegistryProxy()), "PORTAL-10"); + require(address(portal.disputeGameFactory()) == address(_doo.disputeGameFactoryProxy()), "PORTAL-20"); + require(address(portal.systemConfig()) == address(_doo.systemConfigProxy()), "PORTAL-30"); + require(address(portal.superchainConfig()) == address(superchainConfig), "PORTAL-40"); + require(portal.guardian() == superchainConfig.guardian(), "PORTAL-50"); + require(portal.paused() == superchainConfig.paused(), "PORTAL-60"); + require(portal.l2Sender() == Constants.DEFAULT_L2_SENDER, "PORTAL-70"); // This slot is the custom gas token _balance and this check ensures // that it stays unset for forwards compatibility with custom gas token. - require(vm.load(address(portal), bytes32(uint256(61))) == bytes32(0), "PORTAL-70"); + require(vm.load(address(portal), bytes32(uint256(61))) == bytes32(0), "PORTAL-80"); + + // Check once the portal is updated to use the new lockbox. + require(address(portal.ethLockbox()) == address(_doo.ethLockboxProxy()), "PORTAL-90"); + require(portal.proxyAdminOwner() == _doi.opChainProxyAdminOwner(), "PORTAL-100"); + } + + function assertValidETHLockbox(DeployOPChainInput _doi, DeployOPChainOutput _doo) internal { + IETHLockbox lockbox = _doo.ethLockboxProxy(); + + require(address(lockbox.superchainConfig()) == address(_doi.opcm().superchainConfig()), "ETHLOCKBOX-10"); + require(lockbox.authorizedPortals(_doo.optimismPortalProxy()), "ETHLOCKBOX-20"); + require(lockbox.proxyAdminOwner() == _doi.opChainProxyAdminOwner(), "ETHLOCKBOX-30"); } function assertValidDisputeGameFactory(DeployOPChainInput _doi, DeployOPChainOutput _doo) internal { @@ -702,6 +727,11 @@ contract DeployOPChain is Script { == DeployUtils.assertERC1967ImplementationSet(address(_doo.anchorStateRegistryProxy())), "OPCPA-110" ); + require( + admin.getProxyImplementation(address(_doo.ethLockboxProxy())) + == DeployUtils.assertERC1967ImplementationSet(address(_doo.ethLockboxProxy())), + "OPCPA-120" + ); } // -------- Utilities -------- diff --git a/packages/contracts-bedrock/scripts/deploy/DeployStandardValidator.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployStandardValidator.s.sol index 3e955d10b3..eb22a9cad5 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployStandardValidator.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployStandardValidator.s.sol @@ -13,7 +13,8 @@ import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IStandardValidatorBase, IStandardValidatorV180, - IStandardValidatorV200 + IStandardValidatorV200, + IStandardValidatorV300 } from "interfaces/L1/IStandardValidator.sol"; /// @title DeployStandardValidatorInput @@ -245,6 +246,8 @@ contract DeployStandardValidator is Script { validator = deployValidatorV180(_si); } else if (keccak256(bytes(_si.release())) == keccak256(bytes("v2.0.0"))) { validator = deployValidatorV200(_si); + } else if (keccak256(bytes(_si.release())) == keccak256(bytes("v3.0.0"))) { + validator = deployValidatorV300(_si); } else { revert("DeployStandardValidator: invalid release version"); } @@ -284,6 +287,22 @@ contract DeployStandardValidator is Script { return validator; } + function deployValidatorV300(DeployStandardValidatorInput _si) internal returns (address) { + address validator = DeployUtils.createDeterministic({ + _name: "StandardValidator.sol:StandardValidatorV300", + _args: DeployUtils.encodeConstructor( + abi.encodeCall( + IStandardValidatorV300.__constructor__, + (getImplementations(_si), _si.superchainConfig(), _si.l1PAOMultisig(), _si.mips(), _si.challenger()) + ) + ), + _salt: DeployUtils.DEFAULT_SALT + }); + + vm.label(validator, "StandardValidatorV300"); + return validator; + } + function assertValidDeploy(DeployStandardValidatorInput _si, DeployStandardValidatorOutput _so) public view { DeployUtils.assertValidContractAddress(_so.validator()); assertValidValidator(_si, _so); diff --git a/packages/contracts-bedrock/scripts/deploy/ManageDependencies.s.sol b/packages/contracts-bedrock/scripts/deploy/ManageDependencies.s.sol deleted file mode 100644 index 1e8bdc2a7c..0000000000 --- a/packages/contracts-bedrock/scripts/deploy/ManageDependencies.s.sol +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import { Script } from "forge-std/Script.sol"; -import { BaseDeployIO } from "scripts/deploy/BaseDeployIO.sol"; -import { ISystemConfigInterop } from "interfaces/L1/ISystemConfigInterop.sol"; - -contract ManageDependenciesInput is BaseDeployIO { - uint256 internal _chainId; - ISystemConfigInterop _systemConfig; - bool internal _remove; - - // Setter for uint256 type - function set(bytes4 _sel, uint256 _value) public { - if (_sel == this.chainId.selector) _chainId = _value; - else revert("ManageDependenciesInput: unknown selector"); - } - - // Setter for address type - function set(bytes4 _sel, address _addr) public { - require(_addr != address(0), "ManageDependenciesInput: cannot set zero address"); - - if (_sel == this.systemConfig.selector) _systemConfig = ISystemConfigInterop(_addr); - else revert("ManageDependenciesInput: unknown selector"); - } - - // Setter for bool type - function set(bytes4 _sel, bool _value) public { - if (_sel == this.remove.selector) _remove = _value; - else revert("ManageDependenciesInput: unknown selector"); - } - - // Getters - function chainId() public view returns (uint256) { - require(_chainId > 0, "ManageDependenciesInput: not set"); - return _chainId; - } - - function systemConfig() public view returns (ISystemConfigInterop) { - require(address(_systemConfig) != address(0), "ManageDependenciesInput: not set"); - return _systemConfig; - } - - function remove() public view returns (bool) { - return _remove; - } -} - -contract ManageDependencies is Script { - function run(ManageDependenciesInput _input) public { - bool remove = _input.remove(); - uint256 chainId = _input.chainId(); - ISystemConfigInterop systemConfig = _input.systemConfig(); - - // Call the appropriate function based on the remove flag - vm.broadcast(msg.sender); - if (remove) { - systemConfig.removeDependency(chainId); - } else { - systemConfig.addDependency(chainId); - } - } -} diff --git a/packages/contracts-bedrock/scripts/deploy/ReadImplementationAddresses.s.sol b/packages/contracts-bedrock/scripts/deploy/ReadImplementationAddresses.s.sol index 211281b506..e7b7f76edf 100644 --- a/packages/contracts-bedrock/scripts/deploy/ReadImplementationAddresses.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/ReadImplementationAddresses.s.sol @@ -30,6 +30,7 @@ contract ReadImplementationAddressesInput is DeployOPChainOutput { contract ReadImplementationAddressesOutput is BaseDeployIO { address internal _delayedWETH; address internal _optimismPortal; + address internal _ethLockbox; address internal _systemConfig; address internal _l1CrossDomainMessenger; address internal _l1ERC721Bridge; @@ -43,6 +44,7 @@ contract ReadImplementationAddressesOutput is BaseDeployIO { require(_addr != address(0), "ReadImplementationAddressesOutput: cannot set zero address"); if (_sel == this.delayedWETH.selector) _delayedWETH = _addr; else if (_sel == this.optimismPortal.selector) _optimismPortal = _addr; + else if (_sel == this.ethLockbox.selector) _ethLockbox = _addr; else if (_sel == this.systemConfig.selector) _systemConfig = _addr; else if (_sel == this.l1CrossDomainMessenger.selector) _l1CrossDomainMessenger = _addr; else if (_sel == this.l1ERC721Bridge.selector) _l1ERC721Bridge = _addr; @@ -64,6 +66,11 @@ contract ReadImplementationAddressesOutput is BaseDeployIO { return _optimismPortal; } + function ethLockbox() public view returns (address) { + require(_ethLockbox != address(0), "ReadImplementationAddressesOutput: ethLockbox not set"); + return _ethLockbox; + } + function systemConfig() public view returns (address) { require(_systemConfig != address(0), "ReadImplementationAddressesOutput: systemConfig not set"); return _systemConfig; @@ -154,5 +161,8 @@ contract ReadImplementationAddresses is Script { address preimageOracle = address(IMIPS(mipsLogic).oracle()); _rio.set(_rio.preimageOracleSingleton.selector, preimageOracle); + + address ethLockbox = _rii.opcm().implementations().ethLockboxImpl; + _rio.set(_rio.ethLockbox.selector, ethLockbox); } } diff --git a/packages/contracts-bedrock/scripts/deploy/SetDisputeGameImpl.s.sol b/packages/contracts-bedrock/scripts/deploy/SetDisputeGameImpl.s.sol index 17e3d5a99d..77d4880f02 100644 --- a/packages/contracts-bedrock/scripts/deploy/SetDisputeGameImpl.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/SetDisputeGameImpl.s.sol @@ -7,11 +7,11 @@ import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { BaseDeployIO } from "scripts/deploy/BaseDeployIO.sol"; import { GameType } from "src/dispute/lib/Types.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; +import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; contract SetDisputeGameImplInput is BaseDeployIO { IDisputeGameFactory internal _factory; - IOptimismPortal2 internal _portal; + IAnchorStateRegistry internal _anchorStateRegistry; IFaultDisputeGame internal _impl; uint32 internal _gameType; @@ -20,7 +20,7 @@ contract SetDisputeGameImplInput is BaseDeployIO { require(_addr != address(0), "SetDisputeGameImplInput: cannot set zero address"); if (_sel == this.factory.selector) _factory = IDisputeGameFactory(_addr); - else if (_sel == this.portal.selector) _portal = IOptimismPortal2(payable(_addr)); + else if (_sel == this.anchorStateRegistry.selector) _anchorStateRegistry = IAnchorStateRegistry(_addr); else if (_sel == this.impl.selector) _impl = IFaultDisputeGame(_addr); else revert("SetDisputeGameImplInput: unknown selector"); } @@ -36,8 +36,8 @@ contract SetDisputeGameImplInput is BaseDeployIO { return _factory; } - function portal() public view returns (IOptimismPortal2) { - return _portal; + function anchorStateRegistry() public view returns (IAnchorStateRegistry) { + return _anchorStateRegistry; } function impl() public view returns (IFaultDisputeGame) { @@ -57,15 +57,15 @@ contract SetDisputeGameImpl is Script { require(address(factory.gameImpls(gameType)) == address(0), "SDGI-10"); IFaultDisputeGame impl = _input.impl(); - IOptimismPortal2 portal = _input.portal(); + IAnchorStateRegistry anchorStateRegistry = _input.anchorStateRegistry(); vm.broadcast(msg.sender); factory.setImplementation(gameType, impl); - if (address(portal) != address(0)) { - require(address(portal.disputeGameFactory()) == address(factory), "SDGI-20"); + if (address(anchorStateRegistry) != address(0)) { + require(address(anchorStateRegistry.disputeGameFactory()) == address(factory), "SDGI-20"); vm.broadcast(msg.sender); - portal.setRespectedGameType(gameType); + anchorStateRegistry.setRespectedGameType(gameType); } assertValid(_input); @@ -75,9 +75,12 @@ contract SetDisputeGameImpl is Script { GameType gameType = GameType.wrap(_input.gameType()); require(address(_input.factory().gameImpls(gameType)) == address(_input.impl()), "SDGI-30"); - if (address(_input.portal()) != address(0)) { - require(address(_input.portal().disputeGameFactory()) == address(_input.factory()), "SDGI-40"); - require(GameType.unwrap(_input.portal().respectedGameType()) == GameType.unwrap(gameType), "SDGI-50"); + if (address(_input.anchorStateRegistry()) != address(0)) { + require(address(_input.anchorStateRegistry().disputeGameFactory()) == address(_input.factory()), "SDGI-40"); + require( + GameType.unwrap(_input.anchorStateRegistry().respectedGameType()) == GameType.unwrap(gameType), + "SDGI-50" + ); } } } diff --git a/packages/contracts-bedrock/scripts/go-ffi/differential-testing.go b/packages/contracts-bedrock/scripts/go-ffi/differential-testing.go index e35d2a82c3..7c328d23f2 100644 --- a/packages/contracts-bedrock/scripts/go-ffi/differential-testing.go +++ b/packages/contracts-bedrock/scripts/go-ffi/differential-testing.go @@ -38,14 +38,15 @@ var ( {Type: fixedBytes}, } - uint32Type, _ = abi.NewType("uint32", "", nil) - // Plain address type addressType, _ = abi.NewType("address", "", nil) // Plain uint8 type uint8Type, _ = abi.NewType("uint8", "", nil) + // Plain uint32 type + uint32Type, _ = abi.NewType("uint32", "", nil) + // Plain uint256 type uint256Type, _ = abi.NewType("uint256", "", nil) @@ -102,6 +103,19 @@ var ( {Name: "symbol", Type: fixedBytes}, } + // Super root proof tuple (uint8, uint64, OutputRootWithChainId[]) + superRootProof, _ = abi.NewType("tuple", "SuperRootProof", []abi.ArgumentMarshaling{ + {Name: "version", Type: "bytes1"}, + {Name: "timestamp", Type: "uint64"}, + {Name: "outputRoots", Type: "tuple[]", Components: []abi.ArgumentMarshaling{ + {Name: "chainId", Type: "uint256"}, + {Name: "root", Type: "bytes32"}, + }}, + }) + superRootProofArgs = abi.Arguments{ + {Type: superRootProof}, + } + // Dependency tuple (uint256) dependencyArgs = abi.Arguments{{Name: "chainId", Type: uint256Type}} ) @@ -520,6 +534,49 @@ func DiffTestUtils() { packed, err := bytesArgs.Pack(&encoded) checkErr(err, "Error encoding output") + fmt.Print(hexutil.Encode(packed)) + case "encodeSuperRootProof": + // Parse input argument as abi encoded super root proof + if len(args) < 2 { + panic("Error: encodeSuperRoot requires at least 1 argument") + } + + // Parse the input as hex data + superRootProofData := common.FromHex(args[1]) + proof, err := parseSuperRootProof(superRootProofData) + checkErr(err, "Error parsing super root proof") + + // Encode super root proof + encoded, err := encodeSuperRootProof(proof) + checkErr(err, "Error encoding super root") + + // Pack encoded super root + packed, err := bytesArgs.Pack(&encoded) + checkErr(err, "Error encoding output") + + fmt.Print(hexutil.Encode(packed)) + case "hashSuperRootProof": + // Parse input argument as abi encoded super root proof + if len(args) < 2 { + panic("Error: hashSuperRootProof requires at least 1 argument") + } + + // Parse the input as hex data + superRootProofData := common.FromHex(args[1]) + proof, err := parseSuperRootProof(superRootProofData) + checkErr(err, "Error parsing super root proof") + + // Encode super root proof + encoded, err := encodeSuperRootProof(proof) + checkErr(err, "Error encoding super root proof") + + // Hash super root proof + hash := crypto.Keccak256Hash(encoded) + + // Pack hash + packed, err := fixedBytesArgs.Pack(&hash) + checkErr(err, "Error encoding output") + fmt.Print(hexutil.Encode(packed)) default: panic(fmt.Errorf("Unknown command: %s", args[0])) diff --git a/packages/contracts-bedrock/scripts/go-ffi/utils.go b/packages/contracts-bedrock/scripts/go-ffi/utils.go index 0c535c03fe..157dd4d85c 100644 --- a/packages/contracts-bedrock/scripts/go-ffi/utils.go +++ b/packages/contracts-bedrock/scripts/go-ffi/utils.go @@ -1,6 +1,7 @@ package main import ( + "encoding/binary" "errors" "fmt" "math/big" @@ -13,6 +14,18 @@ import ( "github.com/ethereum/go-ethereum/core/types" ) +type OutputRootWithChainId struct { + ChainId *big.Int + Root common.Hash +} + +// Define a proper type for SuperRootProof +type SuperRootProof struct { + Version uint8 + Timestamp uint64 + OutputRoots []OutputRootWithChainId +} + var UnknownNonceVersion = errors.New("Unknown nonce version") // checkOk checks if ok is false, and panics if so. @@ -50,6 +63,78 @@ func encodeCrossDomainMessage(nonce *big.Int, sender common.Address, target comm return encoded, err } +// parseSuperRootProof parses an abi encoded super root proof into a SuperRootProof struct. +func parseSuperRootProof(abiEncodedProof []byte) (*SuperRootProof, error) { + // Parse the input as hex data + unpacked, err := superRootProofArgs.Unpack(abiEncodedProof) + if err != nil { + return nil, err + } + + // The Unpack method returns a slice of interface{}, so we need to get the first element + if len(unpacked) != 1 { + return nil, errors.New("unexpected number of values after unpacking super root proof") + } + + // Use an anonymous struct matching the tuple’s layout. + tmp := unpacked[0].(struct { + Version [1]uint8 `json:"version"` + Timestamp uint64 `json:"timestamp"` + OutputRoots []struct { + ChainId *big.Int `json:"chainId"` + Root [32]byte `json:"root"` + } `json:"outputRoots"` + }) + + // Convert into our desired SuperRootProof type. + proof := SuperRootProof{ + Version: tmp.Version[0], + Timestamp: tmp.Timestamp, + } + for _, o := range tmp.OutputRoots { + proof.OutputRoots = append(proof.OutputRoots, OutputRootWithChainId{ + ChainId: o.ChainId, + Root: common.BytesToHash(o.Root[:]), + }) + } + + return &proof, nil +} + +// encodeSuperRootProof encodes a super root proof into a byte array. +func encodeSuperRootProof(superRootProof *SuperRootProof) ([]byte, error) { + // Version must match the expected version (0x01) + if superRootProof.Version != 0x01 { + return nil, errors.New("invalid super root version") + } + + // Output roots must not be empty + if len(superRootProof.OutputRoots) == 0 { + return nil, errors.New("empty super root") + } + + // Start with version byte and timestamp + encoded := []byte{superRootProof.Version} + + // Add timestamp as bytes8 (uint64) + timestampBytes := make([]byte, 8) + binary.BigEndian.PutUint64(timestampBytes, superRootProof.Timestamp) + encoded = append(encoded, timestampBytes...) + + // Add each output root (chainId + root) + for _, outputRoot := range superRootProof.OutputRoots { + // Append chainId bytes (padded to 32 bytes) + chainIdBytes := make([]byte, 32) + outputRoot.ChainId.FillBytes(chainIdBytes) + encoded = append(encoded, chainIdBytes...) + + // Append root hash (already 32 bytes) + encoded = append(encoded, outputRoot.Root.Bytes()...) + } + + return encoded, nil +} + // hashWithdrawal hashes a withdrawal transaction. func hashWithdrawal(nonce *big.Int, sender common.Address, target common.Address, value *big.Int, gasLimit *big.Int, data []byte) (common.Hash, error) { wd := crossdomain.Withdrawal{ diff --git a/packages/contracts-bedrock/scripts/libraries/Constants.sol b/packages/contracts-bedrock/scripts/libraries/Constants.sol index f98fdd3270..c7ba61822d 100644 --- a/packages/contracts-bedrock/scripts/libraries/Constants.sol +++ b/packages/contracts-bedrock/scripts/libraries/Constants.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import { OutputRoot, Hash } from "src/dispute/lib/Types.sol"; +import { Proposal, Hash } from "src/dispute/lib/Types.sol"; /// @title Constants /// @notice Constants is a library for storing constants. Simple! Don't put everything in here, just @@ -9,7 +9,7 @@ import { OutputRoot, Hash } from "src/dispute/lib/Types.sol"; /// should be defined in that contract instead. library Constants { /// @notice Returns the default starting anchor roots value to be used in a new dispute game. - function DEFAULT_OUTPUT_ROOT() internal pure returns (OutputRoot memory) { - return OutputRoot({ root: Hash.wrap(bytes32(hex"dead")), l2BlockNumber: 0 }); + function DEFAULT_OUTPUT_ROOT() internal pure returns (Proposal memory) { + return Proposal({ root: Hash.wrap(bytes32(hex"dead")), l2SequenceNumber: 0 }); } } diff --git a/packages/contracts-bedrock/scripts/libraries/DeployUtils.sol b/packages/contracts-bedrock/scripts/libraries/DeployUtils.sol index 1cec8d0477..b4c5bd7b14 100644 --- a/packages/contracts-bedrock/scripts/libraries/DeployUtils.sol +++ b/packages/contracts-bedrock/scripts/libraries/DeployUtils.sol @@ -17,6 +17,7 @@ import { IProxy } from "interfaces/universal/IProxy.sol"; import { IAddressManager } from "interfaces/legacy/IAddressManager.sol"; import { IL1ChugSplashProxy, IStaticL1ChugSplashProxy } from "interfaces/legacy/IL1ChugSplashProxy.sol"; import { IResolvedDelegateProxy } from "interfaces/legacy/IResolvedDelegateProxy.sol"; +import { IReinitializableBase } from "interfaces/universal/IReinitializableBase.sol"; library DeployUtils { Vm internal constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); @@ -360,7 +361,19 @@ library DeployUtils { bytes32 slotVal = vm.load(_contractAddress, bytes32(_slot)); uint8 val = uint8((uint256(slotVal) >> (_offset * 8)) & 0xFF); if (_isProxy) { - require(val == 1, "DeployUtils: storage value is not 1 at the given slot and offset"); + // Using a try/catch here to check if the contract has an initVersion() defined. + // EIP-150 safe because we require that we have at least 200k gas before the call which + // is more than enough to avoid running out of gas when 63/64 of the gas is provided to + // the initVersion() call (which simply reads an immutable variable). Since this is + // only ever triggered as part of a script, we can safely assume we'll have the gas. + require(gasleft() > 200_000, "DeployUtils: insufficient gas for initVersion() call"); + + // eip150-safe + try IReinitializableBase(_contractAddress).initVersion() returns (uint8 initVersion_) { + require(val == initVersion_, "DeployUtils: storage value is incorrect at the given slot and offset"); + } catch { + require(val == 1, "DeployUtils: storage value is not set at the given slot and offset"); + } } else { require(val == type(uint8).max, "DeployUtils: storage value is not 0xff at the given slot and offset"); } diff --git a/packages/contracts-bedrock/scripts/libraries/Types.sol b/packages/contracts-bedrock/scripts/libraries/Types.sol index 01d0995aec..f8c6eb9026 100644 --- a/packages/contracts-bedrock/scripts/libraries/Types.sol +++ b/packages/contracts-bedrock/scripts/libraries/Types.sol @@ -14,6 +14,7 @@ library Types { address AnchorStateRegistry; address OptimismMintableERC20Factory; address OptimismPortal; + address ETHLockbox; address SystemConfig; address L1ERC721Bridge; address ProtocolVersions; diff --git a/packages/contracts-bedrock/snapshots/abi/AnchorStateRegistry.json b/packages/contracts-bedrock/snapshots/abi/AnchorStateRegistry.json index 25919d1d4b..0d98a33f22 100644 --- a/packages/contracts-bedrock/snapshots/abi/AnchorStateRegistry.json +++ b/packages/contracts-bedrock/snapshots/abi/AnchorStateRegistry.json @@ -1,6 +1,12 @@ [ { - "inputs": [], + "inputs": [ + { + "internalType": "uint256", + "name": "_disputeGameFinalityDelaySeconds", + "type": "uint256" + } + ], "stateMutability": "nonpayable", "type": "constructor" }, @@ -41,6 +47,38 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "contract IDisputeGame", + "name": "_disputeGame", + "type": "address" + } + ], + "name": "blacklistDisputeGame", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IDisputeGame", + "name": "", + "type": "address" + } + ], + "name": "disputeGameBlacklist", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "disputeGameFactory", @@ -54,6 +92,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "disputeGameFinalityDelaySeconds", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "getAnchorRoot", @@ -84,11 +135,6 @@ "name": "_disputeGameFactory", "type": "address" }, - { - "internalType": "contract IOptimismPortal2", - "name": "_portal", - "type": "address" - }, { "components": [ { @@ -98,13 +144,18 @@ }, { "internalType": "uint256", - "name": "l2BlockNumber", + "name": "l2SequenceNumber", "type": "uint256" } ], - "internalType": "struct OutputRoot", + "internalType": "struct Proposal", "name": "_startingAnchorRoot", "type": "tuple" + }, + { + "internalType": "GameType", + "name": "_startingRespectedGameType", + "type": "uint32" } ], "name": "initialize", @@ -266,12 +317,12 @@ }, { "inputs": [], - "name": "portal", + "name": "paused", "outputs": [ { - "internalType": "contract IOptimismPortal2", + "internalType": "bool", "name": "", - "type": "address" + "type": "bool" } ], "stateMutability": "view", @@ -290,6 +341,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "retirementTimestamp", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -303,6 +367,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "GameType", + "name": "_gameType", + "type": "uint32" + } + ], + "name": "setRespectedGameType", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "superchainConfig", @@ -316,6 +393,13 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "updateRetirementTimestamp", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "version", @@ -339,7 +423,7 @@ "type": "address" } ], - "name": "AnchorNotUpdated", + "name": "AnchorUpdated", "type": "event" }, { @@ -347,12 +431,12 @@ "inputs": [ { "indexed": true, - "internalType": "contract IFaultDisputeGame", - "name": "game", + "internalType": "contract IDisputeGame", + "name": "disputeGame", "type": "address" } ], - "name": "AnchorUpdated", + "name": "DisputeGameBlacklisted", "type": "event" }, { @@ -368,6 +452,32 @@ "name": "Initialized", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "GameType", + "name": "gameType", + "type": "uint32" + } + ], + "name": "RespectedGameTypeSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "RetirementTimestampSet", + "type": "event" + }, { "inputs": [], "name": "AnchorStateRegistry_AnchorGameBlacklisted", diff --git a/packages/contracts-bedrock/snapshots/abi/CrossL2Inbox.json b/packages/contracts-bedrock/snapshots/abi/CrossL2Inbox.json index cb54eeaf11..2b29db32fb 100644 --- a/packages/contracts-bedrock/snapshots/abi/CrossL2Inbox.json +++ b/packages/contracts-bedrock/snapshots/abi/CrossL2Inbox.json @@ -1,89 +1,4 @@ [ - { - "inputs": [], - "name": "blockNumber", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "chainId", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "interopStart", - "outputs": [ - { - "internalType": "uint256", - "name": "interopStart_", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "logIndex", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "origin", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "setInteropStart", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "timestamp", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -190,27 +105,27 @@ }, { "inputs": [], - "name": "InteropStartAlreadySet", + "name": "BlockNumberTooHigh", "type": "error" }, { "inputs": [], - "name": "NoExecutingDeposits", + "name": "LogIndexTooHigh", "type": "error" }, { "inputs": [], - "name": "NotDepositor", + "name": "NoExecutingDeposits", "type": "error" }, { "inputs": [], - "name": "NotEntered", + "name": "NotInAccessList", "type": "error" }, { "inputs": [], - "name": "ReentrantCall", + "name": "TimestampTooHigh", "type": "error" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/DeputyGuardianModule.json b/packages/contracts-bedrock/snapshots/abi/DeputyGuardianModule.json index f100674935..ab662e4ee4 100644 --- a/packages/contracts-bedrock/snapshots/abi/DeputyGuardianModule.json +++ b/packages/contracts-bedrock/snapshots/abi/DeputyGuardianModule.json @@ -23,8 +23,8 @@ { "inputs": [ { - "internalType": "contract IOptimismPortal2", - "name": "_portal", + "internalType": "contract IAnchorStateRegistry", + "name": "_anchorStateRegistry", "type": "address" }, { @@ -75,25 +75,7 @@ "inputs": [ { "internalType": "contract IAnchorStateRegistry", - "name": "_registry", - "type": "address" - }, - { - "internalType": "contract IFaultDisputeGame", - "name": "_game", - "type": "address" - } - ], - "name": "setAnchorState", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "contract IOptimismPortal2", - "name": "_portal", + "name": "_anchorStateRegistry", "type": "address" }, { @@ -127,6 +109,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "contract IAnchorStateRegistry", + "name": "_anchorStateRegistry", + "type": "address" + } + ], + "name": "updateRetirementTimestamp", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "version", @@ -185,6 +180,19 @@ "name": "RespectedGameTypeSet", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "Timestamp", + "name": "updatedAt", + "type": "uint64" + } + ], + "name": "RetirementTimestampUpdated", + "type": "event" + }, { "anonymous": false, "inputs": [], @@ -199,12 +207,12 @@ "type": "string" } ], - "name": "ExecutionFailed", + "name": "DeputyGuardianModule_ExecutionFailed", "type": "error" }, { "inputs": [], - "name": "Unauthorized", + "name": "DeputyGuardianModule_Unauthorized", "type": "error" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/DisputeMonitorHelper.json b/packages/contracts-bedrock/snapshots/abi/DisputeMonitorHelper.json new file mode 100644 index 0000000000..3af4e4c2f4 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/DisputeMonitorHelper.json @@ -0,0 +1,89 @@ +[ + { + "inputs": [ + { + "internalType": "contract IDisputeGameFactory", + "name": "_factory", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_creationRangeStart", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_creationRangeEnd", + "type": "uint256" + } + ], + "name": "getUnresolvedGames", + "outputs": [ + { + "internalType": "contract IDisputeGame[]", + "name": "unresolvedGames_", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IDisputeGameFactory", + "name": "_factory", + "type": "address" + }, + { + "internalType": "contract IDisputeGame", + "name": "_game", + "type": "address" + } + ], + "name": "isGameRegistered", + "outputs": [ + { + "internalType": "bool", + "name": "isValid_", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IDisputeGameFactory", + "name": "_factory", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_targetTimestamp", + "type": "uint256" + }, + { + "internalType": "enum DisputeMonitorHelper.SearchDirection", + "name": "_direction", + "type": "uint8" + } + ], + "name": "search", + "outputs": [ + { + "internalType": "uint256", + "name": "index_", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "DisputeMonitorHelper_InvalidSearchRange", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/ETHLockbox.json b/packages/contracts-bedrock/snapshots/abi/ETHLockbox.json new file mode 100644 index 0000000000..50b2a344f0 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/ETHLockbox.json @@ -0,0 +1,326 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "contract IETHLockbox", + "name": "_lockbox", + "type": "address" + } + ], + "name": "authorizeLockbox", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IOptimismPortal2", + "name": "_portal", + "type": "address" + } + ], + "name": "authorizePortal", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IETHLockbox", + "name": "", + "type": "address" + } + ], + "name": "authorizedLockboxes", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IOptimismPortal2", + "name": "", + "type": "address" + } + ], + "name": "authorizedPortals", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ISuperchainConfig", + "name": "_superchainConfig", + "type": "address" + }, + { + "internalType": "contract IOptimismPortal2[]", + "name": "_portals", + "type": "address[]" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "lockETH", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IETHLockbox", + "name": "_lockbox", + "type": "address" + } + ], + "name": "migrateLiquidity", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxyAdminOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "receiveLiquidity", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "superchainConfig", + "outputs": [ + { + "internalType": "contract ISuperchainConfig", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + } + ], + "name": "unlockETH", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IOptimismPortal2", + "name": "portal", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "ETHLocked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IOptimismPortal2", + "name": "portal", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "ETHUnlocked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IETHLockbox", + "name": "lockbox", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "LiquidityMigrated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IETHLockbox", + "name": "lockbox", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "LiquidityReceived", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IETHLockbox", + "name": "lockbox", + "type": "address" + } + ], + "name": "LockboxAuthorized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IOptimismPortal2", + "name": "portal", + "type": "address" + } + ], + "name": "PortalAuthorized", + "type": "event" + }, + { + "inputs": [], + "name": "ETHLockbox_DifferentProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "ETHLockbox_DifferentSuperchainConfig", + "type": "error" + }, + { + "inputs": [], + "name": "ETHLockbox_InsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "ETHLockbox_NoWithdrawalTransactions", + "type": "error" + }, + { + "inputs": [], + "name": "ETHLockbox_Paused", + "type": "error" + }, + { + "inputs": [], + "name": "ETHLockbox_Unauthorized", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/FaultDisputeGame.json b/packages/contracts-bedrock/snapshots/abi/FaultDisputeGame.json index 3049ad6227..9ba78fac8b 100644 --- a/packages/contracts-bedrock/snapshots/abi/FaultDisputeGame.json +++ b/packages/contracts-bedrock/snapshots/abi/FaultDisputeGame.json @@ -566,6 +566,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "l2SequenceNumber", + "outputs": [ + { + "internalType": "uint256", + "name": "l2SequenceNumber_", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, { "inputs": [], "name": "maxClockDuration", @@ -805,7 +818,7 @@ }, { "internalType": "uint256", - "name": "l2BlockNumber", + "name": "l2SequenceNumber", "type": "uint256" } ], diff --git a/packages/contracts-bedrock/snapshots/abi/L1BlockInterop.json b/packages/contracts-bedrock/snapshots/abi/L1BlockInterop.json deleted file mode 100644 index b3b3d62cb4..0000000000 --- a/packages/contracts-bedrock/snapshots/abi/L1BlockInterop.json +++ /dev/null @@ -1,449 +0,0 @@ -[ - { - "inputs": [], - "name": "DEPOSITOR_ACCOUNT", - "outputs": [ - { - "internalType": "address", - "name": "addr_", - "type": "address" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [], - "name": "baseFeeScalar", - "outputs": [ - { - "internalType": "uint32", - "name": "", - "type": "uint32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "basefee", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "batcherHash", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "blobBaseFee", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "blobBaseFeeScalar", - "outputs": [ - { - "internalType": "uint32", - "name": "", - "type": "uint32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "dependencySetSize", - "outputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "depositsComplete", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "gasPayingToken", - "outputs": [ - { - "internalType": "address", - "name": "addr_", - "type": "address" - }, - { - "internalType": "uint8", - "name": "decimals_", - "type": "uint8" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [], - "name": "gasPayingTokenName", - "outputs": [ - { - "internalType": "string", - "name": "name_", - "type": "string" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [], - "name": "gasPayingTokenSymbol", - "outputs": [ - { - "internalType": "string", - "name": "symbol_", - "type": "string" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [], - "name": "hash", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "isCustomGasToken", - "outputs": [ - { - "internalType": "bool", - "name": "is_", - "type": "bool" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [], - "name": "isDeposit", - "outputs": [ - { - "internalType": "bool", - "name": "isDeposit_", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_chainId", - "type": "uint256" - } - ], - "name": "isInDependencySet", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "l1FeeOverhead", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "l1FeeScalar", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "number", - "outputs": [ - { - "internalType": "uint64", - "name": "", - "type": "uint64" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "operatorFeeConstant", - "outputs": [ - { - "internalType": "uint64", - "name": "", - "type": "uint64" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "operatorFeeScalar", - "outputs": [ - { - "internalType": "uint32", - "name": "", - "type": "uint32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "sequenceNumber", - "outputs": [ - { - "internalType": "uint64", - "name": "", - "type": "uint64" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "enum ConfigType", - "name": "_type", - "type": "uint8" - }, - { - "internalType": "bytes", - "name": "_value", - "type": "bytes" - } - ], - "name": "setConfig", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint64", - "name": "_number", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "_timestamp", - "type": "uint64" - }, - { - "internalType": "uint256", - "name": "_basefee", - "type": "uint256" - }, - { - "internalType": "bytes32", - "name": "_hash", - "type": "bytes32" - }, - { - "internalType": "uint64", - "name": "_sequenceNumber", - "type": "uint64" - }, - { - "internalType": "bytes32", - "name": "_batcherHash", - "type": "bytes32" - }, - { - "internalType": "uint256", - "name": "_l1FeeOverhead", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_l1FeeScalar", - "type": "uint256" - } - ], - "name": "setL1BlockValues", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "setL1BlockValuesEcotone", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "setL1BlockValuesInterop", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "setL1BlockValuesIsthmus", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "timestamp", - "outputs": [ - { - "internalType": "uint64", - "name": "", - "type": "uint64" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "version", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "uint256", - "name": "chainId", - "type": "uint256" - } - ], - "name": "DependencyAdded", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "uint256", - "name": "chainId", - "type": "uint256" - } - ], - "name": "DependencyRemoved", - "type": "event" - }, - { - "inputs": [], - "name": "AlreadyDependency", - "type": "error" - }, - { - "inputs": [], - "name": "CantRemovedDependency", - "type": "error" - }, - { - "inputs": [], - "name": "DependencySetSizeTooLarge", - "type": "error" - }, - { - "inputs": [], - "name": "NotCrossL2Inbox", - "type": "error" - }, - { - "inputs": [], - "name": "NotDependency", - "type": "error" - }, - { - "inputs": [], - "name": "NotDepositor", - "type": "error" - } -] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/L2ToL2CrossDomainMessenger.json b/packages/contracts-bedrock/snapshots/abi/L2ToL2CrossDomainMessenger.json index 3d381ec9ae..e54b24edf5 100644 --- a/packages/contracts-bedrock/snapshots/abi/L2ToL2CrossDomainMessenger.json +++ b/packages/contracts-bedrock/snapshots/abi/L2ToL2CrossDomainMessenger.json @@ -253,11 +253,6 @@ "name": "IdOriginNotL2ToL2CrossDomainMessenger", "type": "error" }, - { - "inputs": [], - "name": "InvalidChainId", - "type": "error" - }, { "inputs": [], "name": "MessageAlreadyRelayed", @@ -273,11 +268,6 @@ "name": "MessageDestinationSameChain", "type": "error" }, - { - "inputs": [], - "name": "MessageTargetCrossL2Inbox", - "type": "error" - }, { "inputs": [], "name": "MessageTargetL2ToL2CrossDomainMessenger", @@ -292,10 +282,5 @@ "inputs": [], "name": "ReentrantCall", "type": "error" - }, - { - "inputs": [], - "name": "TargetCallFailed", - "type": "error" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json index a12ea8e941..47e5d823e8 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json @@ -368,6 +368,11 @@ "name": "l1CrossDomainMessengerProxy", "type": "address" }, + { + "internalType": "contract IETHLockbox", + "name": "ethLockboxProxy", + "type": "address" + }, { "internalType": "contract IOptimismPortal2", "name": "optimismPortalProxy", @@ -438,6 +443,11 @@ "name": "optimismPortalImpl", "type": "address" }, + { + "internalType": "address", + "name": "ethLockboxImpl", + "type": "address" + }, { "internalType": "address", "name": "systemConfigImpl", diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerContractsContainer.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerContractsContainer.json index 45af4462a5..9e09b81cb6 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerContractsContainer.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerContractsContainer.json @@ -75,6 +75,11 @@ "name": "optimismPortalImpl", "type": "address" }, + { + "internalType": "address", + "name": "ethLockboxImpl", + "type": "address" + }, { "internalType": "address", "name": "systemConfigImpl", @@ -210,6 +215,11 @@ "name": "optimismPortalImpl", "type": "address" }, + { + "internalType": "address", + "name": "ethLockboxImpl", + "type": "address" + }, { "internalType": "address", "name": "systemConfigImpl", diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json index d543175c82..b24342213a 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json @@ -271,6 +271,11 @@ "name": "l1CrossDomainMessengerProxy", "type": "address" }, + { + "internalType": "contract IETHLockbox", + "name": "ethLockboxProxy", + "type": "address" + }, { "internalType": "contract IOptimismPortal2", "name": "optimismPortalProxy", @@ -341,6 +346,11 @@ "name": "optimismPortalImpl", "type": "address" }, + { + "internalType": "address", + "name": "ethLockboxImpl", + "type": "address" + }, { "internalType": "address", "name": "systemConfigImpl", diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployerInterop.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployerInterop.json deleted file mode 100644 index d543175c82..0000000000 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployerInterop.json +++ /dev/null @@ -1,502 +0,0 @@ -[ - { - "inputs": [ - { - "internalType": "contract OPContractsManagerContractsContainer", - "name": "_contractsContainer", - "type": "address" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_who", - "type": "address" - } - ], - "name": "assertValidContractAddress", - "outputs": [], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "blueprints", - "outputs": [ - { - "components": [ - { - "internalType": "address", - "name": "addressManager", - "type": "address" - }, - { - "internalType": "address", - "name": "proxy", - "type": "address" - }, - { - "internalType": "address", - "name": "proxyAdmin", - "type": "address" - }, - { - "internalType": "address", - "name": "l1ChugSplashProxy", - "type": "address" - }, - { - "internalType": "address", - "name": "resolvedDelegateProxy", - "type": "address" - }, - { - "internalType": "address", - "name": "permissionedDisputeGame1", - "type": "address" - }, - { - "internalType": "address", - "name": "permissionedDisputeGame2", - "type": "address" - }, - { - "internalType": "address", - "name": "permissionlessDisputeGame1", - "type": "address" - }, - { - "internalType": "address", - "name": "permissionlessDisputeGame2", - "type": "address" - } - ], - "internalType": "struct OPContractsManager.Blueprints", - "name": "", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_l2ChainId", - "type": "uint256" - } - ], - "name": "chainIdToBatchInboxAddress", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [], - "name": "contractsContainer", - "outputs": [ - { - "internalType": "contract OPContractsManagerContractsContainer", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "components": [ - { - "internalType": "address", - "name": "opChainProxyAdminOwner", - "type": "address" - }, - { - "internalType": "address", - "name": "systemConfigOwner", - "type": "address" - }, - { - "internalType": "address", - "name": "batcher", - "type": "address" - }, - { - "internalType": "address", - "name": "unsafeBlockSigner", - "type": "address" - }, - { - "internalType": "address", - "name": "proposer", - "type": "address" - }, - { - "internalType": "address", - "name": "challenger", - "type": "address" - } - ], - "internalType": "struct OPContractsManager.Roles", - "name": "roles", - "type": "tuple" - }, - { - "internalType": "uint32", - "name": "basefeeScalar", - "type": "uint32" - }, - { - "internalType": "uint32", - "name": "blobBasefeeScalar", - "type": "uint32" - }, - { - "internalType": "uint256", - "name": "l2ChainId", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "startingAnchorRoot", - "type": "bytes" - }, - { - "internalType": "string", - "name": "saltMixer", - "type": "string" - }, - { - "internalType": "uint64", - "name": "gasLimit", - "type": "uint64" - }, - { - "internalType": "GameType", - "name": "disputeGameType", - "type": "uint32" - }, - { - "internalType": "Claim", - "name": "disputeAbsolutePrestate", - "type": "bytes32" - }, - { - "internalType": "uint256", - "name": "disputeMaxGameDepth", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "disputeSplitDepth", - "type": "uint256" - }, - { - "internalType": "Duration", - "name": "disputeClockExtension", - "type": "uint64" - }, - { - "internalType": "Duration", - "name": "disputeMaxClockDuration", - "type": "uint64" - } - ], - "internalType": "struct OPContractsManager.DeployInput", - "name": "_input", - "type": "tuple" - }, - { - "internalType": "contract ISuperchainConfig", - "name": "_superchainConfig", - "type": "address" - }, - { - "internalType": "address", - "name": "_deployer", - "type": "address" - } - ], - "name": "deploy", - "outputs": [ - { - "components": [ - { - "internalType": "contract IProxyAdmin", - "name": "opChainProxyAdmin", - "type": "address" - }, - { - "internalType": "contract IAddressManager", - "name": "addressManager", - "type": "address" - }, - { - "internalType": "contract IL1ERC721Bridge", - "name": "l1ERC721BridgeProxy", - "type": "address" - }, - { - "internalType": "contract ISystemConfig", - "name": "systemConfigProxy", - "type": "address" - }, - { - "internalType": "contract IOptimismMintableERC20Factory", - "name": "optimismMintableERC20FactoryProxy", - "type": "address" - }, - { - "internalType": "contract IL1StandardBridge", - "name": "l1StandardBridgeProxy", - "type": "address" - }, - { - "internalType": "contract IL1CrossDomainMessenger", - "name": "l1CrossDomainMessengerProxy", - "type": "address" - }, - { - "internalType": "contract IOptimismPortal2", - "name": "optimismPortalProxy", - "type": "address" - }, - { - "internalType": "contract IDisputeGameFactory", - "name": "disputeGameFactoryProxy", - "type": "address" - }, - { - "internalType": "contract IAnchorStateRegistry", - "name": "anchorStateRegistryProxy", - "type": "address" - }, - { - "internalType": "contract IFaultDisputeGame", - "name": "faultDisputeGame", - "type": "address" - }, - { - "internalType": "contract IPermissionedDisputeGame", - "name": "permissionedDisputeGame", - "type": "address" - }, - { - "internalType": "contract IDelayedWETH", - "name": "delayedWETHPermissionedGameProxy", - "type": "address" - }, - { - "internalType": "contract IDelayedWETH", - "name": "delayedWETHPermissionlessGameProxy", - "type": "address" - } - ], - "internalType": "struct OPContractsManager.DeployOutput", - "name": "", - "type": "tuple" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "implementations", - "outputs": [ - { - "components": [ - { - "internalType": "address", - "name": "superchainConfigImpl", - "type": "address" - }, - { - "internalType": "address", - "name": "protocolVersionsImpl", - "type": "address" - }, - { - "internalType": "address", - "name": "l1ERC721BridgeImpl", - "type": "address" - }, - { - "internalType": "address", - "name": "optimismPortalImpl", - "type": "address" - }, - { - "internalType": "address", - "name": "systemConfigImpl", - "type": "address" - }, - { - "internalType": "address", - "name": "optimismMintableERC20FactoryImpl", - "type": "address" - }, - { - "internalType": "address", - "name": "l1CrossDomainMessengerImpl", - "type": "address" - }, - { - "internalType": "address", - "name": "l1StandardBridgeImpl", - "type": "address" - }, - { - "internalType": "address", - "name": "disputeGameFactoryImpl", - "type": "address" - }, - { - "internalType": "address", - "name": "anchorStateRegistryImpl", - "type": "address" - }, - { - "internalType": "address", - "name": "delayedWETHImpl", - "type": "address" - }, - { - "internalType": "address", - "name": "mipsImpl", - "type": "address" - } - ], - "internalType": "struct OPContractsManager.Implementations", - "name": "", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "uint256", - "name": "l2ChainId", - "type": "uint256" - }, - { - "indexed": true, - "internalType": "address", - "name": "deployer", - "type": "address" - }, - { - "indexed": false, - "internalType": "bytes", - "name": "deployOutput", - "type": "bytes" - } - ], - "name": "Deployed", - "type": "event" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "who", - "type": "address" - } - ], - "name": "AddressHasNoCode", - "type": "error" - }, - { - "inputs": [], - "name": "BytesArrayTooLong", - "type": "error" - }, - { - "inputs": [], - "name": "DeploymentFailed", - "type": "error" - }, - { - "inputs": [], - "name": "EmptyInitcode", - "type": "error" - }, - { - "inputs": [], - "name": "IdentityPrecompileCallFailed", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidChainId", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "string", - "name": "role", - "type": "string" - } - ], - "name": "InvalidRoleAddress", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidStartingAnchorRoot", - "type": "error" - }, - { - "inputs": [], - "name": "NotABlueprint", - "type": "error" - }, - { - "inputs": [], - "name": "ReservedBitsSet", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - } - ], - "name": "UnexpectedPreambleData", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "uint8", - "name": "version", - "type": "uint8" - } - ], - "name": "UnsupportedERCVersion", - "type": "error" - } -] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerGameTypeAdder.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerGameTypeAdder.json index a2088238ca..bc3cd92692 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerGameTypeAdder.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerGameTypeAdder.json @@ -244,6 +244,11 @@ "name": "optimismPortalImpl", "type": "address" }, + { + "internalType": "address", + "name": "ethLockboxImpl", + "type": "address" + }, { "internalType": "address", "name": "systemConfigImpl", diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUpgrader.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUpgrader.json index 8efb6ad77e..42887ce92c 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUpgrader.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUpgrader.json @@ -141,6 +141,11 @@ "name": "optimismPortalImpl", "type": "address" }, + { + "internalType": "address", + "name": "ethLockboxImpl", + "type": "address" + }, { "internalType": "address", "name": "systemConfigImpl", diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json b/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json index 71b8677f7d..9719bfe650 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json @@ -5,11 +5,6 @@ "internalType": "uint256", "name": "_proofMaturityDelaySeconds", "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_disputeGameFinalityDelaySeconds", - "type": "uint256" } ], "stateMutability": "nonpayable", @@ -20,16 +15,16 @@ "type": "receive" }, { - "inputs": [ + "inputs": [], + "name": "anchorStateRegistry", + "outputs": [ { - "internalType": "contract IDisputeGame", - "name": "_disputeGame", + "internalType": "contract IAnchorStateRegistry", + "name": "", "type": "address" } ], - "name": "blacklistDisputeGame", - "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "view", "type": "function" }, { @@ -83,25 +78,6 @@ "stateMutability": "payable", "type": "function" }, - { - "inputs": [ - { - "internalType": "contract IDisputeGame", - "name": "", - "type": "address" - } - ], - "name": "disputeGameBlacklist", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "disputeGameFactory", @@ -135,6 +111,19 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [], + "name": "ethLockbox", + "outputs": [ + { + "internalType": "contract IETHLockbox", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -263,12 +252,20 @@ "type": "function" }, { - "inputs": [ + "inputs": [], + "name": "initVersion", + "outputs": [ { - "internalType": "contract IDisputeGameFactory", - "name": "_disputeGameFactory", - "type": "address" - }, + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ { "internalType": "contract ISystemConfig", "name": "_systemConfig", @@ -280,9 +277,14 @@ "type": "address" }, { - "internalType": "GameType", - "name": "_initialRespectedGameType", - "type": "uint32" + "internalType": "contract IAnchorStateRegistry", + "name": "_anchorStateRegistry", + "type": "address" + }, + { + "internalType": "contract IETHLockbox", + "name": "_ethLockbox", + "type": "address" } ], "name": "initialize", @@ -303,6 +305,31 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "migrateLiquidity", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IETHLockbox", + "name": "_newLockbox", + "type": "address" + }, + { + "internalType": "contract IAnchorStateRegistry", + "name": "_newAnchorStateRegistry", + "type": "address" + } + ], + "name": "migrateToSuperRoots", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -496,6 +523,127 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gasLimit", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "internalType": "struct Types.WithdrawalTransaction", + "name": "_tx", + "type": "tuple" + }, + { + "internalType": "contract IDisputeGame", + "name": "_disputeGameProxy", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_outputRootIndex", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "bytes1", + "name": "version", + "type": "bytes1" + }, + { + "internalType": "uint64", + "name": "timestamp", + "type": "uint64" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "root", + "type": "bytes32" + } + ], + "internalType": "struct Types.OutputRootWithChainId[]", + "name": "outputRoots", + "type": "tuple[]" + } + ], + "internalType": "struct Types.SuperRootProof", + "name": "_superRootProof", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "version", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "stateRoot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "messagePasserStorageRoot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "latestBlockhash", + "type": "bytes32" + } + ], + "internalType": "struct Types.OutputRootProof", + "name": "_outputRootProof", + "type": "tuple" + }, + { + "internalType": "bytes[]", + "name": "_withdrawalProof", + "type": "bytes[]" + } + ], + "name": "proveWithdrawalTransaction", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -525,6 +673,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "proxyAdminOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "respectedGameType", @@ -552,16 +713,16 @@ "type": "function" }, { - "inputs": [ + "inputs": [], + "name": "superRootsActive", + "outputs": [ { - "internalType": "GameType", - "name": "_gameType", - "type": "uint32" + "internalType": "bool", + "name": "", + "type": "bool" } ], - "name": "setRespectedGameType", - "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "view", "type": "function" }, { @@ -590,6 +751,24 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "contract IAnchorStateRegistry", + "name": "_anchorStateRegistry", + "type": "address" + }, + { + "internalType": "contract IETHLockbox", + "name": "_ethLockbox", + "type": "address" + } + ], + "name": "upgrade", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "version", @@ -608,12 +787,18 @@ "inputs": [ { "indexed": true, - "internalType": "contract IDisputeGame", - "name": "disputeGame", + "internalType": "address", + "name": "lockbox", "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "ethBalance", + "type": "uint256" } ], - "name": "DisputeGameBlacklisted", + "name": "ETHMigrated", "type": "event" }, { @@ -633,19 +818,31 @@ "anonymous": false, "inputs": [ { - "indexed": true, - "internalType": "GameType", - "name": "newGameType", - "type": "uint32" + "indexed": false, + "internalType": "contract IETHLockbox", + "name": "oldLockbox", + "type": "address" }, { - "indexed": true, - "internalType": "Timestamp", - "name": "updatedAt", - "type": "uint64" + "indexed": false, + "internalType": "contract IETHLockbox", + "name": "newLockbox", + "type": "address" + }, + { + "indexed": false, + "internalType": "contract IAnchorStateRegistry", + "name": "oldAnchorStateRegistry", + "type": "address" + }, + { + "indexed": false, + "internalType": "contract IAnchorStateRegistry", + "name": "newAnchorStateRegistry", + "type": "address" } ], - "name": "RespectedGameTypeSet", + "name": "PortalMigrated", "type": "event" }, { @@ -744,117 +941,157 @@ }, { "inputs": [], - "name": "AlreadyFinalized", + "name": "ContentLengthMismatch", "type": "error" }, { "inputs": [], - "name": "BadTarget", + "name": "EmptyItem", "type": "error" }, { "inputs": [], - "name": "Blacklisted", + "name": "Encoding_EmptySuperRoot", "type": "error" }, { "inputs": [], - "name": "CallPaused", + "name": "Encoding_InvalidSuperRootVersion", "type": "error" }, { "inputs": [], - "name": "ContentLengthMismatch", + "name": "InvalidDataRemainder", "type": "error" }, { "inputs": [], - "name": "EmptyItem", + "name": "InvalidHeader", "type": "error" }, { "inputs": [], - "name": "GasEstimation", + "name": "OptimismPortal_AlreadyFinalized", "type": "error" }, { "inputs": [], - "name": "InvalidDataRemainder", + "name": "OptimismPortal_BadTarget", "type": "error" }, { "inputs": [], - "name": "InvalidDisputeGame", + "name": "OptimismPortal_CallPaused", "type": "error" }, { "inputs": [], - "name": "InvalidGameType", + "name": "OptimismPortal_CalldataTooLarge", "type": "error" }, { "inputs": [], - "name": "InvalidHeader", + "name": "OptimismPortal_GasEstimation", "type": "error" }, { "inputs": [], - "name": "InvalidMerkleProof", + "name": "OptimismPortal_GasLimitTooLow", "type": "error" }, { "inputs": [], - "name": "InvalidProof", + "name": "OptimismPortal_ImproperDisputeGame", "type": "error" }, { "inputs": [], - "name": "LargeCalldata", + "name": "OptimismPortal_InvalidDisputeGame", "type": "error" }, { "inputs": [], - "name": "LegacyGame", + "name": "OptimismPortal_InvalidMerkleProof", "type": "error" }, { "inputs": [], - "name": "NonReentrant", + "name": "OptimismPortal_InvalidOutputRootChainId", "type": "error" }, { "inputs": [], - "name": "OutOfGas", + "name": "OptimismPortal_InvalidOutputRootIndex", "type": "error" }, { "inputs": [], - "name": "ProposalNotValidated", + "name": "OptimismPortal_InvalidOutputRootProof", "type": "error" }, { "inputs": [], - "name": "SmallGasLimit", + "name": "OptimismPortal_InvalidProofTimestamp", "type": "error" }, { "inputs": [], - "name": "Unauthorized", + "name": "OptimismPortal_InvalidRootClaim", "type": "error" }, { "inputs": [], - "name": "UnexpectedList", + "name": "OptimismPortal_InvalidSuperRootProof", "type": "error" }, { "inputs": [], - "name": "UnexpectedString", + "name": "OptimismPortal_MigratingToSameRegistry", "type": "error" }, { "inputs": [], - "name": "Unproven", + "name": "OptimismPortal_NoReentrancy", + "type": "error" + }, + { + "inputs": [], + "name": "OptimismPortal_ProofNotOldEnough", + "type": "error" + }, + { + "inputs": [], + "name": "OptimismPortal_Unauthorized", + "type": "error" + }, + { + "inputs": [], + "name": "OptimismPortal_Unproven", + "type": "error" + }, + { + "inputs": [], + "name": "OptimismPortal_WrongProofMethod", + "type": "error" + }, + { + "inputs": [], + "name": "OutOfGas", + "type": "error" + }, + { + "inputs": [], + "name": "ReinitializableBase_ZeroInitVersion", + "type": "error" + }, + { + "inputs": [], + "name": "UnexpectedList", + "type": "error" + }, + { + "inputs": [], + "name": "UnexpectedString", "type": "error" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json b/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json deleted file mode 100644 index 2dd27e68ea..0000000000 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json +++ /dev/null @@ -1,878 +0,0 @@ -[ - { - "inputs": [ - { - "internalType": "uint256", - "name": "_proofMaturityDelaySeconds", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_disputeGameFinalityDelaySeconds", - "type": "uint256" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "stateMutability": "payable", - "type": "receive" - }, - { - "inputs": [ - { - "internalType": "contract IDisputeGame", - "name": "_disputeGame", - "type": "address" - } - ], - "name": "blacklistDisputeGame", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "_withdrawalHash", - "type": "bytes32" - }, - { - "internalType": "address", - "name": "_proofSubmitter", - "type": "address" - } - ], - "name": "checkWithdrawal", - "outputs": [], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_to", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_value", - "type": "uint256" - }, - { - "internalType": "uint64", - "name": "_gasLimit", - "type": "uint64" - }, - { - "internalType": "bool", - "name": "_isCreation", - "type": "bool" - }, - { - "internalType": "bytes", - "name": "_data", - "type": "bytes" - } - ], - "name": "depositTransaction", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "contract IDisputeGame", - "name": "", - "type": "address" - } - ], - "name": "disputeGameBlacklist", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "disputeGameFactory", - "outputs": [ - { - "internalType": "contract IDisputeGameFactory", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "disputeGameFinalityDelaySeconds", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "donateETH", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "uint256", - "name": "nonce", - "type": "uint256" - }, - { - "internalType": "address", - "name": "sender", - "type": "address" - }, - { - "internalType": "address", - "name": "target", - "type": "address" - }, - { - "internalType": "uint256", - "name": "value", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "gasLimit", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - } - ], - "internalType": "struct Types.WithdrawalTransaction", - "name": "_tx", - "type": "tuple" - } - ], - "name": "finalizeWithdrawalTransaction", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "uint256", - "name": "nonce", - "type": "uint256" - }, - { - "internalType": "address", - "name": "sender", - "type": "address" - }, - { - "internalType": "address", - "name": "target", - "type": "address" - }, - { - "internalType": "uint256", - "name": "value", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "gasLimit", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - } - ], - "internalType": "struct Types.WithdrawalTransaction", - "name": "_tx", - "type": "tuple" - }, - { - "internalType": "address", - "name": "_proofSubmitter", - "type": "address" - } - ], - "name": "finalizeWithdrawalTransactionExternalProof", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "name": "finalizedWithdrawals", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "guardian", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "contract IDisputeGameFactory", - "name": "_disputeGameFactory", - "type": "address" - }, - { - "internalType": "contract ISystemConfig", - "name": "_systemConfig", - "type": "address" - }, - { - "internalType": "contract ISuperchainConfig", - "name": "_superchainConfig", - "type": "address" - }, - { - "internalType": "GameType", - "name": "_initialRespectedGameType", - "type": "uint32" - } - ], - "name": "initialize", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "l2Sender", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint64", - "name": "_byteCount", - "type": "uint64" - } - ], - "name": "minimumGasLimit", - "outputs": [ - { - "internalType": "uint64", - "name": "", - "type": "uint64" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "_withdrawalHash", - "type": "bytes32" - } - ], - "name": "numProofSubmitters", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "params", - "outputs": [ - { - "internalType": "uint128", - "name": "prevBaseFee", - "type": "uint128" - }, - { - "internalType": "uint64", - "name": "prevBoughtGas", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "prevBlockNum", - "type": "uint64" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "paused", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "proofMaturityDelaySeconds", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - }, - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "name": "proofSubmitters", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "uint256", - "name": "nonce", - "type": "uint256" - }, - { - "internalType": "address", - "name": "sender", - "type": "address" - }, - { - "internalType": "address", - "name": "target", - "type": "address" - }, - { - "internalType": "uint256", - "name": "value", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "gasLimit", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - } - ], - "internalType": "struct Types.WithdrawalTransaction", - "name": "_tx", - "type": "tuple" - }, - { - "internalType": "uint256", - "name": "_disputeGameIndex", - "type": "uint256" - }, - { - "components": [ - { - "internalType": "bytes32", - "name": "version", - "type": "bytes32" - }, - { - "internalType": "bytes32", - "name": "stateRoot", - "type": "bytes32" - }, - { - "internalType": "bytes32", - "name": "messagePasserStorageRoot", - "type": "bytes32" - }, - { - "internalType": "bytes32", - "name": "latestBlockhash", - "type": "bytes32" - } - ], - "internalType": "struct Types.OutputRootProof", - "name": "_outputRootProof", - "type": "tuple" - }, - { - "internalType": "bytes[]", - "name": "_withdrawalProof", - "type": "bytes[]" - } - ], - "name": "proveWithdrawalTransaction", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - }, - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "name": "provenWithdrawals", - "outputs": [ - { - "internalType": "contract IDisputeGame", - "name": "disputeGameProxy", - "type": "address" - }, - { - "internalType": "uint64", - "name": "timestamp", - "type": "uint64" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "respectedGameType", - "outputs": [ - { - "internalType": "GameType", - "name": "", - "type": "uint32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "respectedGameTypeUpdatedAt", - "outputs": [ - { - "internalType": "uint64", - "name": "", - "type": "uint64" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "enum ConfigType", - "name": "_type", - "type": "uint8" - }, - { - "internalType": "bytes", - "name": "_value", - "type": "bytes" - } - ], - "name": "setConfig", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "GameType", - "name": "_gameType", - "type": "uint32" - } - ], - "name": "setRespectedGameType", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "superchainConfig", - "outputs": [ - { - "internalType": "contract ISuperchainConfig", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "systemConfig", - "outputs": [ - { - "internalType": "contract ISystemConfig", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "version", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "contract IDisputeGame", - "name": "disputeGame", - "type": "address" - } - ], - "name": "DisputeGameBlacklisted", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint8", - "name": "version", - "type": "uint8" - } - ], - "name": "Initialized", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "GameType", - "name": "newGameType", - "type": "uint32" - }, - { - "indexed": true, - "internalType": "Timestamp", - "name": "updatedAt", - "type": "uint64" - } - ], - "name": "RespectedGameTypeSet", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "indexed": true, - "internalType": "uint256", - "name": "version", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "bytes", - "name": "opaqueData", - "type": "bytes" - } - ], - "name": "TransactionDeposited", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "bytes32", - "name": "withdrawalHash", - "type": "bytes32" - }, - { - "indexed": false, - "internalType": "bool", - "name": "success", - "type": "bool" - } - ], - "name": "WithdrawalFinalized", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "bytes32", - "name": "withdrawalHash", - "type": "bytes32" - }, - { - "indexed": true, - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "to", - "type": "address" - } - ], - "name": "WithdrawalProven", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "bytes32", - "name": "withdrawalHash", - "type": "bytes32" - }, - { - "indexed": true, - "internalType": "address", - "name": "proofSubmitter", - "type": "address" - } - ], - "name": "WithdrawalProvenExtension1", - "type": "event" - }, - { - "inputs": [], - "name": "AlreadyFinalized", - "type": "error" - }, - { - "inputs": [], - "name": "BadTarget", - "type": "error" - }, - { - "inputs": [], - "name": "Blacklisted", - "type": "error" - }, - { - "inputs": [], - "name": "CallPaused", - "type": "error" - }, - { - "inputs": [], - "name": "ContentLengthMismatch", - "type": "error" - }, - { - "inputs": [], - "name": "EmptyItem", - "type": "error" - }, - { - "inputs": [], - "name": "GasEstimation", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidDataRemainder", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidDisputeGame", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidGameType", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidHeader", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidMerkleProof", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidProof", - "type": "error" - }, - { - "inputs": [], - "name": "LargeCalldata", - "type": "error" - }, - { - "inputs": [], - "name": "LegacyGame", - "type": "error" - }, - { - "inputs": [], - "name": "NonReentrant", - "type": "error" - }, - { - "inputs": [], - "name": "OutOfGas", - "type": "error" - }, - { - "inputs": [], - "name": "ProposalNotValidated", - "type": "error" - }, - { - "inputs": [], - "name": "SmallGasLimit", - "type": "error" - }, - { - "inputs": [], - "name": "Unauthorized", - "type": "error" - }, - { - "inputs": [], - "name": "UnexpectedList", - "type": "error" - }, - { - "inputs": [], - "name": "UnexpectedString", - "type": "error" - }, - { - "inputs": [], - "name": "Unproven", - "type": "error" - } -] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/PermissionedDisputeGame.json b/packages/contracts-bedrock/snapshots/abi/PermissionedDisputeGame.json index 8aa53d301a..2bf04958bb 100644 --- a/packages/contracts-bedrock/snapshots/abi/PermissionedDisputeGame.json +++ b/packages/contracts-bedrock/snapshots/abi/PermissionedDisputeGame.json @@ -589,6 +589,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "l2SequenceNumber", + "outputs": [ + { + "internalType": "uint256", + "name": "l2SequenceNumber_", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, { "inputs": [], "name": "maxClockDuration", @@ -841,7 +854,7 @@ }, { "internalType": "uint256", - "name": "l2BlockNumber", + "name": "l2SequenceNumber", "type": "uint256" } ], diff --git a/packages/contracts-bedrock/snapshots/abi/StandardValidatorV300.json b/packages/contracts-bedrock/snapshots/abi/StandardValidatorV300.json new file mode 100644 index 0000000000..7a2fda6ec8 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/StandardValidatorV300.json @@ -0,0 +1,469 @@ +[ + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "l1ERC721BridgeImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "optimismPortalImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "systemConfigImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "optimismMintableERC20FactoryImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "l1CrossDomainMessengerImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "l1StandardBridgeImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "disputeGameFactoryImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "anchorStateRegistryImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "delayedWETHImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "mipsImpl", + "type": "address" + } + ], + "internalType": "struct StandardValidatorBase.ImplementationsBase", + "name": "_implementations", + "type": "tuple" + }, + { + "internalType": "contract ISuperchainConfig", + "name": "_superchainConfig", + "type": "address" + }, + { + "internalType": "address", + "name": "_l1PAOMultisig", + "type": "address" + }, + { + "internalType": "address", + "name": "_mips", + "type": "address" + }, + { + "internalType": "address", + "name": "_challenger", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "anchorStateRegistryImpl", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "anchorStateRegistryVersion", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "challenger", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "delayedWETHImpl", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "delayedWETHVersion", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "disputeGameFactoryImpl", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "disputeGameFactoryVersion", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "l1CrossDomainMessengerImpl", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "l1CrossDomainMessengerVersion", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "l1ERC721BridgeImpl", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "l1ERC721BridgeVersion", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "l1PAOMultisig", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "l1StandardBridgeImpl", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "l1StandardBridgeVersion", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "mips", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "mipsImpl", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "mipsVersion", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "optimismMintableERC20FactoryImpl", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "optimismMintableERC20FactoryVersion", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "optimismPortalImpl", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "optimismPortalVersion", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "permissionedDisputeGameVersion", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "preimageOracleVersion", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "superchainConfig", + "outputs": [ + { + "internalType": "contract ISuperchainConfig", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "systemConfigImpl", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "systemConfigVersion", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "contract IProxyAdmin", + "name": "proxyAdmin", + "type": "address" + }, + { + "internalType": "contract ISystemConfig", + "name": "sysCfg", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "absolutePrestate", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "l2ChainID", + "type": "uint256" + } + ], + "internalType": "struct StandardValidatorV300.InputV300", + "name": "_input", + "type": "tuple" + }, + { + "internalType": "bool", + "name": "_allowFailure", + "type": "bool" + } + ], + "name": "validate", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/SuperFaultDisputeGame.json b/packages/contracts-bedrock/snapshots/abi/SuperFaultDisputeGame.json index 8c72cb9c74..0dda8da865 100644 --- a/packages/contracts-bedrock/snapshots/abi/SuperFaultDisputeGame.json +++ b/packages/contracts-bedrock/snapshots/abi/SuperFaultDisputeGame.json @@ -476,11 +476,11 @@ }, { "inputs": [], - "name": "l2BlockNumber", + "name": "l2SequenceNumber", "outputs": [ { "internalType": "uint256", - "name": "l2BlockNumber_", + "name": "l2SequenceNumber_", "type": "uint256" } ], @@ -704,11 +704,16 @@ }, { "inputs": [], - "name": "startingBlockNumber", + "name": "startingProposal", "outputs": [ + { + "internalType": "Hash", + "name": "root", + "type": "bytes32" + }, { "internalType": "uint256", - "name": "startingBlockNumber_", + "name": "l2SequenceNumber", "type": "uint256" } ], @@ -717,17 +722,12 @@ }, { "inputs": [], - "name": "startingOutputRoot", + "name": "startingRootHash", "outputs": [ { "internalType": "Hash", - "name": "root", + "name": "startingRootHash_", "type": "bytes32" - }, - { - "internalType": "uint256", - "name": "l2BlockNumber", - "type": "uint256" } ], "stateMutability": "view", @@ -735,12 +735,12 @@ }, { "inputs": [], - "name": "startingRootHash", + "name": "startingSequenceNumber", "outputs": [ { - "internalType": "Hash", - "name": "startingRootHash_", - "type": "bytes32" + "internalType": "uint256", + "name": "startingSequenceNumber_", + "type": "uint256" } ], "stateMutability": "view", diff --git a/packages/contracts-bedrock/snapshots/abi/SuperPermissionedDisputeGame.json b/packages/contracts-bedrock/snapshots/abi/SuperPermissionedDisputeGame.json index 02de35aca2..227c2edc06 100644 --- a/packages/contracts-bedrock/snapshots/abi/SuperPermissionedDisputeGame.json +++ b/packages/contracts-bedrock/snapshots/abi/SuperPermissionedDisputeGame.json @@ -499,11 +499,11 @@ }, { "inputs": [], - "name": "l2BlockNumber", + "name": "l2SequenceNumber", "outputs": [ { "internalType": "uint256", - "name": "l2BlockNumber_", + "name": "l2SequenceNumber_", "type": "uint256" } ], @@ -740,11 +740,16 @@ }, { "inputs": [], - "name": "startingBlockNumber", + "name": "startingProposal", "outputs": [ + { + "internalType": "Hash", + "name": "root", + "type": "bytes32" + }, { "internalType": "uint256", - "name": "startingBlockNumber_", + "name": "l2SequenceNumber", "type": "uint256" } ], @@ -753,17 +758,12 @@ }, { "inputs": [], - "name": "startingOutputRoot", + "name": "startingRootHash", "outputs": [ { "internalType": "Hash", - "name": "root", + "name": "startingRootHash_", "type": "bytes32" - }, - { - "internalType": "uint256", - "name": "l2BlockNumber", - "type": "uint256" } ], "stateMutability": "view", @@ -771,12 +771,12 @@ }, { "inputs": [], - "name": "startingRootHash", + "name": "startingSequenceNumber", "outputs": [ { - "internalType": "Hash", - "name": "startingRootHash_", - "type": "bytes32" + "internalType": "uint256", + "name": "startingSequenceNumber_", + "type": "uint256" } ], "stateMutability": "view", diff --git a/packages/contracts-bedrock/snapshots/abi/SuperchainWETH.json b/packages/contracts-bedrock/snapshots/abi/SuperchainWETH.json index 24b27063f3..cdc8ab052f 100644 --- a/packages/contracts-bedrock/snapshots/abi/SuperchainWETH.json +++ b/packages/contracts-bedrock/snapshots/abi/SuperchainWETH.json @@ -519,6 +519,11 @@ "name": "InvalidCrossDomainSender", "type": "error" }, + { + "inputs": [], + "name": "Permit2AllowanceIsFixedAtInfinity", + "type": "error" + }, { "inputs": [], "name": "Unauthorized", diff --git a/packages/contracts-bedrock/snapshots/abi/SystemConfig.json b/packages/contracts-bedrock/snapshots/abi/SystemConfig.json index 5009ab31ca..2580d74105 100644 --- a/packages/contracts-bedrock/snapshots/abi/SystemConfig.json +++ b/packages/contracts-bedrock/snapshots/abi/SystemConfig.json @@ -17,19 +17,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [], - "name": "DISPUTE_GAME_FACTORY_SLOT", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "L1_CROSS_DOMAIN_MESSENGER_SLOT", @@ -259,11 +246,6 @@ "name": "l1StandardBridge", "type": "address" }, - { - "internalType": "address", - "name": "disputeGameFactory", - "type": "address" - }, { "internalType": "address", "name": "optimismPortal", @@ -283,6 +265,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "initVersion", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -374,11 +369,6 @@ "name": "l1StandardBridge", "type": "address" }, - { - "internalType": "address", - "name": "disputeGameFactory", - "type": "address" - }, { "internalType": "address", "name": "optimismPortal", @@ -393,6 +383,11 @@ "internalType": "struct SystemConfig.Addresses", "name": "_addresses", "type": "tuple" + }, + { + "internalType": "uint256", + "name": "_l2ChainId", + "type": "uint256" } ], "name": "initialize", @@ -439,6 +434,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "l2ChainId", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "maximumGasLimit", @@ -758,6 +766,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_l2ChainId", + "type": "uint256" + } + ], + "name": "upgrade", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "version", @@ -827,5 +848,10 @@ ], "name": "OwnershipTransferred", "type": "event" + }, + { + "inputs": [], + "name": "ReinitializableBase_ZeroInitVersion", + "type": "error" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/SystemConfigInterop.json b/packages/contracts-bedrock/snapshots/abi/SystemConfigInterop.json deleted file mode 100644 index 8bcac1f13c..0000000000 --- a/packages/contracts-bedrock/snapshots/abi/SystemConfigInterop.json +++ /dev/null @@ -1,987 +0,0 @@ -[ - { - "inputs": [], - "name": "BATCH_INBOX_SLOT", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "DISPUTE_GAME_FACTORY_SLOT", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "L1_CROSS_DOMAIN_MESSENGER_SLOT", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "L1_ERC_721_BRIDGE_SLOT", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "L1_STANDARD_BRIDGE_SLOT", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "OPTIMISM_MINTABLE_ERC20_FACTORY_SLOT", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "OPTIMISM_PORTAL_SLOT", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "START_BLOCK_SLOT", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "UNSAFE_BLOCK_SIGNER_SLOT", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "VERSION", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_chainId", - "type": "uint256" - } - ], - "name": "addDependency", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "basefeeScalar", - "outputs": [ - { - "internalType": "uint32", - "name": "", - "type": "uint32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "batchInbox", - "outputs": [ - { - "internalType": "address", - "name": "addr_", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "batcherHash", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "blobbasefeeScalar", - "outputs": [ - { - "internalType": "uint32", - "name": "", - "type": "uint32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "dependencyManager", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "disputeGameFactory", - "outputs": [ - { - "internalType": "address", - "name": "addr_", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "eip1559Denominator", - "outputs": [ - { - "internalType": "uint32", - "name": "", - "type": "uint32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "eip1559Elasticity", - "outputs": [ - { - "internalType": "uint32", - "name": "", - "type": "uint32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "gasLimit", - "outputs": [ - { - "internalType": "uint64", - "name": "", - "type": "uint64" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getAddresses", - "outputs": [ - { - "components": [ - { - "internalType": "address", - "name": "l1CrossDomainMessenger", - "type": "address" - }, - { - "internalType": "address", - "name": "l1ERC721Bridge", - "type": "address" - }, - { - "internalType": "address", - "name": "l1StandardBridge", - "type": "address" - }, - { - "internalType": "address", - "name": "disputeGameFactory", - "type": "address" - }, - { - "internalType": "address", - "name": "optimismPortal", - "type": "address" - }, - { - "internalType": "address", - "name": "optimismMintableERC20Factory", - "type": "address" - } - ], - "internalType": "struct SystemConfig.Addresses", - "name": "", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_owner", - "type": "address" - }, - { - "internalType": "uint32", - "name": "_basefeeScalar", - "type": "uint32" - }, - { - "internalType": "uint32", - "name": "_blobbasefeeScalar", - "type": "uint32" - }, - { - "internalType": "bytes32", - "name": "_batcherHash", - "type": "bytes32" - }, - { - "internalType": "uint64", - "name": "_gasLimit", - "type": "uint64" - }, - { - "internalType": "address", - "name": "_unsafeBlockSigner", - "type": "address" - }, - { - "components": [ - { - "internalType": "uint32", - "name": "maxResourceLimit", - "type": "uint32" - }, - { - "internalType": "uint8", - "name": "elasticityMultiplier", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "baseFeeMaxChangeDenominator", - "type": "uint8" - }, - { - "internalType": "uint32", - "name": "minimumBaseFee", - "type": "uint32" - }, - { - "internalType": "uint32", - "name": "systemTxMaxGas", - "type": "uint32" - }, - { - "internalType": "uint128", - "name": "maximumBaseFee", - "type": "uint128" - } - ], - "internalType": "struct IResourceMetering.ResourceConfig", - "name": "_config", - "type": "tuple" - }, - { - "internalType": "address", - "name": "_batchInbox", - "type": "address" - }, - { - "components": [ - { - "internalType": "address", - "name": "l1CrossDomainMessenger", - "type": "address" - }, - { - "internalType": "address", - "name": "l1ERC721Bridge", - "type": "address" - }, - { - "internalType": "address", - "name": "l1StandardBridge", - "type": "address" - }, - { - "internalType": "address", - "name": "disputeGameFactory", - "type": "address" - }, - { - "internalType": "address", - "name": "optimismPortal", - "type": "address" - }, - { - "internalType": "address", - "name": "optimismMintableERC20Factory", - "type": "address" - } - ], - "internalType": "struct SystemConfig.Addresses", - "name": "_addresses", - "type": "tuple" - } - ], - "name": "initialize", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_owner", - "type": "address" - }, - { - "internalType": "uint32", - "name": "_basefeeScalar", - "type": "uint32" - }, - { - "internalType": "uint32", - "name": "_blobbasefeeScalar", - "type": "uint32" - }, - { - "internalType": "bytes32", - "name": "_batcherHash", - "type": "bytes32" - }, - { - "internalType": "uint64", - "name": "_gasLimit", - "type": "uint64" - }, - { - "internalType": "address", - "name": "_unsafeBlockSigner", - "type": "address" - }, - { - "components": [ - { - "internalType": "uint32", - "name": "maxResourceLimit", - "type": "uint32" - }, - { - "internalType": "uint8", - "name": "elasticityMultiplier", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "baseFeeMaxChangeDenominator", - "type": "uint8" - }, - { - "internalType": "uint32", - "name": "minimumBaseFee", - "type": "uint32" - }, - { - "internalType": "uint32", - "name": "systemTxMaxGas", - "type": "uint32" - }, - { - "internalType": "uint128", - "name": "maximumBaseFee", - "type": "uint128" - } - ], - "internalType": "struct IResourceMetering.ResourceConfig", - "name": "_config", - "type": "tuple" - }, - { - "internalType": "address", - "name": "_batchInbox", - "type": "address" - }, - { - "components": [ - { - "internalType": "address", - "name": "l1CrossDomainMessenger", - "type": "address" - }, - { - "internalType": "address", - "name": "l1ERC721Bridge", - "type": "address" - }, - { - "internalType": "address", - "name": "l1StandardBridge", - "type": "address" - }, - { - "internalType": "address", - "name": "disputeGameFactory", - "type": "address" - }, - { - "internalType": "address", - "name": "optimismPortal", - "type": "address" - }, - { - "internalType": "address", - "name": "optimismMintableERC20Factory", - "type": "address" - } - ], - "internalType": "struct SystemConfig.Addresses", - "name": "_addresses", - "type": "tuple" - }, - { - "internalType": "address", - "name": "_dependencyManager", - "type": "address" - } - ], - "name": "initialize", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "l1CrossDomainMessenger", - "outputs": [ - { - "internalType": "address", - "name": "addr_", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "l1ERC721Bridge", - "outputs": [ - { - "internalType": "address", - "name": "addr_", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "l1StandardBridge", - "outputs": [ - { - "internalType": "address", - "name": "addr_", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "maximumGasLimit", - "outputs": [ - { - "internalType": "uint64", - "name": "", - "type": "uint64" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [], - "name": "minimumGasLimit", - "outputs": [ - { - "internalType": "uint64", - "name": "", - "type": "uint64" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "operatorFeeConstant", - "outputs": [ - { - "internalType": "uint64", - "name": "", - "type": "uint64" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "operatorFeeScalar", - "outputs": [ - { - "internalType": "uint32", - "name": "", - "type": "uint32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "optimismMintableERC20Factory", - "outputs": [ - { - "internalType": "address", - "name": "addr_", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "optimismPortal", - "outputs": [ - { - "internalType": "address", - "name": "addr_", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "overhead", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "owner", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_chainId", - "type": "uint256" - } - ], - "name": "removeDependency", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "renounceOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "resourceConfig", - "outputs": [ - { - "components": [ - { - "internalType": "uint32", - "name": "maxResourceLimit", - "type": "uint32" - }, - { - "internalType": "uint8", - "name": "elasticityMultiplier", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "baseFeeMaxChangeDenominator", - "type": "uint8" - }, - { - "internalType": "uint32", - "name": "minimumBaseFee", - "type": "uint32" - }, - { - "internalType": "uint32", - "name": "systemTxMaxGas", - "type": "uint32" - }, - { - "internalType": "uint128", - "name": "maximumBaseFee", - "type": "uint128" - } - ], - "internalType": "struct IResourceMetering.ResourceConfig", - "name": "", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "scalar", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "_batcherHash", - "type": "bytes32" - } - ], - "name": "setBatcherHash", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint32", - "name": "_denominator", - "type": "uint32" - }, - { - "internalType": "uint32", - "name": "_elasticity", - "type": "uint32" - } - ], - "name": "setEIP1559Params", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_overhead", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_scalar", - "type": "uint256" - } - ], - "name": "setGasConfig", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint32", - "name": "_basefeeScalar", - "type": "uint32" - }, - { - "internalType": "uint32", - "name": "_blobbasefeeScalar", - "type": "uint32" - } - ], - "name": "setGasConfigEcotone", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint64", - "name": "_gasLimit", - "type": "uint64" - } - ], - "name": "setGasLimit", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint32", - "name": "_operatorFeeScalar", - "type": "uint32" - }, - { - "internalType": "uint64", - "name": "_operatorFeeConstant", - "type": "uint64" - } - ], - "name": "setOperatorFeeScalars", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_unsafeBlockSigner", - "type": "address" - } - ], - "name": "setUnsafeBlockSigner", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "startBlock", - "outputs": [ - { - "internalType": "uint256", - "name": "startBlock_", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "transferOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "unsafeBlockSigner", - "outputs": [ - { - "internalType": "address", - "name": "addr_", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "version", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "uint256", - "name": "version", - "type": "uint256" - }, - { - "indexed": true, - "internalType": "enum SystemConfig.UpdateType", - "name": "updateType", - "type": "uint8" - }, - { - "indexed": false, - "internalType": "bytes", - "name": "data", - "type": "bytes" - } - ], - "name": "ConfigUpdate", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint8", - "name": "version", - "type": "uint8" - } - ], - "name": "Initialized", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "previousOwner", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "OwnershipTransferred", - "type": "event" - } -] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi_loader.go b/packages/contracts-bedrock/snapshots/abi_loader.go index 34d6375436..4845485121 100644 --- a/packages/contracts-bedrock/snapshots/abi_loader.go +++ b/packages/contracts-bedrock/snapshots/abi_loader.go @@ -10,6 +10,9 @@ import ( //go:embed abi/DisputeGameFactory.json var disputeGameFactory []byte +//go:embed abi/SuperFaultDisputeGame.json +var superFaultDisputeGame []byte + //go:embed abi/FaultDisputeGame.json var faultDisputeGame []byte @@ -31,6 +34,9 @@ var crossL2Inbox []byte func LoadDisputeGameFactoryABI() *abi.ABI { return loadABI(disputeGameFactory) } +func LoadSuperFaultDisputeGameABI() *abi.ABI { + return loadABI(superFaultDisputeGame) +} func LoadFaultDisputeGameABI() *abi.ABI { return loadABI(faultDisputeGame) } diff --git a/packages/contracts-bedrock/snapshots/abi_loader_test.go b/packages/contracts-bedrock/snapshots/abi_loader_test.go index 0bb58922c6..dc9d0884cb 100644 --- a/packages/contracts-bedrock/snapshots/abi_loader_test.go +++ b/packages/contracts-bedrock/snapshots/abi_loader_test.go @@ -14,6 +14,7 @@ func TestLoadABIs(t *testing.T) { }{ {"DisputeGameFactory", LoadDisputeGameFactoryABI}, {"FaultDisputeGame", LoadFaultDisputeGameABI}, + {"SuperFaultDisputeGame", LoadSuperFaultDisputeGameABI}, {"PreimageOracle", LoadPreimageOracleABI}, {"MIPS", LoadMIPSABI}, {"DelayedWETH", LoadDelayedWETHABI}, diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 8a31cd06dc..066796b6ee 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -1,237 +1,229 @@ { - "src/L1/DataAvailabilityChallenge.sol": { + "src/L1/DataAvailabilityChallenge.sol:DataAvailabilityChallenge": { "initCodeHash": "0xacbae98cc7c0f7ecbf36dc44bbf7cb0a011e6e6b781e28b9dbf947e31482b30d", "sourceCodeHash": "0xe772f7db8033e4a738850cb28ac4849d3a454c93732135a8a10d4f7cb498088e" }, - "src/L1/L1CrossDomainMessenger.sol": { + "src/L1/ETHLockbox.sol:ETHLockbox": { + "initCodeHash": "0x3b5eac93a55c8fc20a4a517f5683aa1b2aa4424652b3bc2e9d4d6f4b6c9219e9", + "sourceCodeHash": "0x350aefb43cd1c40b7555473e09ad901970b813b7fd16ff4f009194810075685c" + }, + "src/L1/L1CrossDomainMessenger.sol:L1CrossDomainMessenger": { "initCodeHash": "0x5a6272f6bd4346da460b7ff2ebc426d158d7ffc65dc0f2c0651a9e6d545bfd03", "sourceCodeHash": "0xf11fa72481dbe43dad4e7a48645fcf92d0feeefa0b98b282042bdda568508372" }, - "src/L1/L1ERC721Bridge.sol": { + "src/L1/L1ERC721Bridge.sol:L1ERC721Bridge": { "initCodeHash": "0x819fcccb74bef53241d7d651f73949115e5706b61c7cb8cdb9b6d9d34ef39e89", "sourceCodeHash": "0x5036f59ae3414d1106f148765bba9d1759844c9c2d3e18ab5c81bb49cf59eab8" }, - "src/L1/L1StandardBridge.sol": { + "src/L1/L1StandardBridge.sol:L1StandardBridge": { "initCodeHash": "0xbd4a03a3611a0de9669c4c8556f90c5aaef666b899d9ded1c07abc60263da8d6", "sourceCodeHash": "0x44797707aea8c63dec049a02d69ea056662a06e5cf320028ab8b388634bf1c67" }, - "src/L1/OPContractsManager.sol": { - "initCodeHash": "0x216fe3ceef5d3e839d18a00383793bd326f20944fc3b55ef099a26a22141de18", - "sourceCodeHash": "0xd1de3414a3db731447cb6172df23a00cb1664783de56a93fb29f87d6bc8b8bb0" - }, - "src/L1/OptimismPortal2.sol": { - "initCodeHash": "0xd1651b8a6f4d25611a0105d5cc7c1da3921417bd44da870ec63bf5ccd1bc7c63", - "sourceCodeHash": "0xce7373d8c7df47caa8b090f3afb3d2539677f12cb3eff7fc0ab1fd85638f05c1" + "src/L1/OPContractsManager.sol:OPContractsManager": { + "initCodeHash": "0x4c4cd28e0942b7a9c25152f5af1e67d0c301d2c017aa2c01b91d0646b255984c", + "sourceCodeHash": "0xcce6d86f7988147942ad3bac6a8bbaf1aa576cd4dd7f81716a944620770012a3" }, - "src/L1/OptimismPortalInterop.sol": { - "initCodeHash": "0xd59854648bf205dfbea96b483b2937441c32e9ef66b002468c2c14c0d6661728", - "sourceCodeHash": "0xd00b267dcf125e77c10b28c088be4378ec779927e3bcfeb6aa9a7f3d51370490" + "src/L1/OptimismPortal2.sol:OptimismPortal2": { + "initCodeHash": "0xbc7db7b1016025228d99a40db1760290b333bb90563dff5514fd253fd91019ba", + "sourceCodeHash": "0x367e0a1c609e3c82db41a8c5e056108cbbae58d61bfeb08707449655485ba8ab" }, - "src/L1/ProtocolVersions.sol": { + "src/L1/ProtocolVersions.sol:ProtocolVersions": { "initCodeHash": "0x5a76c8530cb24cf23d3baacc6eefaac226382af13f1e2a35535d2ec2b0573b29", "sourceCodeHash": "0xb3e32b18c95d4940980333e1e99b4dcf42d8a8bfce78139db4dc3fb06e9349d0" }, - "src/L1/SuperchainConfig.sol": { + "src/L1/SuperchainConfig.sol:SuperchainConfig": { "initCodeHash": "0x23c54871316523111f3385b5acd7fcbda3c91096d0d44a21d8bae61736c380d7", "sourceCodeHash": "0xfd56e63e76b1f203cceeb9bbb14396ae803cbbbf7e80ca0ee11fb586321812af" }, - "src/L1/SystemConfig.sol": { - "initCodeHash": "0xd5b8b8eb47763556d9953019d1f81b1d790f15433aa9696b159a3fc45ecee148", - "sourceCodeHash": "0x6bfbc78b0fef2f65beff11a81f924728a7bd439a56986997621099551805aff9" + "src/L1/SystemConfig.sol:SystemConfig": { + "initCodeHash": "0x1edc0fd719aaa39eef32e3b29bacec77b512a3494904643bab2a21a855507bbc", + "sourceCodeHash": "0xfa541a4f124ef5568f80ddefbcb257a9204b94ebef921c854598b8b510ea14d1" }, - "src/L1/SystemConfigInterop.sol": { - "initCodeHash": "0xbef4696b2dcb6d43c3b3c438338bfb2224a1ea5002ed0612ec36a7821d7e3da2", - "sourceCodeHash": "0x1653aaa4d2b44d34ca1f9f2b4971eeb7594c8c2d27771b1f68b8d38cb79f2368" - }, - "src/L2/BaseFeeVault.sol": { + "src/L2/BaseFeeVault.sol:BaseFeeVault": { "initCodeHash": "0xc403d4c555d8e69a2699e01d192ae7327136701fa02da10a6d75a584b3c364c9", "sourceCodeHash": "0xfa56426153227e798150f6becc30a33fd20a3c6e0d73c797a3922dd631acbb57" }, - "src/L2/CrossL2Inbox.sol": { - "initCodeHash": "0x2bc4a3765004f9a9e6e5278753bce3c3d53cc95da62efcc0cb10c50d8c806cd4", - "sourceCodeHash": "0x661d7659f09b7f909e8bd5e6c41e8c98f2091036ed2123b7e18a1a74120bd849" + "src/L2/CrossL2Inbox.sol:CrossL2Inbox": { + "initCodeHash": "0xa7d89c648e0dd2f0a95780feb7ce7674dbd35a1c2de354a0222da7cef8b596c6", + "sourceCodeHash": "0xe001c9d4cf55e90c227459573b7eaac6015dea6cd9622ebf117e9ecba54cb4cc" }, - "src/L2/ETHLiquidity.sol": { - "initCodeHash": "0x776ece4a1bb24d97287806769470327641da240b083898a90943e2844957cc46", - "sourceCodeHash": "0xe5c08ce62327113e4bbaf29f47e5f1ddfad6fbd63c07132eedfba5af5325f331" + "src/L2/ETHLiquidity.sol:ETHLiquidity": { + "initCodeHash": "0xfcb50f32bbbd7e426f83c5c320ea655b896c026324a5e51983c1b15041ef88ca", + "sourceCodeHash": "0x91e93fa27b6927d61039f5d8d22be9ec780583bcdba90a4c78c6e7945d5a9a81" }, - "src/L2/GasPriceOracle.sol": { + "src/L2/GasPriceOracle.sol:GasPriceOracle": { "initCodeHash": "0x38ef70b2783dd45ad807afcf57972c7df4abaaeb5d16d17cdb451b9e931a9cbb", "sourceCodeHash": "0x4351fe2ac1106c8c220b8cfe7839bc107c24d8084deb21259ac954f5a362725d" }, - "src/L2/L1Block.sol": { + "src/L2/L1Block.sol:L1Block": { "initCodeHash": "0xa1f984b8ea199574261c19122b5a9c8c7dbd3633980b1e7aaf6b7af24af60478", "sourceCodeHash": "0xd04d64355dcf55247ac937748518e7f9620ae3f9eabe80fae9a82c0115ed77bc" }, - "src/L2/L1BlockInterop.sol": { - "initCodeHash": "0x55d09f00ad284fd7ca4b55c45fb901ed021b83118012be217aec53876ab34c12", - "sourceCodeHash": "0x7dd627c198a583fbe2c7d257f06001e1a2e563c6c7d79ea6ba9ca0d47cd1599b" - }, - "src/L2/L1FeeVault.sol": { + "src/L2/L1FeeVault.sol:L1FeeVault": { "initCodeHash": "0x6745b7be3895a5e8d373df0066d931bae29c47672ac46c2f5829bd0052cc6d9e", "sourceCodeHash": "0xd0471c328c1d17c5863261322bf8d5aff2e7e9e3a1135631a993aa75667621df" }, - "src/L2/L2CrossDomainMessenger.sol": { + "src/L2/L2CrossDomainMessenger.sol:L2CrossDomainMessenger": { "initCodeHash": "0xe160be403df12709c371c33195d1b9c3b5e9499e902e86bdabc8eed749c3fd61", "sourceCodeHash": "0x12ea125038b87e259a0d203e119faa6e9726ab2bdbc30430f820ccd48fe87e14" }, - "src/L2/L2ERC721Bridge.sol": { + "src/L2/L2ERC721Bridge.sol:L2ERC721Bridge": { "initCodeHash": "0x863f0f5b410983f3e51cd97c60a3a42915141b7452864d0e176571d640002b81", "sourceCodeHash": "0xc05bfcfadfd09a56cfea68e7c1853faa36d114d9a54cd307348be143e442c35a" }, - "src/L2/L2StandardBridge.sol": { + "src/L2/L2StandardBridge.sol:L2StandardBridge": { "initCodeHash": "0xba5b288a396b34488ba7be68473305529c7da7c43e5f1cfc48d6a4aecd014103", "sourceCodeHash": "0x9dd26676cd1276c807ffd4747236783c5170d0919c70693e70b7e4c4c2675429" }, - "src/L2/L2StandardBridgeInterop.sol": { + "src/L2/L2StandardBridgeInterop.sol:L2StandardBridgeInterop": { "initCodeHash": "0xa7a2e7efe8116ebb21f47ee06c1e62d3b2f5a046478094611a2ab4b714154030", "sourceCodeHash": "0xde724da82ecf3c96b330c2876a7285b6e2b933ac599241eaa3174c443ebbe33a" }, - "src/L2/L2ToL1MessagePasser.sol": { + "src/L2/L2ToL1MessagePasser.sol:L2ToL1MessagePasser": { "initCodeHash": "0xf9d82084dcef31a3737a76d8ee4e5842ea190d0f77ed4678adb3bbb95217050f", "sourceCodeHash": "0xaef8ea36c5b78cd12e0e62811d51db627ccf0dfd2cc5479fb707a10ef0d42048" }, - "src/L2/L2ToL2CrossDomainMessenger.sol": { - "initCodeHash": "0xc56db8cb569efa0467fd53ab3fa218af3051e54f5517d7fafb7b5831b4350618", - "sourceCodeHash": "0x72062343a044e9c56f4143dcfc71706286eb205902006c2afcf6a4cd90c3e9f8" + "src/L2/L2ToL2CrossDomainMessenger.sol:L2ToL2CrossDomainMessenger": { + "initCodeHash": "0x7ad26c308c506ca65e080c8a33eceac48201221cd291030844021cf1cfde4871", + "sourceCodeHash": "0xe59eec8a64d6c42d9cc2a67aa649718cad590961f92787d62435bf8cde93f7f3" }, - "src/L2/OperatorFeeVault.sol": { + "src/L2/OperatorFeeVault.sol:OperatorFeeVault": { "initCodeHash": "0x3d8c0d7736e8767f2f797da1c20c5fe30bd7f48a4cf75f376290481ad7c0f91f", "sourceCodeHash": "0x2022fdb4e32769eb9446dab4aed4b8abb5261fd866f381cccfa7869df1a2adff" }, - "src/L2/OptimismMintableERC721.sol": { + "src/L2/OptimismMintableERC721.sol:OptimismMintableERC721": { "initCodeHash": "0xcfa6ad9997a422aef5a19a490a0a535bc870ee34b1f5258c2949eb3680f71e8a", "sourceCodeHash": "0xb67b91f28c8666fee26c40375f835c61629e0f14054bfaf78bc3c61175bbf136" }, - "src/L2/OptimismMintableERC721Factory.sol": { + "src/L2/OptimismMintableERC721Factory.sol:OptimismMintableERC721Factory": { "initCodeHash": "0x9ccba9a5db77356c361fe4aea0e93498a56bda9fdac8d5e654d6f7abc4553028", "sourceCodeHash": "0x2e4b8535b1f7749a0479b2c1de86b3ff79ee4ff6122c6f87c52d66cd301f3f97" }, - "src/L2/OptimismSuperchainERC20.sol": { + "src/L2/OptimismSuperchainERC20.sol:OptimismSuperchainERC20": { "initCodeHash": "0xdac32a1057a6bc8a8d2ffdce1db8f34950cd0ffd1454d2133865736d21869192", "sourceCodeHash": "0x4a7924f2195074145ac8e6221d77b24cd22d97423db2053937897e9d788990e2" }, - "src/L2/OptimismSuperchainERC20Beacon.sol": { + "src/L2/OptimismSuperchainERC20Beacon.sol:OptimismSuperchainERC20Beacon": { "initCodeHash": "0x8a4d7cac6dd8ce583c996837893b93560297be1269f97f785a502748b25ba310", "sourceCodeHash": "0xb57024e16b528bade5fee7c236e03ffbb3f22e6376e6852e2109298af850b43c" }, - "src/L2/OptimismSuperchainERC20Factory.sol": { + "src/L2/OptimismSuperchainERC20Factory.sol:OptimismSuperchainERC20Factory": { "initCodeHash": "0x44659ea207ed173db4f1b519944c09c671d49f118e9d9ab85a010b8ebaf899e7", "sourceCodeHash": "0xa1c0346cfe6932dde05dc6c1d9505cac38434d8a8f9e1e437253b1f4115f2506" }, - "src/L2/SequencerFeeVault.sol": { + "src/L2/SequencerFeeVault.sol:SequencerFeeVault": { "initCodeHash": "0x02ca6cb6eebd2d6b91cf1eab483ee00b3233a7e8ad31f0e9cafc1f645ab3c24a", "sourceCodeHash": "0x85c740c0888368ee95607635818ee698c27582e8917f40bc590d240447376da9" }, - "src/L2/SuperchainERC20.sol": { + "src/L2/SuperchainERC20.sol:SuperchainERC20": { "initCodeHash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", - "sourceCodeHash": "0x981dca5b09da9038a9dff071b40a880e1b52b20268c6780ef54be3bc98a4f629" + "sourceCodeHash": "0x5fd3259418010699c51e1ecfa4dc6aa707b7debb606b0bdc2176b5b34916e217" }, - "src/L2/SuperchainTokenBridge.sol": { + "src/L2/SuperchainTokenBridge.sol:SuperchainTokenBridge": { "initCodeHash": "0x6b568ed564aede82a3a4cbcdb51282cad0e588a3fe6d91cf76616d3113df3901", "sourceCodeHash": "0xcd2b49cb7cf6d18616ee8bec9183fe5b5b460941875bc0b4158c4d5390ec3b0c" }, - "src/L2/SuperchainWETH.sol": { - "initCodeHash": "0x545686820e440d72529c815b7406844272d5ec33b741b2be6ebbe3a3db1ca8ad", - "sourceCodeHash": "0x6145e61cc0a0c95db882a76ecffea15c358c2b574d5157e53b85a69908701613" + "src/L2/SuperchainWETH.sol:SuperchainWETH": { + "initCodeHash": "0xc865e00858668be0eba7d0062a88aa0554b8f8fdae1db67cd8e1ab04cd014cde", + "sourceCodeHash": "0x05f70cd9358ae4a17ace23f568c7952f8a81099278da6a46a601b8a101280a7e" }, - "src/L2/WETH.sol": { + "src/L2/WETH.sol:WETH": { "initCodeHash": "0x38b396fc35d72e8013bad2fe8d7dea5285499406d4c4b62e27c54252e1e0f00a", "sourceCodeHash": "0xf4f83ca89d2519045a2916c670bda66f39b431a13921e639a5342bfc6157b178" }, - "src/cannon/MIPS.sol": { + "src/cannon/MIPS.sol:MIPS": { "initCodeHash": "0x6072be7e25ad30d16ef86e2f95343b5378bc29d79541b3711f3463e712baebf6", "sourceCodeHash": "0x51d93a684bd9def207a47f6c1dbe481aba5def3f77533d4a6e490784204d113b" }, - "src/cannon/MIPS2.sol": { + "src/cannon/MIPS2.sol:MIPS2": { "initCodeHash": "0x1f4e7cfdbcf7a8ca0ebac69bc7fe74143286a0e51a06ee9cbd699d68efd26dba", "sourceCodeHash": "0x20256a2196daca39b56bfae1c90b8871349916dc47461b5ca078c2013c067571" }, - "src/cannon/MIPS64.sol": { + "src/cannon/MIPS64.sol:MIPS64": { "initCodeHash": "0x0a274f73b9fae62524a5773e480b398846e1140aed373be211a07cb586e4758e", "sourceCodeHash": "0xb710bd6d4844f9ee45f301bb815786619b5e2d6b2f85ae17f39bee4f414f1957" }, - "src/cannon/PreimageOracle.sol": { + "src/cannon/PreimageOracle.sol:PreimageOracle": { "initCodeHash": "0x6af5b0e83b455aab8d0946c160a4dc049a4e03be69f8a2a9e87b574f27b25a66", "sourceCodeHash": "0x03c160168986ffc8d26a90c37366e7ad6da03f49d83449e1f8b3de0f4b590f6f" }, - "src/dispute/AnchorStateRegistry.sol": { - "initCodeHash": "0x08cc5a5e41eadb6c411fa6387ddc0cf12be360855599dd622cce84c0ba081e77", - "sourceCodeHash": "0xe0aaa79f7184724ff0fba2e92e85f652f936fecd099288edb0a0f6b0e0240f34" + "src/dispute/AnchorStateRegistry.sol:AnchorStateRegistry": { + "initCodeHash": "0x38a6746412f59e12d279c23ac6c620b2c306c117532d2299d511808851488bbd", + "sourceCodeHash": "0x2e64207064472cfdae29fb4f7499ae49fd76ef6cb4c51babd15679432d6a14f6" }, - "src/dispute/DelayedWETH.sol": { + "src/dispute/DelayedWETH.sol:DelayedWETH": { "initCodeHash": "0xdd0b5e523f3b53563fe0b6e6165fb73605b14910ffa32a7cbed855cdebab47c6", "sourceCodeHash": "0x592fe720a86be12fd83511f641d5b3dee80834a7ed83a6ab81d3e7e4712c15d8" }, - "src/dispute/DisputeGameFactory.sol": { + "src/dispute/DisputeGameFactory.sol:DisputeGameFactory": { "initCodeHash": "0xfd3ead515b80db01722e56203bd3ef85c41fc2cb201cc6afa9fb42d8a9731bca", "sourceCodeHash": "0x08efbce44394f555a4e656816897029e2e80e455e4200a1b887fc18993294660" }, - "src/dispute/FaultDisputeGame.sol": { - "initCodeHash": "0xd86a649deaa0d5ca6c4b60afb941c52fd2b5d10857657defda3629faaff6d77e", - "sourceCodeHash": "0xf5d3760949af227ea161f552a1d0c5d605a26bfd24dc428f674c011cb7f0a416" + "src/dispute/FaultDisputeGame.sol:FaultDisputeGame": { + "initCodeHash": "0xcf599cfd92543953cb3ccf3127aa095cb2752e872cdd3953d17821b7716e72c0", + "sourceCodeHash": "0x41058e11ea000b6f0aaf925d0f05c2ed7b6ab38c83594dd44f6fb5d6b9f671a6" }, - "src/dispute/PermissionedDisputeGame.sol": { - "initCodeHash": "0x63ab07ca14b77da770de1814b112e1a21ea984c5132b8ff3beaab2cf88018c19", - "sourceCodeHash": "0xba32e6f35777426839a60e5556c09844e805eaabc14b9d3732cc64f2a99ce7fc" + "src/dispute/PermissionedDisputeGame.sol:PermissionedDisputeGame": { + "initCodeHash": "0x7ad9fd33a79a43877ea204a5f75aa6a870da2ab9cbfce96cbc14dc4ddff7164b", + "sourceCodeHash": "0x88571b2360f58fd648e70289d828781d4f29007b3f6005b5a9cacf5353a3ecbf" }, - "src/dispute/SuperFaultDisputeGame.sol": { - "initCodeHash": "0x397d9eda98459859f30f170f0d3c6f4320d474ea36510ae7ece020800b80ba22", - "sourceCodeHash": "0xb08789200c909d24c8ebfbde9b9c55c50b9ebae2d21e761464c1b5abbbe59317" + "src/dispute/SuperFaultDisputeGame.sol:SuperFaultDisputeGame": { + "initCodeHash": "0x25a4e28d12b7a6885a8f50ef182775005339efa0010897affe0f44325fbbfcac", + "sourceCodeHash": "0xe0094f825c433e6d34b2ef1fa6f2aefc5603ac7f83ce1348444ba9af706f9a3f" }, - "src/dispute/SuperPermissionedDisputeGame.sol": { - "initCodeHash": "0x9613232c1c11abee8782e2e9a5bb963923c73643d8c228e558f06082e8886f67", - "sourceCodeHash": "0xaefa8af211cee1b34063252f62020cd468175a2ab9cfc46b4e4cb03190e2d659" + "src/dispute/SuperPermissionedDisputeGame.sol:SuperPermissionedDisputeGame": { + "initCodeHash": "0xaf43915296e333361835fbd8a74342e4e84a467f860cb5fd25bd773b672e2bbc", + "sourceCodeHash": "0xdbae9a8ea6aca8b4f8a5f9559e3a1d254153dd89ef3984a84f8ff6aeaa8020b3" }, - "src/legacy/DeployerWhitelist.sol": { + "src/legacy/DeployerWhitelist.sol:DeployerWhitelist": { "initCodeHash": "0x53099379ed48b87f027d55712dbdd1da7d7099925426eb0531da9c0012e02c29", "sourceCodeHash": "0xf22c94ed20c32a8ed2705a22d12c6969c3c3bad409c4efe2f95b0db74f210e10" }, - "src/legacy/L1BlockNumber.sol": { + "src/legacy/L1BlockNumber.sol:L1BlockNumber": { "initCodeHash": "0x60dded11d35e42fe15ef5dd94d28aae6b8ff3e67c6fbbc667a6729fcb3ca7a9a", "sourceCodeHash": "0x53ef11021a52e9c87024a870566ec5dba1d1a12752396e654904384efdd8203e" }, - "src/legacy/LegacyMessagePasser.sol": { + "src/legacy/LegacyMessagePasser.sol:LegacyMessagePasser": { "initCodeHash": "0x3ca911b0578be7f8c91e7d01442a5609f04e5866768f99c8e31627c9ba79c9f0", "sourceCodeHash": "0x62c9a6182d82692fb9c173ddb0d7978bcff2d1d4dc8cd2f10625e1e65bda6888" }, - "src/safe/DeputyGuardianModule.sol": { - "initCodeHash": "0x5eaf823d81995ce1f703f26e31049c54c1d4902dd9873a0b4645d470f2f459a2", - "sourceCodeHash": "0x17236a91c4171ae9525eae0e59fa65bb2dc320d62677cfc7d7eb942f182619fb" + "src/safe/DeputyGuardianModule.sol:DeputyGuardianModule": { + "initCodeHash": "0xf0ccc9997a120b2642bf4b15b5c12b041219c5e748c0c5e24af36f9cde62f175", + "sourceCodeHash": "0xa7dc83c6278118b9a7b4633633eeaa7e5b466bf24fdd184925ab620555caf0aa" }, - "src/safe/DeputyPauseModule.sol": { + "src/safe/DeputyPauseModule.sol:DeputyPauseModule": { "initCodeHash": "0xa3b7bf0c93b41f39ebc18a81322b90127a633d684ae9f86c2f2a1c48fe7f1372", "sourceCodeHash": "0xfbe7f5733aa57de7557605e40e03e9708fbdec872bc8739c551905c4b1e1b61b" }, - "src/safe/LivenessGuard.sol": { + "src/safe/LivenessGuard.sol:LivenessGuard": { "initCodeHash": "0xc8e29e8b12f423c8cd229a38bc731240dd815d96f1b0ab96c71494dde63f6a81", "sourceCodeHash": "0x72b8d8d855e7af8beee29330f6cb9b9069acb32e23ce940002ec9a41aa012a16" }, - "src/safe/LivenessModule.sol": { + "src/safe/LivenessModule.sol:LivenessModule": { "initCodeHash": "0xde3b3273aa37604048b5fa228b90f3b05997db613dfcda45061545a669b2476a", "sourceCodeHash": "0x918965e52bbd358ac827ebe35998f5d8fa5ca77d8eb9ab8986b44181b9aaa48a" }, - "src/universal/OptimismMintableERC20.sol": { + "src/universal/OptimismMintableERC20.sol:OptimismMintableERC20": { "initCodeHash": "0xc3289416829b252c830ad7d389a430986a7404df4fe0be37cb19e1c40907f047", "sourceCodeHash": "0xf5e29dd5c750ea935c7281ec916ba5277f5610a0a9e984e53ae5d5245b3cf2f4" }, - "src/universal/OptimismMintableERC20Factory.sol": { + "src/universal/OptimismMintableERC20Factory.sol:OptimismMintableERC20Factory": { "initCodeHash": "0xdb4d93a65cf9d3e3af77d3d62249f06580e80a0431542350f953f0a4041566b4", "sourceCodeHash": "0xd1bad4408c26eb9c7b0ddcb088f0d4e3be73a43d899263ec8610f4d41a178ec7" }, - "src/universal/StorageSetter.sol": { + "src/universal/StorageSetter.sol:StorageSetter": { "initCodeHash": "0x8831c079f7b7a52679e8a15e0ea14e30ea7bb4f93feed0fcd369942fe8c1f1ec", "sourceCodeHash": "0x42151e2547ec5270353977fd66e78fa1fde18f362d7021cf7ddce16d5201b3ec" }, - "src/vendor/asterisc/RISCV.sol": { + "src/vendor/asterisc/RISCV.sol:RISCV": { "initCodeHash": "0x7329cca924e189eeaa2d883234f6cb5fd787c8bf3339d8298e721778c2947ce5", "sourceCodeHash": "0x02025b303a8f37b4e541f8c7936a8651402a60ea0147a53176e06b51b15a1f84" }, - "src/vendor/eas/EAS.sol": { + "src/vendor/eas/EAS.sol:EAS": { "initCodeHash": "0xbd79d6fff128b3da3e09ead84b805b7540740190488f2791a6b4e5b7aabf9cff", "sourceCodeHash": "0x3512c3a1b5871341346f6646a04c0895dd563e9824f2ab7ab965b6a81a41ad2e" }, - "src/vendor/eas/SchemaRegistry.sol": { + "src/vendor/eas/SchemaRegistry.sol:SchemaRegistry": { "initCodeHash": "0x2bfce526f82622288333d53ca3f43a0a94306ba1bab99241daa845f8f4b18bd4", "sourceCodeHash": "0xf49d7b0187912a6bb67926a3222ae51121e9239495213c975b3b4b217ee57a1b" } diff --git a/packages/contracts-bedrock/snapshots/storageLayout/AnchorStateRegistry.json b/packages/contracts-bedrock/snapshots/storageLayout/AnchorStateRegistry.json index fac376a90d..1a1e86085b 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/AnchorStateRegistry.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/AnchorStateRegistry.json @@ -29,23 +29,37 @@ }, { "bytes": "20", - "label": "portal", + "label": "anchorGame", "offset": 0, "slot": "2", - "type": "contract IOptimismPortal2" + "type": "contract IFaultDisputeGame" }, { - "bytes": "20", - "label": "anchorGame", + "bytes": "64", + "label": "startingAnchorRoot", "offset": 0, "slot": "3", - "type": "contract IFaultDisputeGame" + "type": "struct Proposal" }, { - "bytes": "64", - "label": "startingAnchorRoot", + "bytes": "32", + "label": "disputeGameBlacklist", "offset": 0, - "slot": "4", - "type": "struct OutputRoot" + "slot": "5", + "type": "mapping(contract IDisputeGame => bool)" + }, + { + "bytes": "4", + "label": "respectedGameType", + "offset": 0, + "slot": "6", + "type": "GameType" + }, + { + "bytes": "8", + "label": "retirementTimestamp", + "offset": 4, + "slot": "6", + "type": "uint64" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerDeployerInterop.json b/packages/contracts-bedrock/snapshots/storageLayout/DisputeMonitorHelper.json similarity index 100% rename from packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerDeployerInterop.json rename to packages/contracts-bedrock/snapshots/storageLayout/DisputeMonitorHelper.json diff --git a/packages/contracts-bedrock/snapshots/storageLayout/ETHLockbox.json b/packages/contracts-bedrock/snapshots/storageLayout/ETHLockbox.json new file mode 100644 index 0000000000..0f69cd3e54 --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/ETHLockbox.json @@ -0,0 +1,37 @@ +[ + { + "bytes": "1", + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "uint8" + }, + { + "bytes": "1", + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "bool" + }, + { + "bytes": "20", + "label": "superchainConfig", + "offset": 2, + "slot": "0", + "type": "contract ISuperchainConfig" + }, + { + "bytes": "32", + "label": "authorizedPortals", + "offset": 0, + "slot": "1", + "type": "mapping(contract IOptimismPortal2 => bool)" + }, + { + "bytes": "32", + "label": "authorizedLockboxes", + "offset": 0, + "slot": "2", + "type": "mapping(contract IETHLockbox => bool)" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/FaultDisputeGame.json b/packages/contracts-bedrock/snapshots/storageLayout/FaultDisputeGame.json index 85a5897c02..02c6e21ec5 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/FaultDisputeGame.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/FaultDisputeGame.json @@ -88,7 +88,7 @@ "label": "startingOutputRoot", "offset": 0, "slot": "8", - "type": "struct OutputRoot" + "type": "struct Proposal" }, { "bytes": "1", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/L1BlockInterop.json b/packages/contracts-bedrock/snapshots/storageLayout/L1BlockInterop.json deleted file mode 100644 index d7f312e0bf..0000000000 --- a/packages/contracts-bedrock/snapshots/storageLayout/L1BlockInterop.json +++ /dev/null @@ -1,100 +0,0 @@ -[ - { - "bytes": "8", - "label": "number", - "offset": 0, - "slot": "0", - "type": "uint64" - }, - { - "bytes": "8", - "label": "timestamp", - "offset": 8, - "slot": "0", - "type": "uint64" - }, - { - "bytes": "32", - "label": "basefee", - "offset": 0, - "slot": "1", - "type": "uint256" - }, - { - "bytes": "32", - "label": "hash", - "offset": 0, - "slot": "2", - "type": "bytes32" - }, - { - "bytes": "8", - "label": "sequenceNumber", - "offset": 0, - "slot": "3", - "type": "uint64" - }, - { - "bytes": "4", - "label": "blobBaseFeeScalar", - "offset": 8, - "slot": "3", - "type": "uint32" - }, - { - "bytes": "4", - "label": "baseFeeScalar", - "offset": 12, - "slot": "3", - "type": "uint32" - }, - { - "bytes": "32", - "label": "batcherHash", - "offset": 0, - "slot": "4", - "type": "bytes32" - }, - { - "bytes": "32", - "label": "l1FeeOverhead", - "offset": 0, - "slot": "5", - "type": "uint256" - }, - { - "bytes": "32", - "label": "l1FeeScalar", - "offset": 0, - "slot": "6", - "type": "uint256" - }, - { - "bytes": "32", - "label": "blobBaseFee", - "offset": 0, - "slot": "7", - "type": "uint256" - }, - { - "bytes": "8", - "label": "operatorFeeConstant", - "offset": 0, - "slot": "8", - "type": "uint64" - }, - { - "bytes": "4", - "label": "operatorFeeScalar", - "offset": 8, - "slot": "8", - "type": "uint32" - }, - { - "bytes": "64", - "label": "dependencySet", - "offset": 0, - "slot": "9", - "type": "struct EnumerableSet.UintSet" - } -] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerContractsContainer.json b/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerContractsContainer.json index 98248abd1d..a8ce2f3c54 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerContractsContainer.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerContractsContainer.json @@ -7,7 +7,7 @@ "type": "struct OPContractsManager.Blueprints" }, { - "bytes": "384", + "bytes": "416", "label": "implementation", "offset": 0, "slot": "9", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json index 654695522e..a66c4b71b4 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json @@ -78,10 +78,10 @@ }, { "bytes": "20", - "label": "disputeGameFactory", + "label": "spacer_56_0_20", "offset": 0, "slot": "56", - "type": "contract IDisputeGameFactory" + "type": "address" }, { "bytes": "32", @@ -92,21 +92,21 @@ }, { "bytes": "32", - "label": "disputeGameBlacklist", + "label": "spacer_58_0_32", "offset": 0, "slot": "58", - "type": "mapping(contract IDisputeGame => bool)" + "type": "bytes32" }, { "bytes": "4", - "label": "respectedGameType", + "label": "spacer_59_0_4", "offset": 0, "slot": "59", "type": "GameType" }, { "bytes": "8", - "label": "respectedGameTypeUpdatedAt", + "label": "spacer_59_4_8", "offset": 4, "slot": "59", "type": "uint64" @@ -124,5 +124,26 @@ "offset": 0, "slot": "61", "type": "uint256" + }, + { + "bytes": "20", + "label": "anchorStateRegistry", + "offset": 0, + "slot": "62", + "type": "contract IAnchorStateRegistry" + }, + { + "bytes": "20", + "label": "ethLockbox", + "offset": 0, + "slot": "63", + "type": "contract IETHLockbox" + }, + { + "bytes": "1", + "label": "superRootsActive", + "offset": 20, + "slot": "63", + "type": "bool" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json deleted file mode 100644 index 654695522e..0000000000 --- a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json +++ /dev/null @@ -1,128 +0,0 @@ -[ - { - "bytes": "1", - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "uint8" - }, - { - "bytes": "1", - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "bool" - }, - { - "bytes": "32", - "label": "params", - "offset": 0, - "slot": "1", - "type": "struct ResourceMetering.ResourceParams" - }, - { - "bytes": "1536", - "label": "__gap", - "offset": 0, - "slot": "2", - "type": "uint256[48]" - }, - { - "bytes": "20", - "label": "l2Sender", - "offset": 0, - "slot": "50", - "type": "address" - }, - { - "bytes": "32", - "label": "finalizedWithdrawals", - "offset": 0, - "slot": "51", - "type": "mapping(bytes32 => bool)" - }, - { - "bytes": "32", - "label": "spacer_52_0_32", - "offset": 0, - "slot": "52", - "type": "bytes32" - }, - { - "bytes": "1", - "label": "spacer_53_0_1", - "offset": 0, - "slot": "53", - "type": "bool" - }, - { - "bytes": "20", - "label": "superchainConfig", - "offset": 1, - "slot": "53", - "type": "contract ISuperchainConfig" - }, - { - "bytes": "20", - "label": "spacer_54_0_20", - "offset": 0, - "slot": "54", - "type": "address" - }, - { - "bytes": "20", - "label": "systemConfig", - "offset": 0, - "slot": "55", - "type": "contract ISystemConfig" - }, - { - "bytes": "20", - "label": "disputeGameFactory", - "offset": 0, - "slot": "56", - "type": "contract IDisputeGameFactory" - }, - { - "bytes": "32", - "label": "provenWithdrawals", - "offset": 0, - "slot": "57", - "type": "mapping(bytes32 => mapping(address => struct OptimismPortal2.ProvenWithdrawal))" - }, - { - "bytes": "32", - "label": "disputeGameBlacklist", - "offset": 0, - "slot": "58", - "type": "mapping(contract IDisputeGame => bool)" - }, - { - "bytes": "4", - "label": "respectedGameType", - "offset": 0, - "slot": "59", - "type": "GameType" - }, - { - "bytes": "8", - "label": "respectedGameTypeUpdatedAt", - "offset": 4, - "slot": "59", - "type": "uint64" - }, - { - "bytes": "32", - "label": "proofSubmitters", - "offset": 0, - "slot": "60", - "type": "mapping(bytes32 => address[])" - }, - { - "bytes": "32", - "label": "spacer_61_0_32", - "offset": 0, - "slot": "61", - "type": "uint256" - } -] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/PermissionedDisputeGame.json b/packages/contracts-bedrock/snapshots/storageLayout/PermissionedDisputeGame.json index 85a5897c02..02c6e21ec5 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/PermissionedDisputeGame.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/PermissionedDisputeGame.json @@ -88,7 +88,7 @@ "label": "startingOutputRoot", "offset": 0, "slot": "8", - "type": "struct OutputRoot" + "type": "struct Proposal" }, { "bytes": "1", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/StandardValidatorV300.json b/packages/contracts-bedrock/snapshots/storageLayout/StandardValidatorV300.json new file mode 100644 index 0000000000..e4ffb9ad9b --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/StandardValidatorV300.json @@ -0,0 +1,100 @@ +[ + { + "bytes": "20", + "label": "superchainConfig", + "offset": 0, + "slot": "0", + "type": "contract ISuperchainConfig" + }, + { + "bytes": "20", + "label": "l1PAOMultisig", + "offset": 0, + "slot": "1", + "type": "address" + }, + { + "bytes": "20", + "label": "mips", + "offset": 0, + "slot": "2", + "type": "address" + }, + { + "bytes": "20", + "label": "challenger", + "offset": 0, + "slot": "3", + "type": "address" + }, + { + "bytes": "20", + "label": "l1ERC721BridgeImpl", + "offset": 0, + "slot": "4", + "type": "address" + }, + { + "bytes": "20", + "label": "optimismPortalImpl", + "offset": 0, + "slot": "5", + "type": "address" + }, + { + "bytes": "20", + "label": "systemConfigImpl", + "offset": 0, + "slot": "6", + "type": "address" + }, + { + "bytes": "20", + "label": "optimismMintableERC20FactoryImpl", + "offset": 0, + "slot": "7", + "type": "address" + }, + { + "bytes": "20", + "label": "l1CrossDomainMessengerImpl", + "offset": 0, + "slot": "8", + "type": "address" + }, + { + "bytes": "20", + "label": "l1StandardBridgeImpl", + "offset": 0, + "slot": "9", + "type": "address" + }, + { + "bytes": "20", + "label": "disputeGameFactoryImpl", + "offset": 0, + "slot": "10", + "type": "address" + }, + { + "bytes": "20", + "label": "anchorStateRegistryImpl", + "offset": 0, + "slot": "11", + "type": "address" + }, + { + "bytes": "20", + "label": "delayedWETHImpl", + "offset": 0, + "slot": "12", + "type": "address" + }, + { + "bytes": "20", + "label": "mipsImpl", + "offset": 0, + "slot": "13", + "type": "address" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/SuperFaultDisputeGame.json b/packages/contracts-bedrock/snapshots/storageLayout/SuperFaultDisputeGame.json index 62bd48ce75..8764cfaf1b 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/SuperFaultDisputeGame.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/SuperFaultDisputeGame.json @@ -71,10 +71,10 @@ }, { "bytes": "64", - "label": "startingOutputRoot", + "label": "startingProposal", "offset": 0, "slot": "7", - "type": "struct OutputRoot" + "type": "struct Proposal" }, { "bytes": "1", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/SuperPermissionedDisputeGame.json b/packages/contracts-bedrock/snapshots/storageLayout/SuperPermissionedDisputeGame.json index 62bd48ce75..8764cfaf1b 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/SuperPermissionedDisputeGame.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/SuperPermissionedDisputeGame.json @@ -71,10 +71,10 @@ }, { "bytes": "64", - "label": "startingOutputRoot", + "label": "startingProposal", "offset": 0, "slot": "7", - "type": "struct OutputRoot" + "type": "struct Proposal" }, { "bytes": "1", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json b/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json index ea0d05feb9..3d1796e5e4 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/SystemConfig.json @@ -110,5 +110,12 @@ "offset": 12, "slot": "106", "type": "uint64" + }, + { + "bytes": "32", + "label": "l2ChainId", + "offset": 0, + "slot": "107", + "type": "uint256" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/SystemConfigInterop.json b/packages/contracts-bedrock/snapshots/storageLayout/SystemConfigInterop.json deleted file mode 100644 index ea0d05feb9..0000000000 --- a/packages/contracts-bedrock/snapshots/storageLayout/SystemConfigInterop.json +++ /dev/null @@ -1,114 +0,0 @@ -[ - { - "bytes": "1", - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "uint8" - }, - { - "bytes": "1", - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "bool" - }, - { - "bytes": "1600", - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "uint256[50]" - }, - { - "bytes": "20", - "label": "_owner", - "offset": 0, - "slot": "51", - "type": "address" - }, - { - "bytes": "1568", - "label": "__gap", - "offset": 0, - "slot": "52", - "type": "uint256[49]" - }, - { - "bytes": "32", - "label": "overhead", - "offset": 0, - "slot": "101", - "type": "uint256" - }, - { - "bytes": "32", - "label": "scalar", - "offset": 0, - "slot": "102", - "type": "uint256" - }, - { - "bytes": "32", - "label": "batcherHash", - "offset": 0, - "slot": "103", - "type": "bytes32" - }, - { - "bytes": "8", - "label": "gasLimit", - "offset": 0, - "slot": "104", - "type": "uint64" - }, - { - "bytes": "4", - "label": "basefeeScalar", - "offset": 8, - "slot": "104", - "type": "uint32" - }, - { - "bytes": "4", - "label": "blobbasefeeScalar", - "offset": 12, - "slot": "104", - "type": "uint32" - }, - { - "bytes": "32", - "label": "_resourceConfig", - "offset": 0, - "slot": "105", - "type": "struct IResourceMetering.ResourceConfig" - }, - { - "bytes": "4", - "label": "eip1559Denominator", - "offset": 0, - "slot": "106", - "type": "uint32" - }, - { - "bytes": "4", - "label": "eip1559Elasticity", - "offset": 4, - "slot": "106", - "type": "uint32" - }, - { - "bytes": "4", - "label": "operatorFeeScalar", - "offset": 8, - "slot": "106", - "type": "uint32" - }, - { - "bytes": "8", - "label": "operatorFeeConstant", - "offset": 12, - "slot": "106", - "type": "uint64" - } -] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L1/ETHLockbox.sol b/packages/contracts-bedrock/src/L1/ETHLockbox.sol new file mode 100644 index 0000000000..364194a39d --- /dev/null +++ b/packages/contracts-bedrock/src/L1/ETHLockbox.sol @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +// Contracts +import { ProxyAdminOwnedBase } from "src/L1/ProxyAdminOwnedBase.sol"; +import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; + +// Libraries +import { Constants } from "src/libraries/Constants.sol"; + +// Interfaces +import { ISemver } from "interfaces/universal/ISemver.sol"; +import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPortal2.sol"; +import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; + +/// @custom:proxied true +/// @title ETHLockbox +/// @notice Manages ETH liquidity locking and unlocking for authorized OptimismPortals, enabling unified ETH liquidity +/// management across chains in the superchain cluster. +contract ETHLockbox is ProxyAdminOwnedBase, Initializable, ISemver { + /// @notice Thrown when the lockbox is paused. + error ETHLockbox_Paused(); + + /// @notice Thrown when the caller is not authorized. + error ETHLockbox_Unauthorized(); + + /// @notice Thrown when the value to unlock is greater than the balance of the lockbox. + error ETHLockbox_InsufficientBalance(); + + /// @notice Thrown when attempting to unlock ETH from the lockbox through a withdrawal transaction. + error ETHLockbox_NoWithdrawalTransactions(); + + /// @notice Thrown when the admin owner of the lockbox is different from the admin owner of the proxy admin. + error ETHLockbox_DifferentProxyAdminOwner(); + + /// @notice Thrown when any authorized portal has a different SuperchainConfig. + error ETHLockbox_DifferentSuperchainConfig(); + + /// @notice Emitted when ETH is locked in the lockbox by an authorized portal. + /// @param portal The address of the portal that locked the ETH. + /// @param amount The amount of ETH locked. + event ETHLocked(IOptimismPortal indexed portal, uint256 amount); + + /// @notice Emitted when ETH is unlocked from the lockbox by an authorized portal. + /// @param portal The address of the portal that unlocked the ETH. + /// @param amount The amount of ETH unlocked. + event ETHUnlocked(IOptimismPortal indexed portal, uint256 amount); + + /// @notice Emitted when a portal is authorized to lock and unlock ETH. + /// @param portal The address of the portal that was authorized. + event PortalAuthorized(IOptimismPortal indexed portal); + + /// @notice Emitted when an ETH lockbox is authorized to migrate its liquidity to the current ETH lockbox. + /// @param lockbox The address of the ETH lockbox that was authorized. + event LockboxAuthorized(IETHLockbox indexed lockbox); + + /// @notice Emitted when ETH liquidity is migrated from the current ETH lockbox to another. + /// @param lockbox The address of the ETH lockbox that was migrated. + event LiquidityMigrated(IETHLockbox indexed lockbox, uint256 amount); + + /// @notice Emitted when ETH liquidity is received during an authorized lockbox migration. + /// @param lockbox The address of the ETH lockbox that received the liquidity. + /// @param amount The amount of ETH received. + event LiquidityReceived(IETHLockbox indexed lockbox, uint256 amount); + + /// @notice The address of the SuperchainConfig contract. + ISuperchainConfig public superchainConfig; + + /// @notice Mapping of authorized portals. + mapping(IOptimismPortal => bool) public authorizedPortals; + + /// @notice Mapping of authorized lockboxes. + mapping(IETHLockbox => bool) public authorizedLockboxes; + + /// @notice Semantic version. + /// @custom:semver 1.0.0 + function version() public view virtual returns (string memory) { + return "1.0.0"; + } + + /// @notice Constructs the ETHLockbox contract. + constructor() { + _disableInitializers(); + } + + /// @notice Initializer. + /// @param _superchainConfig The address of the SuperchainConfig contract. + /// @param _portals The addresses of the portals to authorize. + function initialize( + ISuperchainConfig _superchainConfig, + IOptimismPortal[] calldata _portals + ) + external + initializer + { + superchainConfig = ISuperchainConfig(_superchainConfig); + for (uint256 i; i < _portals.length; i++) { + _authorizePortal(_portals[i]); + } + } + + /// @notice Getter for the current paused status. + function paused() public view returns (bool) { + return superchainConfig.paused(); + } + + /// @notice Authorizes a portal to lock and unlock ETH. + /// @param _portal The address of the portal to authorize. + function authorizePortal(IOptimismPortal _portal) external { + // Check that the sender is the proxy admin owner. + if (msg.sender != proxyAdminOwner()) revert ETHLockbox_Unauthorized(); + + // Authorize the portal. + _authorizePortal(_portal); + } + + /// @notice Receives the ETH liquidity migrated from an authorized lockbox. + function receiveLiquidity() external payable { + // Check that the sender is authorized to trigger this function. + IETHLockbox sender = IETHLockbox(payable(msg.sender)); + if (!authorizedLockboxes[sender]) revert ETHLockbox_Unauthorized(); + + // Emit the event. + emit LiquidityReceived(sender, msg.value); + } + + /// @notice Locks ETH in the lockbox. + /// Called by an authorized portal on a deposit to lock the ETH value. + function lockETH() external payable { + // Check that the sender is authorized to trigger this function. + IOptimismPortal sender = IOptimismPortal(payable(msg.sender)); + if (!authorizedPortals[sender]) revert ETHLockbox_Unauthorized(); + + // Emit the event. + emit ETHLocked(sender, msg.value); + } + + /// @notice Unlocks ETH from the lockbox. + /// Called by an authorized portal when finalizing a withdrawal that requires ETH. + /// Cannot be called if the lockbox is paused. + /// @param _value The amount of ETH to unlock. + function unlockETH(uint256 _value) external { + // Unlocks are blocked when paused, locks are not. + if (paused()) revert ETHLockbox_Paused(); + + // Check that the sender is authorized to trigger this function. + IOptimismPortal sender = IOptimismPortal(payable(msg.sender)); + if (!authorizedPortals[sender]) revert ETHLockbox_Unauthorized(); + + // Check that we have enough balance to process the unlock. + if (_value > address(this).balance) revert ETHLockbox_InsufficientBalance(); + + // Check that the sender is not executing a withdrawal transaction. + if (sender.l2Sender() != Constants.DEFAULT_L2_SENDER) { + revert ETHLockbox_NoWithdrawalTransactions(); + } + + // Using donateETH to avoid triggering a deposit. + sender.donateETH{ value: _value }(); + + // Emit the event. + emit ETHUnlocked(sender, _value); + } + + /// @notice Authorizes an ETH lockbox to migrate its liquidity to the current ETH lockbox. We + /// allow this function to be called more than once for the same lockbox. A lockbox + /// cannot be removed from the authorized list once added. + /// @param _lockbox The address of the ETH lockbox to authorize. + function authorizeLockbox(IETHLockbox _lockbox) external { + // Check that the sender is the proxy admin owner. + if (msg.sender != proxyAdminOwner()) revert ETHLockbox_Unauthorized(); + + // Check that the lockbox has the same proxy admin owner. + if (!_sameProxyAdminOwner(address(_lockbox))) revert ETHLockbox_DifferentProxyAdminOwner(); + + // Authorize the lockbox. + authorizedLockboxes[_lockbox] = true; + + // Emit the event. + emit LockboxAuthorized(_lockbox); + } + + /// @notice Migrates liquidity from the current ETH lockbox to another. + /// @dev Must be called atomically with `OptimismPortal.updateLockbox()` in the same + /// transaction batch, or otherwise the OptimismPortal may not be able to unlock ETH + /// from the ETHLockbox on finalized withdrawals. + /// @param _lockbox The address of the ETH lockbox to migrate liquidity to. + function migrateLiquidity(IETHLockbox _lockbox) external { + // Check that the sender is the proxy admin owner. + if (msg.sender != proxyAdminOwner()) revert ETHLockbox_Unauthorized(); + + // Check that the lockbox has the same proxy admin owner. + if (!_sameProxyAdminOwner(address(_lockbox))) revert ETHLockbox_DifferentProxyAdminOwner(); + + // Receive the liquidity. + IETHLockbox(_lockbox).receiveLiquidity{ value: address(this).balance }(); + + // Emit the event. + emit LiquidityMigrated(_lockbox, address(this).balance); + } + + /// @notice Authorizes a portal to lock and unlock ETH. + /// @param _portal The address of the portal to authorize. + function _authorizePortal(IOptimismPortal _portal) internal { + // Check that the portal has the same proxy admin owner. + if (!_sameProxyAdminOwner(address(_portal))) revert ETHLockbox_DifferentProxyAdminOwner(); + + // Check that the portal has the same superchain config. + if (_portal.superchainConfig() != superchainConfig) revert ETHLockbox_DifferentSuperchainConfig(); + + // Authorize the portal. + authorizedPortals[_portal] = true; + + // Emit the event. + emit PortalAuthorized(_portal); + } +} diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index 8aaabee8ef..f30d26b4c2 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.15; import { Blueprint } from "src/libraries/Blueprint.sol"; import { Constants } from "src/libraries/Constants.sol"; import { Bytes } from "src/libraries/Bytes.sol"; -import { Claim, Duration, GameType, GameTypes, OutputRoot } from "src/dispute/lib/Types.sol"; +import { Claim, Duration, GameType, Hash, GameTypes, Proposal } from "src/dispute/lib/Types.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; // Interfaces @@ -17,20 +17,19 @@ import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.so import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IAddressManager } from "interfaces/legacy/IAddressManager.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; -import { IDelayedWETH } from "interfaces/dispute/IDelayedWETH.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { IPermissionedDisputeGame } from "interfaces/dispute/IPermissionedDisputeGame.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; +import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPortal2.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.sol"; import { IL1ERC721Bridge } from "interfaces/L1/IL1ERC721Bridge.sol"; import { IL1StandardBridge } from "interfaces/L1/IL1StandardBridge.sol"; import { IOptimismMintableERC20Factory } from "interfaces/universal/IOptimismMintableERC20Factory.sol"; import { IHasSuperchainConfig } from "interfaces/L1/IHasSuperchainConfig.sol"; -import { ISystemConfigInterop } from "interfaces/L1/ISystemConfigInterop.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; contract OPContractsManagerContractsContainer { /// @notice Addresses of the Blueprint contracts. @@ -298,6 +297,7 @@ contract OPContractsManagerGameTypeAdder is OPContractsManagerBase { // This conversion is safe because the GameType is a uint32, which will always fit in an int256. int256 gameTypeInt = int256(uint256(gameConfig.disputeGameType.raw())); + // Ensure that the game configs are added in ascending order, and not duplicated. if (lastGameConfig >= gameTypeInt) revert OPContractsManager.InvalidGameConfigs(); lastGameConfig = gameTypeInt; @@ -309,6 +309,7 @@ contract OPContractsManagerGameTypeAdder is OPContractsManagerBase { getGameImplementation(getDisputeGameFactory(gameConfig.systemConfig), GameTypes.PERMISSIONED_CANNON) ) ); + // Pull out the chain ID. uint256 l2ChainId = getL2ChainId(pdg); @@ -526,73 +527,144 @@ contract OPContractsManagerUpgrader is OPContractsManagerBase { /// @dev This function is intended to be called via DELEGATECALL from the Upgrade Controller Safe function upgrade(OPContractsManager.OpChainConfig[] memory _opChainConfigs) external virtual { OPContractsManager.Implementations memory impls = getImplementations(); - OPContractsManager.Blueprints memory bps = getBlueprints(); for (uint256 i = 0; i < _opChainConfigs.length; i++) { assertValidOpChainConfig(_opChainConfigs[i]); - ISystemConfig.Addresses memory opChainAddrs = _opChainConfigs[i].systemConfigProxy.getAddresses(); - // -------- Upgrade SystemConfig to Isthmus implementation -------- - upgradeTo( - _opChainConfigs[i].proxyAdmin, address(_opChainConfigs[i].systemConfigProxy), impls.systemConfigImpl - ); + // Use the SystemConfig to grab the DisputeGameFactory address. + IDisputeGameFactory dgf = IDisputeGameFactory(_opChainConfigs[i].systemConfigProxy.disputeGameFactory()); - // -------- Upgrade Contracts Stored in SystemConfig -------- + // All chains have the PermissionedDisputeGame, grab that. + IPermissionedDisputeGame permissionedDisputeGame = + IPermissionedDisputeGame(address(getGameImplementation(dgf, GameTypes.PERMISSIONED_CANNON))); - // OptimismPortal and L1CrossDomainMessenger are being upgraded to include the fixes - // for EIP-7623 (minimum gas limits for L1 -> L2 messages). - upgradeTo(_opChainConfigs[i].proxyAdmin, opChainAddrs.optimismPortal, impls.optimismPortalImpl); - upgradeTo( - _opChainConfigs[i].proxyAdmin, opChainAddrs.l1CrossDomainMessenger, impls.l1CrossDomainMessengerImpl - ); + // Grab the L2 chain ID from the PermissionedDisputeGame. + uint256 l2ChainId = getL2ChainId(IFaultDisputeGame(address(permissionedDisputeGame))); - // L1ERC721Bridge and L1StandardBridge are being upgraded to include the tweaks to the - // EOA checking code for EIP-7702 (code length == 23). - upgradeTo(_opChainConfigs[i].proxyAdmin, opChainAddrs.l1ERC721Bridge, impls.l1ERC721BridgeImpl); - upgradeTo(_opChainConfigs[i].proxyAdmin, opChainAddrs.l1StandardBridge, impls.l1StandardBridgeImpl); + // Start by upgrading the SystemConfig contract to have the l2ChainId. + upgradeToAndCall( + _opChainConfigs[i].proxyAdmin, + address(_opChainConfigs[i].systemConfigProxy), + impls.systemConfigImpl, + abi.encodeCall(ISystemConfig.upgrade, (l2ChainId)) + ); - // -------- Discover and Upgrade Proofs Contracts -------- + // Grab chain addresses here. We need to do this after the SystemConfig upgrade or the + // addresses will be incorrect. + ISystemConfig.Addresses memory opChainAddrs = _opChainConfigs[i].systemConfigProxy.getAddresses(); - // All chains have the Permissioned Dispute Game. - IPermissionedDisputeGame permissionedDisputeGame = IPermissionedDisputeGame( - address( - getGameImplementation( - IDisputeGameFactory(opChainAddrs.disputeGameFactory), GameTypes.PERMISSIONED_CANNON - ) - ) - ); + // Grab the current respectedGameType from the OptimismPortal contract before the upgrade. + GameType respectedGameType = IOptimismPortal(payable(opChainAddrs.optimismPortal)).respectedGameType(); + + // Grab the current SuperchainConfig from the OptimismPortal contract before the upgrade. + ISuperchainConfig superchainConfig = + IOptimismPortal(payable(opChainAddrs.optimismPortal)).superchainConfig(); + + // Separate context to avoid stack too deep. + IAnchorStateRegistry newAnchorStateRegistryProxy; + { + // Deploy a new AnchorStateRegistry contract. + // We use the SOT suffix to avoid CREATE2 conflicts with the existing ASR. + newAnchorStateRegistryProxy = IAnchorStateRegistry( + deployProxy({ + _l2ChainId: l2ChainId, + _proxyAdmin: _opChainConfigs[i].proxyAdmin, + _saltMixer: reusableSaltMixer(_opChainConfigs[i]), + _contractName: "AnchorStateRegistry-SOT" + }) + ); - // We're also going to need the l2ChainId below, so we cache it in the outer scope. - uint256 l2ChainId = getL2ChainId(IFaultDisputeGame(address(permissionedDisputeGame))); + // Separate context to avoid stack too deep. + { + // Get the existing anchor root from the old AnchorStateRegistry contract. + // Get the AnchorStateRegistry from the PermissionedDisputeGame. + (Hash root, uint256 l2BlockNumber) = getAnchorStateRegistry( + IFaultDisputeGame(address(permissionedDisputeGame)) + ).anchors(respectedGameType); + + // Upgrade and initialize the AnchorStateRegistry contract. + // Since this is a net-new contract, we need to initialize it. + upgradeToAndCall( + _opChainConfigs[i].proxyAdmin, + address(newAnchorStateRegistryProxy), + impls.anchorStateRegistryImpl, + abi.encodeCall( + IAnchorStateRegistry.initialize, + ( + superchainConfig, + dgf, + Proposal({ root: root, l2SequenceNumber: l2BlockNumber }), + respectedGameType + ) + ) + ); + } + } - deployAndSetNewGameImpl({ - _l2ChainId: l2ChainId, - _disputeGame: IDisputeGame(address(permissionedDisputeGame)), - _gameType: GameTypes.PERMISSIONED_CANNON, - _opChainConfig: _opChainConfigs[i], - _implementations: impls, - _blueprints: bps, - _opChainAddrs: opChainAddrs - }); + // Upgrade the OptimismPortal contract implementation. + upgradeTo(_opChainConfigs[i].proxyAdmin, opChainAddrs.optimismPortal, impls.optimismPortalImpl); - // Now retrieve the permissionless game. If it exists, replace its implementation. - IFaultDisputeGame permissionlessDisputeGame = IFaultDisputeGame( - address(getGameImplementation(IDisputeGameFactory(opChainAddrs.disputeGameFactory), GameTypes.CANNON)) - ); + // Separate context to avoid stack too deep. + { + // Deploy the ETHLockbox proxy. + IETHLockbox ethLockbox; + { + ethLockbox = IETHLockbox( + deployProxy({ + _l2ChainId: l2ChainId, + _proxyAdmin: _opChainConfigs[i].proxyAdmin, + _saltMixer: reusableSaltMixer(_opChainConfigs[i]), + _contractName: "ETHLockbox" + }) + ); + + // Initialize the ETHLockbox setting the OptimismPortal as an authorized portal. + IOptimismPortal[] memory portals = new IOptimismPortal[](1); + portals[0] = IOptimismPortal(payable(opChainAddrs.optimismPortal)); + upgradeToAndCall( + _opChainConfigs[i].proxyAdmin, + address(ethLockbox), + impls.ethLockboxImpl, + abi.encodeCall(IETHLockbox.initialize, (superchainConfig, portals)) + ); + } + + // Call `upgrade` on the OptimismPortal contract. + IOptimismPortal(payable(opChainAddrs.optimismPortal)).upgrade(newAnchorStateRegistryProxy, ethLockbox); + } - if (address(permissionlessDisputeGame) != address(0)) { - // Deploy and set a new permissionless game to update its prestate + // We also need to redeploy the dispute games because the AnchorStateRegistry is new. + // Separate context to avoid stack too deep. + { + // Deploy and set a new permissioned game to update its prestate. deployAndSetNewGameImpl({ _l2ChainId: l2ChainId, - _disputeGame: IDisputeGame(address(permissionlessDisputeGame)), - _gameType: GameTypes.CANNON, - _opChainConfig: _opChainConfigs[i], - _implementations: impls, - _blueprints: bps, - _opChainAddrs: opChainAddrs + _disputeGame: IDisputeGame(address(permissionedDisputeGame)), + _newAnchorStateRegistryProxy: newAnchorStateRegistryProxy, + _gameType: GameTypes.PERMISSIONED_CANNON, + _opChainConfig: _opChainConfigs[i] }); } + // Separate context to avoid stack too deep. + { + // Now retrieve the permissionless game. + IFaultDisputeGame permissionlessDisputeGame = + IFaultDisputeGame(address(getGameImplementation(dgf, GameTypes.CANNON))); + + // If it exists, replace its implementation. + if (address(permissionlessDisputeGame) != address(0)) { + // Deploy and set a new permissionless game to update its prestate + deployAndSetNewGameImpl({ + _l2ChainId: l2ChainId, + _disputeGame: IDisputeGame(address(permissionlessDisputeGame)), + _newAnchorStateRegistryProxy: newAnchorStateRegistryProxy, + _gameType: GameTypes.CANNON, + _opChainConfig: _opChainConfigs[i] + }); + } + } + // Emit the upgraded event with the address of the caller. Since this will be a delegatecall, // the caller will be the value of the ADDRESS opcode. emit Upgraded(l2ChainId, _opChainConfigs[i].systemConfigProxy, address(this)); @@ -621,32 +693,39 @@ contract OPContractsManagerUpgrader is OPContractsManagerBase { /// @notice Deploys and sets a new dispute game implementation /// @param _l2ChainId The L2 chain ID /// @param _disputeGame The current dispute game implementation + /// @param _newAnchorStateRegistryProxy The new anchor state registry proxy /// @param _gameType The type of game to deploy /// @param _opChainConfig The OP chain configuration - /// @param _blueprints The blueprint addresses - /// @param _implementations The implementation addresses - /// @param _opChainAddrs The OP chain addresses function deployAndSetNewGameImpl( uint256 _l2ChainId, IDisputeGame _disputeGame, + IAnchorStateRegistry _newAnchorStateRegistryProxy, GameType _gameType, - OPContractsManager.OpChainConfig memory _opChainConfig, - OPContractsManager.Blueprints memory _blueprints, - OPContractsManager.Implementations memory _implementations, - ISystemConfig.Addresses memory _opChainAddrs + OPContractsManager.OpChainConfig memory _opChainConfig ) internal { + OPContractsManager.Blueprints memory bps = getBlueprints(); + OPContractsManager.Implementations memory impls = getImplementations(); + // Get the constructor params for the game IFaultDisputeGame.GameConstructorParams memory params = getGameConstructorParams(IFaultDisputeGame(address(_disputeGame))); // Modify the params with the new vm values. - params.vm = IBigStepper(_implementations.mipsImpl); - if (Claim.unwrap(_opChainConfig.absolutePrestate) == bytes32(0)) { + params.anchorStateRegistry = IAnchorStateRegistry(address(_newAnchorStateRegistryProxy)); + params.vm = IBigStepper(impls.mipsImpl); + + // If the prestate is set in the config, use it. If not set, we'll try to use the prestate + // that already exists on the current dispute game. + if (Claim.unwrap(_opChainConfig.absolutePrestate) != bytes32(0)) { + params.absolutePrestate = _opChainConfig.absolutePrestate; + } + + // As a sanity check, if the prestate is zero here, revert. + if (params.absolutePrestate.raw() == bytes32(0)) { revert OPContractsManager.PrestateNotSet(); } - params.absolutePrestate = _opChainConfig.absolutePrestate; IDisputeGame newGame; if (GameType.unwrap(_gameType) == GameType.unwrap(GameTypes.PERMISSIONED_CANNON)) { @@ -654,8 +733,8 @@ contract OPContractsManagerUpgrader is OPContractsManagerBase { address challenger = getChallenger(IPermissionedDisputeGame(address(_disputeGame))); newGame = IDisputeGame( Blueprint.deployFrom( - _blueprints.permissionedDisputeGame1, - _blueprints.permissionedDisputeGame2, + bps.permissionedDisputeGame1, + bps.permissionedDisputeGame2, computeSalt(_l2ChainId, reusableSaltMixer(_opChainConfig), "PermissionedDisputeGame"), encodePermissionedFDGConstructor(params, proposer, challenger) ) @@ -663,14 +742,19 @@ contract OPContractsManagerUpgrader is OPContractsManagerBase { } else { newGame = IDisputeGame( Blueprint.deployFrom( - _blueprints.permissionlessDisputeGame1, - _blueprints.permissionlessDisputeGame2, + bps.permissionlessDisputeGame1, + bps.permissionlessDisputeGame2, computeSalt(_l2ChainId, reusableSaltMixer(_opChainConfig), "PermissionlessDisputeGame"), encodePermissionlessFDGConstructor(params) ) ); } - setDGFImplementation(IDisputeGameFactory(_opChainAddrs.disputeGameFactory), _gameType, IDisputeGame(newGame)); + + // Grab the DisputeGameFactory from the SystemConfig. + IDisputeGameFactory dgf = IDisputeGameFactory(_opChainConfig.systemConfigProxy.disputeGameFactory()); + + // Set the new implementation. + setDGFImplementation(dgf, _gameType, IDisputeGame(newGame)); } } @@ -727,9 +811,11 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { // Deploy ERC-1967 proxied contracts. output.l1ERC721BridgeProxy = IL1ERC721Bridge(deployProxy(_input.l2ChainId, output.opChainProxyAdmin, _input.saltMixer, "L1ERC721Bridge")); - output.optimismPortalProxy = IOptimismPortal2( + output.optimismPortalProxy = IOptimismPortal( payable(deployProxy(_input.l2ChainId, output.opChainProxyAdmin, _input.saltMixer, "OptimismPortal")) ); + output.ethLockboxProxy = + IETHLockbox(deployProxy(_input.l2ChainId, output.opChainProxyAdmin, _input.saltMixer, "ETHLockbox")); output.systemConfigProxy = ISystemConfig(deployProxy(_input.l2ChainId, output.opChainProxyAdmin, _input.saltMixer, "SystemConfig")); output.optimismMintableERC20FactoryProxy = IOptimismMintableERC20Factory( @@ -811,6 +897,12 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { output.opChainProxyAdmin, address(output.optimismPortalProxy), implementation.optimismPortalImpl, data ); + // Initialize the ETHLockbox. + IOptimismPortal[] memory portals = new IOptimismPortal[](1); + portals[0] = output.optimismPortalProxy; + data = encodeETHLockboxInitializer(_superchainConfig, portals); + upgradeToAndCall(output.opChainProxyAdmin, address(output.ethLockboxProxy), implementation.ethLockboxImpl, data); + data = encodeSystemConfigInitializer(_input, output); upgradeToAndCall( output.opChainProxyAdmin, address(output.systemConfigProxy), implementation.systemConfigImpl, data @@ -895,7 +987,6 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { l1CrossDomainMessenger: address(_output.l1CrossDomainMessengerProxy), l1ERC721Bridge: address(_output.l1ERC721BridgeProxy), l1StandardBridge: address(_output.l1StandardBridgeProxy), - disputeGameFactory: address(_output.disputeGameFactoryProxy), optimismPortal: address(_output.optimismPortalProxy), optimismMintableERC20Factory: address(_output.optimismMintableERC20FactoryProxy) }); @@ -903,7 +994,6 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { assertValidContractAddress(opChainAddrs_.l1CrossDomainMessenger); assertValidContractAddress(opChainAddrs_.l1ERC721Bridge); assertValidContractAddress(opChainAddrs_.l1StandardBridge); - assertValidContractAddress(opChainAddrs_.disputeGameFactory); assertValidContractAddress(opChainAddrs_.optimismPortal); assertValidContractAddress(opChainAddrs_.optimismMintableERC20Factory); } @@ -964,16 +1054,24 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { returns (bytes memory) { return abi.encodeCall( - IOptimismPortal2.initialize, - ( - _output.disputeGameFactoryProxy, - _output.systemConfigProxy, - _superchainConfig, - GameTypes.PERMISSIONED_CANNON - ) + IOptimismPortal.initialize, + (_output.systemConfigProxy, _superchainConfig, _output.anchorStateRegistryProxy, _output.ethLockboxProxy) ); } + /// @notice Helper method for encoding the ETHLockbox initializer data. + function encodeETHLockboxInitializer( + ISuperchainConfig _superchainConfig, + IOptimismPortal[] memory _portals + ) + internal + view + virtual + returns (bytes memory) + { + return abi.encodeCall(IETHLockbox.initialize, (_superchainConfig, _portals)); + } + /// @notice Helper method for encoding the SystemConfig initializer data. function encodeSystemConfigInitializer( OPContractsManager.DeployInput memory _input, @@ -998,7 +1096,8 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { _input.roles.unsafeBlockSigner, referenceResourceConfig, chainIdToBatchInboxAddress(_input.l2ChainId), - opChainAddrs + opChainAddrs, + _input.l2ChainId ) ); } @@ -1055,10 +1154,10 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { virtual returns (bytes memory) { - OutputRoot memory startingAnchorRoot = abi.decode(_input.startingAnchorRoot, (OutputRoot)); + Proposal memory startingAnchorRoot = abi.decode(_input.startingAnchorRoot, (Proposal)); return abi.encodeCall( IAnchorStateRegistry.initialize, - (_superchainConfig, _output.disputeGameFactoryProxy, _output.optimismPortalProxy, startingAnchorRoot) + (_superchainConfig, _output.disputeGameFactoryProxy, startingAnchorRoot, GameTypes.PERMISSIONED_CANNON) ); } @@ -1075,51 +1174,6 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { } } -contract OPContractsManagerDeployerInterop is OPContractsManagerDeployer { - constructor(OPContractsManagerContractsContainer _contractsContainer) - OPContractsManagerDeployer(_contractsContainer) - { } - - // The `SystemConfigInterop` contract has an extra `address _dependencyManager` argument - // that we must account for. - function encodeSystemConfigInitializer( - OPContractsManager.DeployInput memory _input, - OPContractsManager.DeployOutput memory _output - ) - internal - view - virtual - override - returns (bytes memory) - { - (IResourceMetering.ResourceConfig memory referenceResourceConfig, ISystemConfig.Addresses memory opChainAddrs) = - defaultSystemConfigParams(_input, _output); - - // TODO For now we assume that the dependency manager is the same as system config owner. - // This is currently undefined since it's not part of the standard config, so we may need - // to update where this value is pulled from in the future. To support a different dependency - // manager in this contract without an invasive change of redefining the `Roles` struct, - // we will make the change described in https://github.com/ethereum-optimism/optimism/issues/11783. - address dependencyManager = address(_input.roles.systemConfigOwner); - - return abi.encodeCall( - ISystemConfigInterop.initialize, - ( - _input.roles.systemConfigOwner, - _input.basefeeScalar, - _input.blobBasefeeScalar, - bytes32(uint256(uint160(_input.roles.batcher))), // batcherHash - _input.gasLimit, - _input.roles.unsafeBlockSigner, - referenceResourceConfig, - chainIdToBatchInboxAddress(_input.l2ChainId), - opChainAddrs, - dependencyManager - ) - ); - } -} - contract OPContractsManager is ISemver { // -------- Structs -------- @@ -1139,7 +1193,7 @@ contract OPContractsManager is ISemver { uint32 basefeeScalar; uint32 blobBasefeeScalar; uint256 l2ChainId; - // The correct type is OutputRoot memory but OP Deployer does not yet support structs. + // The correct type is Proposal memory but OP Deployer does not yet support structs. bytes startingAnchorRoot; // The salt mixer is used as part of making the resulting salt unique. string saltMixer; @@ -1162,8 +1216,9 @@ contract OPContractsManager is ISemver { IOptimismMintableERC20Factory optimismMintableERC20FactoryProxy; IL1StandardBridge l1StandardBridgeProxy; IL1CrossDomainMessenger l1CrossDomainMessengerProxy; + IETHLockbox ethLockboxProxy; // Fault proof contracts below. - IOptimismPortal2 optimismPortalProxy; + IOptimismPortal optimismPortalProxy; IDisputeGameFactory disputeGameFactoryProxy; IAnchorStateRegistry anchorStateRegistryProxy; IFaultDisputeGame faultDisputeGame; @@ -1195,6 +1250,7 @@ contract OPContractsManager is ISemver { address protocolVersionsImpl; address l1ERC721BridgeImpl; address optimismPortalImpl; + address ethLockboxImpl; address systemConfigImpl; address optimismMintableERC20FactoryImpl; address l1CrossDomainMessengerImpl; @@ -1235,9 +1291,9 @@ contract OPContractsManager is ISemver { // -------- Constants and Variables -------- - /// @custom:semver 1.9.0 + /// @custom:semver 1.12.1 function version() public pure virtual returns (string memory) { - return "1.9.0"; + return "1.12.1"; } OPContractsManagerGameTypeAdder public immutable opcmGameTypeAdder; diff --git a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol index 0b43241ea4..a6478226fb 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol @@ -2,11 +2,12 @@ pragma solidity 0.8.15; // Contracts +import { ProxyAdminOwnedBase } from "src/L1/ProxyAdminOwnedBase.sol"; import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; import { ResourceMetering } from "src/L1/ResourceMetering.sol"; +import { ReinitializableBase } from "src/universal/ReinitializableBase.sol"; // Libraries -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { EOA } from "src/libraries/EOA.sol"; import { SafeCall } from "src/libraries/SafeCall.sol"; import { Constants } from "src/libraries/Constants.sol"; @@ -14,59 +15,35 @@ import { Types } from "src/libraries/Types.sol"; import { Hashing } from "src/libraries/Hashing.sol"; import { SecureMerkleTrie } from "src/libraries/trie/SecureMerkleTrie.sol"; import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol"; -import { - BadTarget, - LargeCalldata, - SmallGasLimit, - Unauthorized, - CallPaused, - GasEstimation, - NonReentrant, - InvalidProof, - InvalidGameType, - InvalidDisputeGame, - InvalidMerkleProof, - Blacklisted, - Unproven, - ProposalNotValidated, - AlreadyFinalized, - LegacyGame -} from "src/libraries/PortalErrors.sol"; -import { GameStatus, GameType, Claim, Timestamp } from "src/dispute/lib/Types.sol"; +import { GameStatus, GameType } from "src/dispute/lib/Types.sol"; // Interfaces -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; +import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; /// @custom:proxied true /// @title OptimismPortal2 /// @notice The OptimismPortal is a low-level contract responsible for passing messages between L1 /// and L2. Messages sent directly to the OptimismPortal have no form of replayability. /// Users are encouraged to use the L1CrossDomainMessenger for a higher-level interface. -contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { - /// @notice Allows for interactions with non standard ERC20 tokens. - using SafeERC20 for IERC20; - +contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase, ProxyAdminOwnedBase, ISemver { /// @notice Represents a proven withdrawal. - /// @custom:field disputeGameProxy The address of the dispute game proxy that the withdrawal was proven against. + /// @custom:field disputeGameProxy Game that the withdrawal was proven against. /// @custom:field timestamp Timestamp at which the withdrawal was proven. struct ProvenWithdrawal { IDisputeGame disputeGameProxy; uint64 timestamp; } - /// @notice The delay between when a withdrawal transaction is proven and when it may be finalized. + /// @notice The delay between when a withdrawal is proven and when it may be finalized. uint256 internal immutable PROOF_MATURITY_DELAY_SECONDS; - /// @notice The delay between when a dispute game is resolved and when a withdrawal proven against it may be - /// finalized. - uint256 internal immutable DISPUTE_GAME_FINALITY_DELAY_SECONDS; - /// @notice Version of the deposit event. uint256 internal constant DEPOSIT_VERSION = 0; @@ -94,7 +71,7 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// @notice Spacer for backwards compatibility. bool private spacer_53_0_1; - /// @notice Contract of the Superchain Config. + /// @notice Address of the SuperchainConfig contract. ISuperchainConfig public superchainConfig; /// @custom:legacy @@ -102,25 +79,30 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// @notice Spacer taking up the legacy `l2Oracle` address slot. address private spacer_54_0_20; - /// @notice Contract of the SystemConfig. + /// @notice Address of the SystemConfig contract. /// @custom:network-specific ISystemConfig public systemConfig; - /// @notice Address of the DisputeGameFactory. /// @custom:network-specific - IDisputeGameFactory public disputeGameFactory; + /// @custom:legacy + /// @custom:spacer disputeGameFactory + /// @notice Spacer taking up the legacy `disputeGameFactory` address slot. + address private spacer_56_0_20; - /// @notice A mapping of withdrawal hashes to proof submitters to `ProvenWithdrawal` data. + /// @notice A mapping of withdrawal hashes to proof submitters to ProvenWithdrawal data. mapping(bytes32 => mapping(address => ProvenWithdrawal)) public provenWithdrawals; - /// @notice A mapping of dispute game addresses to whether or not they are blacklisted. - mapping(IDisputeGame => bool) public disputeGameBlacklist; + /// @custom:legacy + /// @custom:spacer disputeGameBlacklist + bytes32 private spacer_58_0_32; - /// @notice The game type that the OptimismPortal consults for output proposals. - GameType public respectedGameType; + /// @custom:legacy + /// @custom:spacer respectedGameType + GameType private spacer_59_0_4; - /// @notice The timestamp at which the respected game type was last updated. - uint64 public respectedGameTypeUpdatedAt; + /// @custom:legacy + /// @custom:spacer respectedGameTypeUpdatedAt + uint64 private spacer_59_4_8; /// @notice Mapping of withdrawal hashes to addresses that have submitted a proof for the /// withdrawal. Original OptimismPortal contract only allowed one proof to be submitted @@ -133,12 +115,19 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// @custom:legacy /// @custom:spacer _balance - /// @notice Spacer taking up the legacy `_balance` slot. uint256 private spacer_61_0_32; - /// @notice Emitted when a transaction is deposited from L1 to L2. - /// The parameters of this event are read by the rollup node and used to derive deposit - /// transactions on L2. + /// @notice Address of the AnchorStateRegistry contract. + IAnchorStateRegistry public anchorStateRegistry; + + /// @notice Address of the ETHLockbox contract. + IETHLockbox public ethLockbox; + + /// @notice Whether the OptimismPortal is using Super Roots or Output Roots. + bool public superRootsActive; + + /// @notice Emitted when a transaction is deposited from L1 to L2. The parameters of this event + /// are read by the rollup node and used to derive deposit transactions on L2. /// @param from Address that triggered the deposit transaction. /// @param to Address that the deposit transaction is directed to. /// @param version Version of this deposit transaction event. @@ -151,8 +140,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// @param to Address that the withdrawal transaction is directed to. event WithdrawalProven(bytes32 indexed withdrawalHash, address indexed from, address indexed to); - /// @notice Emitted when a withdrawal transaction is proven. Exists as a separate event to allow for backwards - /// compatibility for tooling that observes the `WithdrawalProven` event. + /// @notice Emitted when a withdrawal transaction is proven. Exists as a separate event to + /// allow for backwards compatibility for tooling that observes the WithdrawalProven + /// event. /// @param withdrawalHash Hash of the withdrawal transaction. /// @param proofSubmitter Address of the proof submitter. event WithdrawalProvenExtension1(bytes32 indexed withdrawalHash, address indexed proofSubmitter); @@ -162,74 +152,147 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// @param success Whether the withdrawal transaction was successful. event WithdrawalFinalized(bytes32 indexed withdrawalHash, bool success); - /// @notice Emitted when a dispute game is blacklisted by the Guardian. - /// @param disputeGame Address of the dispute game that was blacklisted. - event DisputeGameBlacklisted(IDisputeGame indexed disputeGame); + /// @notice Emitted when the total ETH balance is migrated to the ETHLockbox. + /// @param lockbox The address of the ETHLockbox contract. + /// @param ethBalance Amount of ETH migrated. + event ETHMigrated(address indexed lockbox, uint256 ethBalance); + + /// @notice Emitted when the ETHLockbox contract is updated. + /// @param oldLockbox The address of the old ETHLockbox contract. + /// @param newLockbox The address of the new ETHLockbox contract. + /// @param oldAnchorStateRegistry The address of the old AnchorStateRegistry contract. + /// @param newAnchorStateRegistry The address of the new AnchorStateRegistry contract. + event PortalMigrated( + IETHLockbox oldLockbox, + IETHLockbox newLockbox, + IAnchorStateRegistry oldAnchorStateRegistry, + IAnchorStateRegistry newAnchorStateRegistry + ); + + /// @notice Thrown when a withdrawal has already been finalized. + error OptimismPortal_AlreadyFinalized(); + + /// @notice Thrown when the target of a withdrawal is unsafe. + error OptimismPortal_BadTarget(); + + /// @notice Thrown when the calldata for a deposit is too large. + error OptimismPortal_CalldataTooLarge(); + + /// @notice Thrown when the portal is paused. + error OptimismPortal_CallPaused(); + + /// @notice Thrown when a gas estimation transaction is being executed. + error OptimismPortal_GasEstimation(); + + /// @notice Thrown when the gas limit for a deposit is too low. + error OptimismPortal_GasLimitTooLow(); + + /// @notice Thrown when the target of a withdrawal is not a proper dispute game. + error OptimismPortal_ImproperDisputeGame(); + + /// @notice Thrown when a withdrawal has not been proven against a valid dispute game. + error OptimismPortal_InvalidDisputeGame(); + + /// @notice Thrown when a withdrawal has not been proven against a valid merkle proof. + error OptimismPortal_InvalidMerkleProof(); + + /// @notice Thrown when a withdrawal has not been proven against a valid output root proof. + error OptimismPortal_InvalidOutputRootProof(); + + /// @notice Thrown when a withdrawal's timestamp is not greater than the dispute game's creation timestamp. + error OptimismPortal_InvalidProofTimestamp(); + + /// @notice Thrown when the root claim of a dispute game is invalid. + error OptimismPortal_InvalidRootClaim(); + + /// @notice Thrown when a withdrawal is being finalized by a reentrant call. + error OptimismPortal_NoReentrancy(); - /// @notice Emitted when the Guardian changes the respected game type in the portal. - /// @param newGameType The new respected game type. - /// @param updatedAt The timestamp at which the respected game type was updated. - event RespectedGameTypeSet(GameType indexed newGameType, Timestamp indexed updatedAt); + /// @notice Thrown when a withdrawal has not been proven for long enough. + error OptimismPortal_ProofNotOldEnough(); + + /// @notice Thrown when a withdrawal has not been proven. + error OptimismPortal_Unproven(); + + /// @notice Thrown when the caller is not authorized to call the function. + error OptimismPortal_Unauthorized(); + + /// @notice Thrown when the wrong proof method is used. + error OptimismPortal_WrongProofMethod(); + + /// @notice Thrown when a super root proof is invalid. + error OptimismPortal_InvalidSuperRootProof(); + + /// @notice Thrown when an output root index is invalid. + error OptimismPortal_InvalidOutputRootIndex(); + + /// @notice Thrown when an output root chain id is invalid. + error OptimismPortal_InvalidOutputRootChainId(); + + /// @notice Thrown when trying to migrate to the same AnchorStateRegistry. + error OptimismPortal_MigratingToSameRegistry(); /// @notice Reverts when paused. modifier whenNotPaused() { - if (paused()) revert CallPaused(); + if (paused()) revert OptimismPortal_CallPaused(); _; } /// @notice Semantic version. - /// @custom:semver 3.14.0 + /// @custom:semver 4.1.0 function version() public pure virtual returns (string memory) { - return "3.14.0"; + return "4.1.0"; } - /// @notice Constructs the OptimismPortal contract. - constructor(uint256 _proofMaturityDelaySeconds, uint256 _disputeGameFinalityDelaySeconds) { + /// @param _proofMaturityDelaySeconds The proof maturity delay in seconds. + constructor(uint256 _proofMaturityDelaySeconds) ReinitializableBase(2) { PROOF_MATURITY_DELAY_SECONDS = _proofMaturityDelaySeconds; - DISPUTE_GAME_FINALITY_DELAY_SECONDS = _disputeGameFinalityDelaySeconds; - _disableInitializers(); } /// @notice Initializer. - /// @param _disputeGameFactory Contract of the DisputeGameFactory. - /// @param _systemConfig Contract of the SystemConfig. - /// @param _superchainConfig Contract of the SuperchainConfig. + /// @param _systemConfig Address of the SystemConfig. + /// @param _superchainConfig Address of the SuperchainConfig. + /// @param _anchorStateRegistry Address of the AnchorStateRegistry. + /// @param _ethLockbox Contract of the ETHLockbox. function initialize( - IDisputeGameFactory _disputeGameFactory, ISystemConfig _systemConfig, ISuperchainConfig _superchainConfig, - GameType _initialRespectedGameType + IAnchorStateRegistry _anchorStateRegistry, + IETHLockbox _ethLockbox ) external - initializer + reinitializer(initVersion()) { - disputeGameFactory = _disputeGameFactory; systemConfig = _systemConfig; superchainConfig = _superchainConfig; + anchorStateRegistry = _anchorStateRegistry; + ethLockbox = _ethLockbox; - // Set the `l2Sender` slot, only if it is currently empty. This signals the first initialization of the - // contract. + // Set the l2Sender slot, only if it is currently empty. This signals the first + // initialization of the contract. if (l2Sender == address(0)) { l2Sender = Constants.DEFAULT_L2_SENDER; - - // Set the `respectedGameTypeUpdatedAt` timestamp, to ignore all games of the respected type prior - // to this operation. - respectedGameTypeUpdatedAt = uint64(block.timestamp); - - // Set the initial respected game type - respectedGameType = _initialRespectedGameType; } __ResourceMetering_init(); } - /// @notice Getter function for the address of the guardian. - /// Public getter is legacy and will be removed in the future. Use `SuperchainConfig.guardian()` instead. - /// @return Address of the guardian. - /// @custom:legacy - function guardian() public view returns (address) { - return superchainConfig.guardian(); + /// @notice Upgrades the OptimismPortal contract to have a reference to the AnchorStateRegistry. + /// @param _anchorStateRegistry AnchorStateRegistry contract. + /// @param _ethLockbox ETHLockbox contract. + function upgrade( + IAnchorStateRegistry _anchorStateRegistry, + IETHLockbox _ethLockbox + ) + external + reinitializer(initVersion()) + { + anchorStateRegistry = _anchorStateRegistry; + ethLockbox = _ethLockbox; + + // Migrate the whole ETH balance to the ETHLockbox. + _migrateLiquidity(); } /// @notice Getter for the current paused status. @@ -242,9 +305,33 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { return PROOF_MATURITY_DELAY_SECONDS; } + /// @notice Getter for the address of the DisputeGameFactory contract. + function disputeGameFactory() public view returns (IDisputeGameFactory) { + return anchorStateRegistry.disputeGameFactory(); + } + + /// @custom:legacy + /// @notice Getter function for the address of the guardian. + function guardian() public view returns (address) { + return superchainConfig.guardian(); + } + + /// @custom:legacy /// @notice Getter for the dispute game finality delay. - function disputeGameFinalityDelaySeconds() public view returns (uint256) { - return DISPUTE_GAME_FINALITY_DELAY_SECONDS; + function disputeGameFinalityDelaySeconds() external view returns (uint256) { + return anchorStateRegistry.disputeGameFinalityDelaySeconds(); + } + + /// @custom:legacy + /// @notice Getter for the respected game type. + function respectedGameType() external view returns (GameType) { + return anchorStateRegistry.respectedGameType(); + } + + /// @custom:legacy + /// @notice Getter for the timestamp at which the respected game type was updated. + function respectedGameTypeUpdatedAt() external view returns (uint64) { + return anchorStateRegistry.retirementTimestamp(); } /// @notice Computes the minimum gas limit for a deposit. @@ -267,28 +354,88 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { } /// @notice Accepts ETH value without triggering a deposit to L2. - /// This function mainly exists for the sake of the migration between the legacy - /// Optimism system and Bedrock. function donateETH() external payable { // Intentionally empty. } - /// @notice Getter for the resource config. - /// Used internally by the ResourceMetering contract. - /// The SystemConfig is the source of truth for the resource config. - /// @return config_ ResourceMetering ResourceConfig - function _resourceConfig() internal view override returns (ResourceMetering.ResourceConfig memory config_) { - IResourceMetering.ResourceConfig memory config = systemConfig.resourceConfig(); - assembly ("memory-safe") { - config_ := config + /// @notice Allows the owner of the ProxyAdmin to migrate the OptimismPortal to use a new + /// lockbox, point at a new AnchorStateRegistry, and start to use the Super Roots proof + /// method. Primarily used for OptimismPortal instances to join the interop set, but + /// can also be used to swap the proof method from Output Roots to Super Roots if the + /// provided lockbox is the same as the current one. + /// @dev It is possible to change lockboxes without migrating liquidity. This can cause one + /// of the OptimismPortal instances connected to the new lockbox to not be able to + /// unlock sufficient ETH to finalize withdrawals which would trigger reverts. To avoid + /// this issue, guarantee that this function is called atomically alongside the + /// ETHLockbox.migrateLiquidity() function within the same transaction. + /// @param _newLockbox The address of the new ETHLockbox contract. + function migrateToSuperRoots(IETHLockbox _newLockbox, IAnchorStateRegistry _newAnchorStateRegistry) external { + // Make sure the caller is the owner of the ProxyAdmin. + if (msg.sender != proxyAdminOwner()) revert OptimismPortal_Unauthorized(); + + // Chains can use this method to swap the proof method from Output Roots to Super Roots + // without joining the interop set. In this case, the old and new lockboxes will be the + // same. However, whether or not a chain is joining the interop set, all chains will need a + // new AnchorStateRegistry when migrating to Super Roots. We therefore check that the new + // AnchorStateRegistry is different than the old one to prevent this function from being + // accidentally misused. + if (anchorStateRegistry == _newAnchorStateRegistry) { + revert OptimismPortal_MigratingToSameRegistry(); } + + // Update the ETHLockbox. + IETHLockbox oldLockbox = ethLockbox; + ethLockbox = _newLockbox; + + // Update the AnchorStateRegistry. + IAnchorStateRegistry oldAnchorStateRegistry = anchorStateRegistry; + anchorStateRegistry = _newAnchorStateRegistry; + + // Set the proof method to Super Roots. We expect that migration will happen more than once + // for some chains (switching to single-chain Super Roots and then later joining the + // interop set) so we don't need to check that this is false. + superRootsActive = true; + + // Emit a PortalMigrated event. + emit PortalMigrated(oldLockbox, _newLockbox, oldAnchorStateRegistry, _newAnchorStateRegistry); } - /// @notice Proves a withdrawal transaction. + /// @notice Proves a withdrawal transaction using a Super Root proof. Only callable when the + /// OptimismPortal is using Super Roots (superRootsActive flag is true). + /// @param _tx Withdrawal transaction to finalize. + /// @param _disputeGameProxy Address of the dispute game to prove the withdrawal against. + /// @param _outputRootIndex Index of the target Output Root within the Super Root. + /// @param _superRootProof Inclusion proof of the Output Root within the Super Root. + /// @param _outputRootProof Inclusion proof of the L2ToL1MessagePasser storage root. + /// @param _withdrawalProof Inclusion proof of the withdrawal within the L2ToL1MessagePasser. + function proveWithdrawalTransaction( + Types.WithdrawalTransaction memory _tx, + IDisputeGame _disputeGameProxy, + uint256 _outputRootIndex, + Types.SuperRootProof calldata _superRootProof, + Types.OutputRootProof calldata _outputRootProof, + bytes[] calldata _withdrawalProof + ) + external + whenNotPaused + { + // Make sure that the OptimismPortal is using Super Roots. + if (!superRootsActive) { + revert OptimismPortal_WrongProofMethod(); + } + + // Prove the transaction. + _proveWithdrawalTransaction( + _tx, _disputeGameProxy, _outputRootIndex, _superRootProof, _outputRootProof, _withdrawalProof + ); + } + + /// @notice Proves a withdrawal transaction using an Output Root proof. Only callable when the + /// OptimismPortal is using Output Roots (superRootsActive flag is false). /// @param _tx Withdrawal transaction to finalize. /// @param _disputeGameIndex Index of the dispute game to prove the withdrawal against. - /// @param _outputRootProof Inclusion proof of the L2ToL1MessagePasser contract's storage root. - /// @param _withdrawalProof Inclusion proof of the withdrawal in L2ToL1MessagePasser contract. + /// @param _outputRootProof Inclusion proof of the L2ToL1MessagePasser storage root. + /// @param _withdrawalProof Inclusion proof of the withdrawal within the L2ToL1MessagePasser. function proveWithdrawalTransaction( Types.WithdrawalTransaction memory _tx, uint256 _disputeGameIndex, @@ -298,47 +445,102 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { external whenNotPaused { - // Prevent users from creating a deposit transaction where this address is the message - // sender on L2. Because this is checked here, we do not need to check again in - // `finalizeWithdrawalTransaction`. - if (_tx.target == address(this)) revert BadTarget(); + // Make sure that the OptimismPortal is using Output Roots. + if (superRootsActive) { + revert OptimismPortal_WrongProofMethod(); + } // Fetch the dispute game proxy from the `DisputeGameFactory` contract. - (GameType gameType,, IDisputeGame gameProxy) = disputeGameFactory.gameAtIndex(_disputeGameIndex); - Claim outputRoot = gameProxy.rootClaim(); - - // The game type of the dispute game must be the respected game type. - if (gameType.raw() != respectedGameType.raw()) revert InvalidGameType(); - - // The game type of the DisputeGame must have been the respected game type at creation. - // eip150-safe - try gameProxy.wasRespectedGameTypeWhenCreated() returns (bool wasRespected_) { - if (!wasRespected_) revert InvalidGameType(); - } catch { - revert LegacyGame(); + (,, IDisputeGame disputeGameProxy) = disputeGameFactory().gameAtIndex(_disputeGameIndex); + + // Create a dummy super root proof to pass into the internal function. Note that this is + // not a valid Super Root proof but it isn't used anywhere in the internal function when + // using Output Roots. + Types.SuperRootProof memory superRootProof; + + // Prove the transaction. + _proveWithdrawalTransaction(_tx, disputeGameProxy, 0, superRootProof, _outputRootProof, _withdrawalProof); + } + + /// @notice Internal function for proving a withdrawal transaction, used by both the Super Root + /// and Output Root proof functions. Will eventually be replaced with a single function + /// when the Output Root proof method is deprecated. + /// @param _tx Withdrawal transaction to prove. + /// @param _disputeGameProxy Address of the dispute game to prove the withdrawal against. + /// @param _outputRootIndex Index of the target Output Root within the Super Root. + /// @param _superRootProof Inclusion proof of the Output Root within the Super Root. + /// @param _outputRootProof Inclusion proof of the L2ToL1MessagePasser storage root. + /// @param _withdrawalProof Inclusion proof of the withdrawal within the L2ToL1MessagePasser. + function _proveWithdrawalTransaction( + Types.WithdrawalTransaction memory _tx, + IDisputeGame _disputeGameProxy, + uint256 _outputRootIndex, + Types.SuperRootProof memory _superRootProof, + Types.OutputRootProof memory _outputRootProof, + bytes[] memory _withdrawalProof + ) + internal + { + // Make sure that the target address is safe. + if (_isUnsafeTarget(_tx.target)) { + revert OptimismPortal_BadTarget(); } - // Game must have been created after the respected game type was updated. This check is a - // strict inequality because we want to prevent users from being able to prove or finalize - // withdrawals against games that were created in the same block that the retirement - // timestamp was set. If the retirement timestamp and game type are changed in the same - // block, such games could still be considered valid even if they used the old game type - // that we intended to invalidate. - require( - gameProxy.createdAt().raw() > respectedGameTypeUpdatedAt, - "OptimismPortal: dispute game created before respected game type was updated" - ); + // Game must be a Proper Game. + if (!anchorStateRegistry.isGameProper(_disputeGameProxy)) { + revert OptimismPortal_ImproperDisputeGame(); + } + + // Game must have been respected game type when created. + if (!anchorStateRegistry.isGameRespected(_disputeGameProxy)) { + revert OptimismPortal_InvalidDisputeGame(); + } + + // Game must not have resolved in favor of the Challenger (invalid root claim). + if (_disputeGameProxy.status() == GameStatus.CHALLENGER_WINS) { + revert OptimismPortal_InvalidDisputeGame(); + } - // Verify that the output root can be generated with the elements in the proof. - if (outputRoot.raw() != Hashing.hashOutputRootProof(_outputRootProof)) revert InvalidProof(); + // As a sanity check, we make sure that the current timestamp is not less than or equal to + // the dispute game's creation timestamp. Not strictly necessary but extra layer of + // safety against weird bugs. Note that this blocks withdrawals from being proven in the + // same block that a dispute game is created. + if (block.timestamp <= _disputeGameProxy.createdAt().raw()) { + revert OptimismPortal_InvalidProofTimestamp(); + } + + // Validate the provided Output Root and/or Super Root proof depending on proof method. + if (superRootsActive) { + // Verify that the super root can be generated with the elements in the proof. + if (_disputeGameProxy.rootClaim().raw() != Hashing.hashSuperRootProof(_superRootProof)) { + revert OptimismPortal_InvalidSuperRootProof(); + } + + // Check that the index exists in the super root proof. + if (_outputRootIndex >= _superRootProof.outputRoots.length) { + revert OptimismPortal_InvalidOutputRootIndex(); + } + + // Check that the output root has the correct chain id. + Types.OutputRootWithChainId memory outputRoot = _superRootProof.outputRoots[_outputRootIndex]; + if (outputRoot.chainId != systemConfig.l2ChainId()) { + revert OptimismPortal_InvalidOutputRootChainId(); + } + + // Verify that the output root can be generated with the elements in the proof. + if (outputRoot.root != Hashing.hashOutputRootProof(_outputRootProof)) { + revert OptimismPortal_InvalidOutputRootProof(); + } + } else { + // Verify that the output root can be generated with the elements in the proof. + if (_disputeGameProxy.rootClaim().raw() != Hashing.hashOutputRootProof(_outputRootProof)) { + revert OptimismPortal_InvalidOutputRootProof(); + } + } // Load the ProvenWithdrawal into memory, using the withdrawal hash as a unique identifier. bytes32 withdrawalHash = Hashing.hashWithdrawal(_tx); - // We do not allow for proving withdrawals against dispute games that have resolved against the favor - // of the root claim. - if (gameProxy.status() == GameStatus.CHALLENGER_WINS) revert InvalidDisputeGame(); - // Compute the storage slot of the withdrawal hash in the L2ToL1MessagePasser contract. // Refer to the Solidity documentation for more information on how storage layouts are // computed for mappings. @@ -360,21 +562,22 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { _proof: _withdrawalProof, _root: _outputRootProof.messagePasserStorageRoot }) == false - ) revert InvalidMerkleProof(); + ) { + revert OptimismPortal_InvalidMerkleProof(); + } - // Designate the withdrawalHash as proven by storing the `disputeGameProxy` & `timestamp` in the - // `provenWithdrawals` mapping. A `withdrawalHash` can only be proven once unless the dispute game it proved - // against resolves against the favor of the root claim. + // Designate the withdrawalHash as proven by storing the disputeGameProxy and timestamp in + // the provenWithdrawals mapping. A given user may re-prove a withdrawalHash multiple + // times, but each proof will reset the proof timer. provenWithdrawals[withdrawalHash][msg.sender] = - ProvenWithdrawal({ disputeGameProxy: gameProxy, timestamp: uint64(block.timestamp) }); - - // Emit a `WithdrawalProven` event. - emit WithdrawalProven(withdrawalHash, _tx.sender, _tx.target); - // Emit a `WithdrawalProvenExtension1` event. - emit WithdrawalProvenExtension1(withdrawalHash, msg.sender); + ProvenWithdrawal({ disputeGameProxy: _disputeGameProxy, timestamp: uint64(block.timestamp) }); // Add the proof submitter to the list of proof submitters for this withdrawal hash. proofSubmitters[withdrawalHash].push(msg.sender); + + // Emit a WithdrawalProven events. + emit WithdrawalProven(withdrawalHash, _tx.sender, _tx.target); + emit WithdrawalProvenExtension1(withdrawalHash, msg.sender); } /// @notice Finalizes a withdrawal transaction. @@ -396,9 +599,16 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { // Make sure that the l2Sender has not yet been set. The l2Sender is set to a value other // than the default value when a withdrawal transaction is being finalized. This check is // a defacto reentrancy guard. - if (l2Sender != Constants.DEFAULT_L2_SENDER) revert NonReentrant(); + if (l2Sender != Constants.DEFAULT_L2_SENDER) { + revert OptimismPortal_NoReentrancy(); + } + + // Make sure that the target address is safe. + if (_isUnsafeTarget(_tx.target)) { + revert OptimismPortal_BadTarget(); + } - // Compute the withdrawal hash. + // Grab the withdrawal. bytes32 withdrawalHash = Hashing.hashWithdrawal(_tx); // Check that the withdrawal can be finalized. @@ -407,6 +617,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { // Mark the withdrawal as finalized so it can't be replayed. finalizedWithdrawals[withdrawalHash] = true; + // Unlock the ETH from the ETHLockbox. + if (_tx.value > 0) ethLockbox.unlockETH(_tx.value); + // Set the l2Sender so contracts know who triggered this withdrawal on L2. l2Sender = _tx.sender; @@ -430,14 +643,61 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { // sub call to the target contract if the minimum gas limit specified by the user would not // be sufficient to execute the sub call. if (!success && tx.origin == Constants.ESTIMATION_ADDRESS) { - revert GasEstimation(); + revert OptimismPortal_GasEstimation(); + } + } + + /// @notice Checks that a withdrawal has been proven and is ready to be finalized. + /// @param _withdrawalHash Hash of the withdrawal. + /// @param _proofSubmitter Address of the proof submitter. + function checkWithdrawal(bytes32 _withdrawalHash, address _proofSubmitter) public view { + // Grab the withdrawal and dispute game proxy. + ProvenWithdrawal memory provenWithdrawal = provenWithdrawals[_withdrawalHash][_proofSubmitter]; + IDisputeGame disputeGameProxy = provenWithdrawal.disputeGameProxy; + + // Check that this withdrawal has not already been finalized, this is replay protection. + if (finalizedWithdrawals[_withdrawalHash]) { + revert OptimismPortal_AlreadyFinalized(); + } + + // A withdrawal can only be finalized if it has been proven. We know that a withdrawal has + // been proven at least once when its timestamp is non-zero. Unproven withdrawals will have + // a timestamp of zero. + if (provenWithdrawal.timestamp == 0) { + revert OptimismPortal_Unproven(); + } + + // As a sanity check, we make sure that the proven withdrawal's timestamp is greater than + // starting timestamp inside the Dispute Game. Not strictly necessary but extra layer of + // safety against weird bugs in the proving step. Note that this blocks withdrawals that + // are proven in the same block that a dispute game is created. + if (provenWithdrawal.timestamp <= disputeGameProxy.createdAt().raw()) { + revert OptimismPortal_InvalidProofTimestamp(); + } + + // A proven withdrawal must wait at least `PROOF_MATURITY_DELAY_SECONDS` before finalizing. + if (block.timestamp - provenWithdrawal.timestamp <= PROOF_MATURITY_DELAY_SECONDS) { + revert OptimismPortal_ProofNotOldEnough(); + } + + // Check that the root claim is valid. + if (!anchorStateRegistry.isGameClaimValid(disputeGameProxy)) { + revert OptimismPortal_InvalidRootClaim(); } } + /// @notice Migrates the total ETH balance to the ETHLockbox. + function migrateLiquidity() public { + if (msg.sender != proxyAdminOwner()) revert OptimismPortal_Unauthorized(); + _migrateLiquidity(); + } + /// @notice Accepts deposits of ETH and data, and emits a TransactionDeposited event for use in /// deriving deposit transactions. Note that if a deposit is made by a contract, its /// address will be aliased when retrieved using `tx.origin` or `msg.sender`. Consider /// using the CrossDomainMessenger contracts for a simpler developer experience. + /// @dev The `msg.value` is locked on the ETHLockbox and minted as ETH when the deposit + /// arrives on L2, while `_value` specifies how much ETH to send to the target. /// @param _to Target address on L2. /// @param _value ETH value to send to the recipient. /// @param _gasLimit Amount of L2 gas to purchase by burning gas on L1. @@ -454,19 +714,28 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { payable metered(_gasLimit) { + // Lock the ETH in the ETHLockbox. + if (msg.value > 0) ethLockbox.lockETH{ value: msg.value }(); + // Just to be safe, make sure that people specify address(0) as the target when doing // contract creations. - if (_isCreation && _to != address(0)) revert BadTarget(); + if (_isCreation && _to != address(0)) { + revert OptimismPortal_BadTarget(); + } // Prevent depositing transactions that have too small of a gas limit. Users should pay // more for more resource usage. - if (_gasLimit < minimumGasLimit(uint64(_data.length))) revert SmallGasLimit(); + if (_gasLimit < minimumGasLimit(uint64(_data.length))) { + revert OptimismPortal_GasLimitTooLow(); + } // Prevent the creation of deposit transactions that have too much calldata. This gives an // upper limit on the size of unsafe blocks over the p2p network. 120kb is chosen to ensure // that the transaction can fit into the p2p network policy of 128kb even though deposit // transactions are not gossipped over the p2p network. - if (_data.length > 120_000) revert LargeCalldata(); + if (_data.length > 120_000) { + revert OptimismPortal_CalldataTooLarge(); + } // Transform the from-address to its alias if the caller is a contract. address from = msg.sender; @@ -484,108 +753,34 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { emit TransactionDeposited(from, _to, DEPOSIT_VERSION, opaqueData); } - /// @notice Blacklists a dispute game. Should only be used in the event that a dispute game resolves incorrectly. - /// @param _disputeGame Dispute game to blacklist. - function blacklistDisputeGame(IDisputeGame _disputeGame) external { - if (msg.sender != guardian()) revert Unauthorized(); - disputeGameBlacklist[_disputeGame] = true; - emit DisputeGameBlacklisted(_disputeGame); + /// @notice External getter for the number of proof submitters for a withdrawal hash. + /// @param _withdrawalHash Hash of the withdrawal. + /// @return The number of proof submitters for the withdrawal hash. + function numProofSubmitters(bytes32 _withdrawalHash) external view returns (uint256) { + return proofSubmitters[_withdrawalHash].length; } - /// @notice Sets the respected game type. Changing this value can alter the security properties of the system, - /// depending on the new game's behavior. - /// @param _gameType The game type to consult for output proposals. - function setRespectedGameType(GameType _gameType) external { - if (msg.sender != guardian()) revert Unauthorized(); - // respectedGameTypeUpdatedAt is now no longer set by default. We want to avoid modifying - // this function's signature as that would result in changes to the DeputyGuardianModule. - // We use type(uint32).max as a temporary solution to allow us to update the - // respectedGameTypeUpdatedAt timestamp without modifying this function's signature. - if (_gameType.raw() == type(uint32).max) { - respectedGameTypeUpdatedAt = uint64(block.timestamp); - } else { - respectedGameType = _gameType; - } - emit RespectedGameTypeSet(respectedGameType, Timestamp.wrap(respectedGameTypeUpdatedAt)); + /// @notice Checks if a target address is unsafe. + function _isUnsafeTarget(address _target) internal view virtual returns (bool) { + // Prevent users from targetting an unsafe target address on a withdrawal transaction. + return _target == address(this) || _target == address(ethLockbox); } - /// @notice Checks if a withdrawal can be finalized. This function will revert if the withdrawal cannot be - /// finalized, and otherwise has no side-effects. - /// @param _withdrawalHash Hash of the withdrawal to check. - /// @param _proofSubmitter The submitter of the proof for the withdrawal hash - function checkWithdrawal(bytes32 _withdrawalHash, address _proofSubmitter) public view { - ProvenWithdrawal memory provenWithdrawal = provenWithdrawals[_withdrawalHash][_proofSubmitter]; - IDisputeGame disputeGameProxy = provenWithdrawal.disputeGameProxy; - - // The dispute game must not be blacklisted. - if (disputeGameBlacklist[disputeGameProxy]) revert Blacklisted(); - - // A withdrawal can only be finalized if it has been proven. We know that a withdrawal has - // been proven at least once when its timestamp is non-zero. Unproven withdrawals will have - // a timestamp of zero. - if (provenWithdrawal.timestamp == 0) revert Unproven(); - - // Grab the createdAt timestamp once. - uint64 createdAt = disputeGameProxy.createdAt().raw(); - - // As a sanity check, we make sure that the proven withdrawal's timestamp is greater than - // starting timestamp inside the Dispute Game. Not strictly necessary but extra layer of - // safety against weird bugs in the proving step. - require( - provenWithdrawal.timestamp > createdAt, - "OptimismPortal: withdrawal timestamp less than dispute game creation timestamp" - ); + /// @notice Migrates the total ETH balance to the ETHLockbox. + function _migrateLiquidity() internal { + uint256 ethBalance = address(this).balance; + ethLockbox.lockETH{ value: ethBalance }(); - // A proven withdrawal must wait at least `PROOF_MATURITY_DELAY_SECONDS` before finalizing. - require( - block.timestamp - provenWithdrawal.timestamp > PROOF_MATURITY_DELAY_SECONDS, - "OptimismPortal: proven withdrawal has not matured yet" - ); - - // A proven withdrawal must wait until the dispute game it was proven against has been - // resolved in favor of the root claim (the output proposal). This is to prevent users - // from finalizing withdrawals proven against non-finalized output roots. - if (disputeGameProxy.status() != GameStatus.DEFENDER_WINS) revert ProposalNotValidated(); - - // The game type of the dispute game must have been the respected game type at creation - // time. We check that the game type is the respected game type at proving time, but it's - // possible that the respected game type has since changed. Users can still use this game - // to finalize a withdrawal as long as it has not been otherwise invalidated. - // The game type of the DisputeGame must have been the respected game type at creation. - // eip150-safe - try disputeGameProxy.wasRespectedGameTypeWhenCreated() returns (bool wasRespected_) { - if (!wasRespected_) revert InvalidGameType(); - } catch { - revert LegacyGame(); - } - - // Game must have been created after the respected game type was updated. This check is a - // strict inequality because we want to prevent users from being able to prove or finalize - // withdrawals against games that were created in the same block that the retirement - // timestamp was set. If the retirement timestamp and game type are changed in the same - // block, such games could still be considered valid even if they used the old game type - // that we intended to invalidate. - require( - createdAt > respectedGameTypeUpdatedAt, - "OptimismPortal: dispute game created before respected game type was updated" - ); - - // Before a withdrawal can be finalized, the dispute game it was proven against must have been - // resolved for at least `DISPUTE_GAME_FINALITY_DELAY_SECONDS`. This is to allow for manual - // intervention in the event that a dispute game is resolved incorrectly. - require( - block.timestamp - disputeGameProxy.resolvedAt().raw() > DISPUTE_GAME_FINALITY_DELAY_SECONDS, - "OptimismPortal: output proposal in air-gap" - ); - - // Check that this withdrawal has not already been finalized, this is replay protection. - if (finalizedWithdrawals[_withdrawalHash]) revert AlreadyFinalized(); + emit ETHMigrated(address(ethLockbox), ethBalance); } - /// @notice External getter for the number of proof submitters for a withdrawal hash. - /// @param _withdrawalHash Hash of the withdrawal. - /// @return The number of proof submitters for the withdrawal hash. - function numProofSubmitters(bytes32 _withdrawalHash) external view returns (uint256) { - return proofSubmitters[_withdrawalHash].length; + /// @notice Getter for the resource config. Used internally by the ResourceMetering contract. + /// The SystemConfig is the source of truth for the resource config. + /// @return config_ ResourceMetering ResourceConfig + function _resourceConfig() internal view override returns (ResourceMetering.ResourceConfig memory config_) { + IResourceMetering.ResourceConfig memory config = systemConfig.resourceConfig(); + assembly ("memory-safe") { + config_ := config + } } } diff --git a/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol b/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol deleted file mode 100644 index 7920a4931b..0000000000 --- a/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.15; - -// Contracts -import { OptimismPortal2 } from "src/L1/OptimismPortal2.sol"; - -// Libraries -import { Predeploys } from "src/libraries/Predeploys.sol"; -import { Constants } from "src/libraries/Constants.sol"; -import { Unauthorized } from "src/libraries/PortalErrors.sol"; - -// Interfaces -import { IL1BlockInterop, ConfigType } from "interfaces/L2/IL1BlockInterop.sol"; - -/// @custom:proxied true -/// @title OptimismPortalInterop -/// @notice The OptimismPortal is a low-level contract responsible for passing messages between L1 -/// and L2. Messages sent directly to the OptimismPortal have no form of replayability. -/// Users are encouraged to use the L1CrossDomainMessenger for a higher-level interface. -contract OptimismPortalInterop is OptimismPortal2 { - constructor( - uint256 _proofMaturityDelaySeconds, - uint256 _disputeGameFinalityDelaySeconds - ) - OptimismPortal2(_proofMaturityDelaySeconds, _disputeGameFinalityDelaySeconds) - { } - - /// @custom:semver +interop.3 - function version() public pure override returns (string memory) { - return string.concat(super.version(), "+interop.3"); - } - - /// @notice Sets static configuration options for the L2 system. - /// @param _type Type of configuration to set. - /// @param _value Encoded value of the configuration. - function setConfig(ConfigType _type, bytes memory _value) external { - if (msg.sender != address(systemConfig)) revert Unauthorized(); - - // Set L2 deposit gas as used without paying burning gas. Ensures that deposits cannot use too much L2 gas. - // This value must be large enough to cover the cost of calling `L1Block.setConfig`. - useGas(SYSTEM_DEPOSIT_GAS_LIMIT); - - // Emit the special deposit transaction directly that sets the config in the L1Block predeploy contract. - emit TransactionDeposited( - Constants.DEPOSITOR_ACCOUNT, - Predeploys.L1_BLOCK_ATTRIBUTES, - DEPOSIT_VERSION, - abi.encodePacked( - uint256(0), // mint - uint256(0), // value - uint64(SYSTEM_DEPOSIT_GAS_LIMIT), // gasLimit - false, // isCreation, - abi.encodeCall(IL1BlockInterop.setConfig, (_type, _value)) - ) - ); - } -} diff --git a/packages/contracts-bedrock/src/L1/ProxyAdminOwnedBase.sol b/packages/contracts-bedrock/src/L1/ProxyAdminOwnedBase.sol new file mode 100644 index 0000000000..350b91cf10 --- /dev/null +++ b/packages/contracts-bedrock/src/L1/ProxyAdminOwnedBase.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; +import { Storage } from "src/libraries/Storage.sol"; +import { Constants } from "src/libraries/Constants.sol"; + +/// @notice Base contract for ProxyAdmin-owned contracts. It's main goal is to expose the ProxyAdmin owner address on +/// a function and also to check if the current contract and a given proxy have the same ProxyAdmin owner. +abstract contract ProxyAdminOwnedBase { + /// @notice Getter for the owner of the ProxyAdmin. + /// The ProxyAdmin is the owner of the current proxy contract. + function proxyAdminOwner() public view returns (address) { + // Get the proxy admin address reading for the reserved slot it has on the Proxy contract. + IProxyAdmin proxyAdmin = IProxyAdmin(Storage.getAddress(Constants.PROXY_OWNER_ADDRESS)); + // Return the owner of the proxy admin. + return proxyAdmin.owner(); + } + + /// @notice Checks if the ProxyAdmin owner of the current contract is the same as the ProxyAdmin owner of the given + /// proxy. + /// @param _proxy The address of the proxy to check. + function _sameProxyAdminOwner(address _proxy) internal view returns (bool) { + return proxyAdminOwner() == ProxyAdminOwnedBase(_proxy).proxyAdminOwner(); + } +} diff --git a/packages/contracts-bedrock/src/L1/StandardValidator.sol b/packages/contracts-bedrock/src/L1/StandardValidator.sol index 4ff5dc75e9..d36bafb550 100644 --- a/packages/contracts-bedrock/src/L1/StandardValidator.sol +++ b/packages/contracts-bedrock/src/L1/StandardValidator.sol @@ -170,6 +170,7 @@ contract StandardValidatorBase { ) internal view + virtual returns (string memory) { ISemver _semver = ISemver(address(_sysCfg)); @@ -668,3 +669,120 @@ contract StandardValidatorV200 is StandardValidatorBase { return "1.1.4"; } } + +contract StandardValidatorV300 is StandardValidatorBase { + struct InputV300 { + IProxyAdmin proxyAdmin; + ISystemConfig sysCfg; + bytes32 absolutePrestate; + uint256 l2ChainID; + } + + constructor( + ImplementationsBase memory _implementations, + ISuperchainConfig _superchainConfig, + address _l1PAOMultisig, + address _mips, + address _challenger + ) + StandardValidatorBase(_implementations, _superchainConfig, _l1PAOMultisig, _mips, _challenger) + { } + + function validate(InputV300 memory _input, bool _allowFailure) public view returns (string memory) { + string memory _errors = ""; + + _errors = super.validate(_errors, _input.sysCfg, _input.proxyAdmin, _input.absolutePrestate, _input.l2ChainID); + + if (bytes(_errors).length > 0 && !_allowFailure) { + revert(string.concat("StandardValidatorV300: ", _errors)); + } + + return _errors; + } + + function assertValidSystemConfig( + string memory _errors, + ISystemConfig _sysCfg, + IProxyAdmin _admin + ) + internal + view + override + returns (string memory) + { + _errors = super.assertValidSystemConfig(_errors, _sysCfg, _admin); + _errors = internalRequire(_sysCfg.operatorFeeScalar() == 0, "SYSCON-110", _errors); + _errors = internalRequire(_sysCfg.operatorFeeConstant() == 0, "SYSCON-120", _errors); + return _errors; + } + + function assertValidAnchorStateRegistry( + string memory _errors, + IDisputeGameFactory _dgf, + IAnchorStateRegistry _asr, + IProxyAdmin _admin, + GameType _gameType, + string memory _errorPrefix + ) + internal + view + override + returns (string memory) + { + _errors = super.assertValidAnchorStateRegistry(_errors, _dgf, _asr, _admin, _gameType, _errorPrefix); + _errors = internalRequire( + _admin.getProxyImplementation(address(_asr)) == anchorStateRegistryImpl, + string.concat(_errorPrefix, "-ANCHORP-20"), + _errors + ); + return _errors; + } + + function systemConfigVersion() public pure override returns (string memory) { + return "2.5.0"; + } + + function optimismPortalVersion() public pure override returns (string memory) { + return "3.14.0"; + } + + function l1CrossDomainMessengerVersion() public pure override returns (string memory) { + return "2.6.0"; + } + + function l1ERC721BridgeVersion() public pure override returns (string memory) { + return "2.4.0"; + } + + function l1StandardBridgeVersion() public pure override returns (string memory) { + return "2.3.0"; + } + + function mipsVersion() public pure override returns (string memory) { + return "1.0.0"; + } + + function optimismMintableERC20FactoryVersion() public pure override returns (string memory) { + return "1.10.1"; + } + + function disputeGameFactoryVersion() public pure override returns (string memory) { + return "1.0.1"; + } + + function anchorStateRegistryVersion() public pure override returns (string memory) { + return "2.2.2"; + } + + function delayedWETHVersion() public pure override returns (string memory) { + return "1.3.0"; + } + + function permissionedDisputeGameVersion() public pure override returns (string memory) { + return "1.4.1"; + } + + function preimageOracleVersion() public pure override returns (string memory) { + return "1.1.4"; + } +} diff --git a/packages/contracts-bedrock/src/L1/SystemConfig.sol b/packages/contracts-bedrock/src/L1/SystemConfig.sol index e767bc64a5..c2ec4b44db 100644 --- a/packages/contracts-bedrock/src/L1/SystemConfig.sol +++ b/packages/contracts-bedrock/src/L1/SystemConfig.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.15; // Contracts import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { ReinitializableBase } from "src/universal/ReinitializableBase.sol"; // Libraries import { Storage } from "src/libraries/Storage.sol"; @@ -10,13 +11,14 @@ import { Storage } from "src/libraries/Storage.sol"; // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; +import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; /// @custom:proxied true /// @title SystemConfig /// @notice The SystemConfig contract is used to manage configuration of an Optimism network. /// All configuration is stored on L1 and picked up by L2 as part of the derviation of /// the L2 chain. -contract SystemConfig is OwnableUpgradeable, ISemver { +contract SystemConfig is OwnableUpgradeable, ReinitializableBase, ISemver { /// @notice Enum representing different types of updates. /// @custom:value BATCHER Represents an update to the batcher hash. /// @custom:value FEE_SCALARS Represents an update to l1 data fee scalars. @@ -39,7 +41,6 @@ contract SystemConfig is OwnableUpgradeable, ISemver { address l1CrossDomainMessenger; address l1ERC721Bridge; address l1StandardBridge; - address disputeGameFactory; address optimismPortal; address optimismMintableERC20Factory; } @@ -80,10 +81,6 @@ contract SystemConfig is OwnableUpgradeable, ISemver { /// @notice Storage slot for block at which the op-node can start searching for logs from. bytes32 public constant START_BLOCK_SLOT = bytes32(uint256(keccak256("systemconfig.startBlock")) - 1); - /// @notice Storage slot for the DisputeGameFactory address. - bytes32 public constant DISPUTE_GAME_FACTORY_SLOT = - bytes32(uint256(keccak256("systemconfig.disputegamefactory")) - 1); - /// @notice The maximum gas limit that can be set for L2 blocks. This limit is used to enforce that the blocks /// on L2 are not too large to process and prove. Over time, this value can be increased as various /// optimizations and improvements are made to the system at large. @@ -129,6 +126,9 @@ contract SystemConfig is OwnableUpgradeable, ISemver { /// @notice The operator fee constant. uint64 public operatorFeeConstant; + /// @notice The L2 chain ID that this SystemConfig configures. + uint256 public l2ChainId; + /// @notice Emitted when configuration is updated. /// @param version SystemConfig version. /// @param updateType Type of update. @@ -136,15 +136,15 @@ contract SystemConfig is OwnableUpgradeable, ISemver { event ConfigUpdate(uint256 indexed version, UpdateType indexed updateType, bytes data); /// @notice Semantic version. - /// @custom:semver 2.5.0 + /// @custom:semver 3.0.0 function version() public pure virtual returns (string memory) { - return "2.5.0"; + return "3.0.0"; } /// @notice Constructs the SystemConfig contract. /// @dev START_BLOCK_SLOT is set to type(uint256).max here so that it will be a dead value /// in the singleton. - constructor() { + constructor() ReinitializableBase(2) { Storage.setUint(START_BLOCK_SLOT, type(uint256).max); _disableInitializers(); } @@ -161,6 +161,7 @@ contract SystemConfig is OwnableUpgradeable, ISemver { /// @param _batchInbox Batch inbox address. An identifier for the op-node to find /// canonical data. /// @param _addresses Set of L1 contract addresses. These should be the proxies. + /// @param _l2ChainId The L2 chain ID that this SystemConfig configures. function initialize( address _owner, uint32 _basefeeScalar, @@ -170,10 +171,11 @@ contract SystemConfig is OwnableUpgradeable, ISemver { address _unsafeBlockSigner, IResourceMetering.ResourceConfig memory _config, address _batchInbox, - SystemConfig.Addresses memory _addresses + SystemConfig.Addresses memory _addresses, + uint256 _l2ChainId ) public - initializer + reinitializer(initVersion()) { __Ownable_init(); transferOwnership(_owner); @@ -188,13 +190,25 @@ contract SystemConfig is OwnableUpgradeable, ISemver { Storage.setAddress(L1_CROSS_DOMAIN_MESSENGER_SLOT, _addresses.l1CrossDomainMessenger); Storage.setAddress(L1_ERC_721_BRIDGE_SLOT, _addresses.l1ERC721Bridge); Storage.setAddress(L1_STANDARD_BRIDGE_SLOT, _addresses.l1StandardBridge); - Storage.setAddress(DISPUTE_GAME_FACTORY_SLOT, _addresses.disputeGameFactory); Storage.setAddress(OPTIMISM_PORTAL_SLOT, _addresses.optimismPortal); Storage.setAddress(OPTIMISM_MINTABLE_ERC20_FACTORY_SLOT, _addresses.optimismMintableERC20Factory); _setStartBlock(); _setResourceConfig(_config); + + l2ChainId = _l2ChainId; + } + + /// @notice Upgrades the SystemConfig by setting the L2 chain ID variable. + /// @param _l2ChainId The L2 chain ID that this SystemConfig configures. + function upgrade(uint256 _l2ChainId) external reinitializer(initVersion()) { + // Set the L2 chain ID. + l2ChainId = _l2ChainId; + + // Clear out the old dispute game factory address, it's derived now. + bytes32 disputeGameFactorySlot = bytes32(uint256(keccak256("systemconfig.disputegamefactory")) - 1); + Storage.setAddress(disputeGameFactorySlot, address(0)); } /// @notice Returns the minimum L2 gas limit that can be safely set for the system to @@ -240,7 +254,8 @@ contract SystemConfig is OwnableUpgradeable, ISemver { /// @notice Getter for the DisputeGameFactory address. function disputeGameFactory() public view returns (address addr_) { - addr_ = Storage.getAddress(DISPUTE_GAME_FACTORY_SLOT); + IOptimismPortal2 portal = IOptimismPortal2(payable(Storage.getAddress(OPTIMISM_PORTAL_SLOT))); + addr_ = address(portal.disputeGameFactory()); } /// @notice Getter for the OptimismPortal address. @@ -259,7 +274,6 @@ contract SystemConfig is OwnableUpgradeable, ISemver { l1CrossDomainMessenger: l1CrossDomainMessenger(), l1ERC721Bridge: l1ERC721Bridge(), l1StandardBridge: l1StandardBridge(), - disputeGameFactory: disputeGameFactory(), optimismPortal: optimismPortal(), optimismMintableERC20Factory: optimismMintableERC20Factory() }); diff --git a/packages/contracts-bedrock/src/L1/SystemConfigInterop.sol b/packages/contracts-bedrock/src/L1/SystemConfigInterop.sol deleted file mode 100644 index 1d7bd87945..0000000000 --- a/packages/contracts-bedrock/src/L1/SystemConfigInterop.sol +++ /dev/null @@ -1,95 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.15; - -// Contracts -import { SystemConfig } from "src/L1/SystemConfig.sol"; - -// Libraries -import { StaticConfig } from "src/libraries/StaticConfig.sol"; -import { Storage } from "src/libraries/Storage.sol"; - -// Interfaces -import { IOptimismPortalInterop as IOptimismPortal } from "interfaces/L1/IOptimismPortalInterop.sol"; -import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; -import { ConfigType } from "interfaces/L2/IL1BlockInterop.sol"; - -/// @custom:proxied true -/// @title SystemConfigInterop -/// @notice The SystemConfig contract is used to manage configuration of an Optimism network. -/// All configuration is stored on L1 and picked up by L2 as part of the derviation of -/// the L2 chain. -contract SystemConfigInterop is SystemConfig { - /// @notice Storage slot where the dependency manager address is stored - /// @dev Equal to bytes32(uint256(keccak256("systemconfig.dependencymanager")) - 1) - bytes32 internal constant DEPENDENCY_MANAGER_SLOT = - 0x1708e077affb93e89be2665fb0fb72581be66f84dc00d25fed755ae911905b1c; - - /// @notice Initializer. - /// @param _owner Initial owner of the contract. - /// @param _basefeeScalar Initial basefee scalar value. - /// @param _blobbasefeeScalar Initial blobbasefee scalar value. - /// @param _batcherHash Initial batcher hash. - /// @param _gasLimit Initial gas limit. - /// @param _unsafeBlockSigner Initial unsafe block signer address. - /// @param _config Initial ResourceConfig. - /// @param _batchInbox Batch inbox address. An identifier for the op-node to find - /// canonical data. - /// @param _addresses Set of L1 contract addresses. These should be the proxies. - /// @param _dependencyManager The addressed allowed to add/remove from the dependency set - function initialize( - address _owner, - uint32 _basefeeScalar, - uint32 _blobbasefeeScalar, - bytes32 _batcherHash, - uint64 _gasLimit, - address _unsafeBlockSigner, - IResourceMetering.ResourceConfig memory _config, - address _batchInbox, - SystemConfig.Addresses memory _addresses, - address _dependencyManager - ) - external - { - // This method has an initializer modifier, and will revert if already initialized. - initialize({ - _owner: _owner, - _basefeeScalar: _basefeeScalar, - _blobbasefeeScalar: _blobbasefeeScalar, - _batcherHash: _batcherHash, - _gasLimit: _gasLimit, - _unsafeBlockSigner: _unsafeBlockSigner, - _config: _config, - _batchInbox: _batchInbox, - _addresses: _addresses - }); - Storage.setAddress(DEPENDENCY_MANAGER_SLOT, _dependencyManager); - } - - /// @custom:semver +interop.1 - function version() public pure override returns (string memory) { - return string.concat(super.version(), "+interop.1"); - } - - /// @notice Adds a chain to the interop dependency set. Can only be called by the dependency manager. - /// @param _chainId Chain ID of chain to add. - function addDependency(uint256 _chainId) external { - require(msg.sender == dependencyManager(), "SystemConfig: caller is not the dependency manager"); - IOptimismPortal(payable(optimismPortal())).setConfig( - ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(_chainId) - ); - } - - /// @notice Removes a chain from the interop dependency set. Can only be called by the dependency manager - /// @param _chainId Chain ID of the chain to remove. - function removeDependency(uint256 _chainId) external { - require(msg.sender == dependencyManager(), "SystemConfig: caller is not the dependency manager"); - IOptimismPortal(payable(optimismPortal())).setConfig( - ConfigType.REMOVE_DEPENDENCY, StaticConfig.encodeRemoveDependency(_chainId) - ); - } - - /// @notice getter for the dependency manager address - function dependencyManager() public view returns (address) { - return Storage.getAddress(DEPENDENCY_MANAGER_SLOT); - } -} diff --git a/packages/contracts-bedrock/src/L2/CrossL2Inbox.sol b/packages/contracts-bedrock/src/L2/CrossL2Inbox.sol index 59f082cbe0..3c2fe0e568 100644 --- a/packages/contracts-bedrock/src/L2/CrossL2Inbox.sol +++ b/packages/contracts-bedrock/src/L2/CrossL2Inbox.sol @@ -1,27 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -// Libraries -import { Predeploys } from "src/libraries/Predeploys.sol"; -import { TransientContext, TransientReentrancyAware } from "src/libraries/TransientContext.sol"; - // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; -import { IL1BlockInterop } from "interfaces/L2/IL1BlockInterop.sol"; - -/// @notice Thrown when the caller is not DEPOSITOR_ACCOUNT when calling `setInteropStart()` -error NotDepositor(); - -/// @notice Thrown when attempting to set interop start when it's already set. -error InteropStartAlreadySet(); - -/// @notice Thrown when a non-written transient storage slot is attempted to be read from. -error NotEntered(); - -/// @notice Thrown when trying to execute a cross chain message on a deposit transaction. -error NoExecutingDeposits(); /// @notice The struct for a pointer to a message payload in a remote (or local) chain. +/// @custom:field origin The origin address of the message. +/// @custom:field blockNumber The block number of the message. +/// @custom:field logIndex The log index of the message. +/// @custom:field timestamp The timestamp of the message. +/// @custom:field chainId The origin chain ID of the message. struct Identifier { address origin; uint256 blockNumber; @@ -35,117 +23,111 @@ struct Identifier { /// @title CrossL2Inbox /// @notice The CrossL2Inbox is responsible for executing a cross chain message on the destination /// chain. It is permissionless to execute a cross chain message on behalf of any user. -contract CrossL2Inbox is ISemver, TransientReentrancyAware { - /// @notice Storage slot that the interop start timestamp is stored at. - /// Equal to bytes32(uint256(keccak256("crossl2inbox.interopstart")) - 1) - bytes32 internal constant INTEROP_START_SLOT = 0x5c769ee0ee8887661922049dc52480bb60322d765161507707dd9b190af5c149; - - /// @notice Transient storage slot that the origin for an Identifier is stored at. - /// Equal to bytes32(uint256(keccak256("crossl2inbox.identifier.origin")) - 1) - bytes32 internal constant ORIGIN_SLOT = 0xd2b7c5071ec59eb3ff0017d703a8ea513a7d0da4779b0dbefe845808c300c815; - - /// @notice Transient storage slot that the blockNumber for an Identifier is stored at. - /// Equal to bytes32(uint256(keccak256("crossl2inbox.identifier.blocknumber")) - 1) - bytes32 internal constant BLOCK_NUMBER_SLOT = 0x5a1da0738b7fdc60047c07bb519beb02aa32a8619de57e6258da1f1c2e020ccc; - - /// @notice Transient storage slot that the logIndex for an Identifier is stored at. - /// Equal to bytes32(uint256(keccak256("crossl2inbox.identifier.logindex")) - 1) - bytes32 internal constant LOG_INDEX_SLOT = 0xab8acc221aecea88a685fabca5b88bf3823b05f335b7b9f721ca7fe3ffb2c30d; +/// @dev Processes cross-chain messages that are pre-declared in EIP-2930 access lists. Each message +/// requires three specific access-list entries to be valid. It will verify that the storage +/// slot containing the message checksum is "warm" (pre-accessed), which fails if not included +/// in the tx's access list. Nodes pre-check message validity before execution. The checksum +/// combines the message's `Identifier` and `msgHash` with type-3 bit masking. +contract CrossL2Inbox is ISemver { + /// @notice Thrown when trying to execute a cross chain message on a deposit transaction. + error NoExecutingDeposits(); + + /// @notice Thrown when trying to validate a cross chain message with a checksum + /// that is invalid or was not provided in the transaction's access list to set the slot + /// as warm. + error NotInAccessList(); + + /// @notice Thrown when trying to validate a cross chain message with a block number + /// that is greater than 2^64. + error BlockNumberTooHigh(); + + /// @notice Thrown when trying to validate a cross chain message with a timestamp + /// that is greater than 2^64. + error TimestampTooHigh(); + + /// @notice Thrown when trying to validate a cross chain message with a log index + /// that is greater than 2^32. + error LogIndexTooHigh(); - /// @notice Transient storage slot that the timestamp for an Identifier is stored at. - /// Equal to bytes32(uint256(keccak256("crossl2inbox.identifier.timestamp")) - 1) - bytes32 internal constant TIMESTAMP_SLOT = 0x2e148a404a50bb94820b576997fd6450117132387be615e460fa8c5e11777e02; + /// @notice Semantic version. + /// @custom:semver 1.0.0-beta.14 + string public constant version = "1.0.0-beta.14"; - /// @notice Transient storage slot that the chainId for an Identifier is stored at. - /// Equal to bytes32(uint256(keccak256("crossl2inbox.identifier.chainid")) - 1) - bytes32 internal constant CHAINID_SLOT = 0x6e0446e8b5098b8c8193f964f1b567ec3a2bdaeba33d36acb85c1f1d3f92d313; + /// @notice The mask for the most significant bits of the checksum. + /// @dev Used to set the most significant byte to zero. + bytes32 internal constant _MSB_MASK = bytes32(~uint256(0xff << 248)); - /// @notice The address that represents the system caller responsible for L1 attributes - /// transactions. - address internal constant DEPOSITOR_ACCOUNT = 0xDeaDDEaDDeAdDeAdDEAdDEaddeAddEAdDEAd0001; + /// @notice Mask used to set the first byte of the bare checksum to 3 (0x03). + bytes32 internal constant _TYPE_3_MASK = bytes32(uint256(0x03 << 248)); - /// @notice Semantic version. - /// @custom:semver 1.0.0-beta.12 - string public constant version = "1.0.0-beta.12"; + /// @notice The threshold to use to know whether the slot is warm or not. + uint256 internal constant _WARM_READ_THRESHOLD = 1000; /// @notice Emitted when a cross chain message is being executed. /// @param msgHash Hash of message payload being executed. /// @param id Encoded Identifier of the message. event ExecutingMessage(bytes32 indexed msgHash, Identifier id); - /// @notice Sets the Interop Start Timestamp for this chain. Can only be performed once and when the caller is the - /// DEPOSITOR_ACCOUNT. - function setInteropStart() external { - // Check that caller is the DEPOSITOR_ACCOUNT - if (msg.sender != DEPOSITOR_ACCOUNT) revert NotDepositor(); - - // Check that it has not been set already - if (interopStart() != 0) revert InteropStartAlreadySet(); - - // Set Interop Start to block.timestamp - assembly { - sstore(INTEROP_START_SLOT, timestamp()) - } - } + /// @notice Validates a cross chain message on the destination chain and emits an ExecutingMessage + /// event. This function is useful for applications that understand the schema of the + /// message payload and want to process it in a custom way. + /// @dev Makes sure the checksum's slot is warm to ensure the tx included it in the access list. + /// @dev `Identifier.blockNumber` and `Identifier.timestamp` must be less than 2^64, whereas + /// `Identifier.logIndex` must be less than 2^32 to properly fit into the checksum. + /// @param _id Identifier of the message. + /// @param _msgHash Hash of the message payload to call target with. + function validateMessage(Identifier calldata _id, bytes32 _msgHash) external { + bytes32 checksum = _calculateChecksum(_id, _msgHash); + (bool isWarm,) = _isWarm(checksum); + if (!isWarm) revert NotInAccessList(); - /// @notice Returns the interop start timestamp. - /// @return interopStart_ interop start timestamp. - function interopStart() public view returns (uint256 interopStart_) { - assembly { - interopStart_ := sload(INTEROP_START_SLOT) - } + emit ExecutingMessage(_msgHash, _id); } - /// @notice Returns the origin address of the Identifier. If not entered, reverts. - /// @return Origin address of the Identifier. - function origin() external view notEntered returns (address) { - return address(uint160(TransientContext.get(ORIGIN_SLOT))); - } + /// @notice Calculates a custom checksum for a cross chain message `Identifier` and `msgHash`. + /// @param _id The identifier of the message. + /// @param _msgHash The hash of the message. + /// @return checksum_ The checksum of the message. + function _calculateChecksum(Identifier memory _id, bytes32 _msgHash) internal pure returns (bytes32 checksum_) { + if (_id.blockNumber > type(uint64).max) revert BlockNumberTooHigh(); + if (_id.logIndex > type(uint32).max) revert LogIndexTooHigh(); + if (_id.timestamp > type(uint64).max) revert TimestampTooHigh(); - /// @notice Returns the block number of the Identifier. If not entered, reverts. - /// @return Block number of the Identifier. - function blockNumber() external view notEntered returns (uint256) { - return TransientContext.get(BLOCK_NUMBER_SLOT); - } + // Hash the origin address and message hash together + bytes32 logHash = keccak256(abi.encodePacked(_id.origin, _msgHash)); - /// @notice Returns the log index of the Identifier. If not entered, reverts. - /// @return Log index of the Identifier. - function logIndex() external view notEntered returns (uint256) { - return TransientContext.get(LOG_INDEX_SLOT); - } + // Downsize the identifier fields to match the needed type for the custom checksum calculation. + uint64 blockNumber = uint64(_id.blockNumber); + uint64 timestamp = uint64(_id.timestamp); + uint32 logIndex = uint32(_id.logIndex); - /// @notice Returns the timestamp of the Identifier. If not entered, reverts. - /// @return Timestamp of the Identifier. - function timestamp() external view notEntered returns (uint256) { - return TransientContext.get(TIMESTAMP_SLOT); - } + // Pack identifier fields with a left zero padding (uint96(0)) + bytes32 idPacked = bytes32(abi.encodePacked(uint96(0), blockNumber, timestamp, logIndex)); - /// @notice Returns the chain ID of the Identifier. If not entered, reverts. - /// @return _chainId The chain ID of the Identifier. - function chainId() external view notEntered returns (uint256) { - return TransientContext.get(CHAINID_SLOT); - } + // Hash the logHash with the packed identifier data + bytes32 idLogHash = keccak256(abi.encodePacked(logHash, idPacked)); - /// @notice Validates a cross chain message on the destination chain - /// and emits an ExecutingMessage event. This function is useful - /// for applications that understand the schema of the _message payload and want to - /// process it in a custom way. - /// @param _id Identifier of the message. - /// @param _msgHash Hash of the message payload to call target with. - function validateMessage(Identifier calldata _id, bytes32 _msgHash) external { - // We need to know if this is being called on a depositTx - if (IL1BlockInterop(Predeploys.L1_BLOCK_ATTRIBUTES).isDeposit()) revert NoExecutingDeposits(); + // Create the final hash by combining idLogHash with chainId + bytes32 bareChecksum = keccak256(abi.encodePacked(idLogHash, _id.chainId)); - emit ExecutingMessage(_msgHash, _id); + // Apply bit masking to create the final checksum + checksum_ = (bareChecksum & _MSB_MASK) | _TYPE_3_MASK; } - /// @notice Stores the Identifier in transient storage. - /// @param _id Identifier to store. - function _storeIdentifier(Identifier calldata _id) internal { - TransientContext.set(ORIGIN_SLOT, uint160(_id.origin)); - TransientContext.set(BLOCK_NUMBER_SLOT, _id.blockNumber); - TransientContext.set(LOG_INDEX_SLOT, _id.logIndex); - TransientContext.set(TIMESTAMP_SLOT, _id.timestamp); - TransientContext.set(CHAINID_SLOT, _id.chainId); + /// @notice Checks if a slot is warm by measuring the gas cost of loading the slot. + /// @dev Stores and returns the slot value so that the compiler doesn't optimize out the + /// `sload`, this adds cost to the read + /// @param _slot The slot to check. + /// @return isWarm_ Whether the slot is warm. + /// @return value_ The slot value. + function _isWarm(bytes32 _slot) internal view returns (bool isWarm_, uint256 value_) { + assembly { + // Get the gas cost of the reading the slot with `sload`. + let startGas := gas() + value_ := sload(_slot) + let endGas := gas() + // If the gas cost of the `sload` is below than the threshold, the slot is warm. + isWarm_ := iszero(gt(sub(startGas, endGas), _WARM_READ_THRESHOLD)) + } } } diff --git a/packages/contracts-bedrock/src/L2/ETHLiquidity.sol b/packages/contracts-bedrock/src/L2/ETHLiquidity.sol index 5a8c97f819..6b9128e0a6 100644 --- a/packages/contracts-bedrock/src/L2/ETHLiquidity.sol +++ b/packages/contracts-bedrock/src/L2/ETHLiquidity.sol @@ -15,7 +15,8 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; /// @custom:predeploy 0x4200000000000000000000000000000000000025 /// @title ETHLiquidity /// @notice The ETHLiquidity contract allows other contracts to access ETH liquidity without -/// needing to modify the EVM to generate new ETH. +/// needing to modify the EVM to generate new ETH. Contract comes "pre-loaded" with +/// uint248.max balance to prevent liquidity shortages. contract ETHLiquidity is ISemver { /// @notice Emitted when an address burns ETH liquidity. event LiquidityBurned(address indexed caller, uint256 value); @@ -24,8 +25,8 @@ contract ETHLiquidity is ISemver { event LiquidityMinted(address indexed caller, uint256 value); /// @notice Semantic version. - /// @custom:semver 1.0.0-beta.6 - string public constant version = "1.0.0-beta.6"; + /// @custom:semver 1.0.0-beta.7 + string public constant version = "1.0.0-beta.7"; /// @notice Allows an address to lock ETH liquidity into this contract. function burn() external payable { diff --git a/packages/contracts-bedrock/src/L2/L1BlockInterop.sol b/packages/contracts-bedrock/src/L2/L1BlockInterop.sol deleted file mode 100644 index 19c3130601..0000000000 --- a/packages/contracts-bedrock/src/L2/L1BlockInterop.sol +++ /dev/null @@ -1,137 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.15; - -// Contracts -import { L1Block } from "src/L2/L1Block.sol"; - -// Libraries -import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import { StaticConfig } from "src/libraries/StaticConfig.sol"; -import { Predeploys } from "src/libraries/Predeploys.sol"; -import { - NotDepositor, - NotCrossL2Inbox, - NotDependency, - DependencySetSizeTooLarge, - AlreadyDependency, - CantRemovedDependency -} from "src/libraries/L1BlockErrors.sol"; - -/// @notice Enum representing different types of configurations that can be set on L1BlockInterop. -/// @custom:value ADD_DEPENDENCY Represents the config type for adding a chain to the interop dependency set. -/// @custom:value REMOVE_DEPENDENCY Represents the config type for removing a chain from the interop dependency set. -enum ConfigType { - ADD_DEPENDENCY, - REMOVE_DEPENDENCY -} - -/// @custom:proxied true -/// @custom:predeploy 0x4200000000000000000000000000000000000015 -/// @title L1BlockInterop -/// @notice Interop extenstions of L1Block. -contract L1BlockInterop is L1Block { - using EnumerableSet for EnumerableSet.UintSet; - - /// @notice Event emitted when a new dependency is added to the interop dependency set. - event DependencyAdded(uint256 indexed chainId); - - /// @notice Event emitted when a dependency is removed from the interop dependency set. - event DependencyRemoved(uint256 indexed chainId); - - /// @notice The interop dependency set, containing the chain IDs in it. - EnumerableSet.UintSet dependencySet; - - /// @notice Storage slot that the isDeposit is stored at. - /// This is a custom slot that is not part of the standard storage layout. - /// keccak256(abi.encode(uint256(keccak256("l1Block.identifier.isDeposit")) - 1)) & ~bytes32(uint256(0xff)) - uint256 internal constant IS_DEPOSIT_SLOT = 0x921bd3a089295c6e5540e8fba8195448d253efd6f2e3e495b499b627dc36a300; - - /// @custom:semver +interop.6 - function version() public pure override returns (string memory) { - return string.concat(super.version(), "+interop.6"); - } - - /// @notice Returns whether the call was triggered from a a deposit or not. - /// @notice This function is only callable by the CrossL2Inbox contract. - function isDeposit() external view returns (bool isDeposit_) { - if (msg.sender != Predeploys.CROSS_L2_INBOX) revert NotCrossL2Inbox(); - assembly { - isDeposit_ := sload(IS_DEPOSIT_SLOT) - } - } - - /// @notice Returns true if a chain ID is in the interop dependency set and false otherwise. - /// The chain's chain ID is always considered to be in the dependency set. - /// @param _chainId The chain ID to check. - /// @return True if the chain ID to check is in the interop dependency set. False otherwise. - function isInDependencySet(uint256 _chainId) public view returns (bool) { - return _chainId == block.chainid || dependencySet.contains(_chainId); - } - - /// @notice Returns the size of the interop dependency set. - /// @return The size of the interop dependency set. - function dependencySetSize() external view returns (uint8) { - return uint8(dependencySet.length()); - } - - /// @notice Updates the `isDeposit` flag and sets the L1 block values for an Interop upgraded chain. - /// It updates the L1 block values through the `setL1BlockValuesEcotone` function. - /// It forwards the calldata to the internally-used `setL1BlockValuesEcotone` function. - function setL1BlockValuesInterop() external { - // Set the isDeposit flag to true. - assembly { - sstore(IS_DEPOSIT_SLOT, 1) - } - - _setL1BlockValuesEcotone(); - } - - /// @notice Resets the isDeposit flag. - /// Should only be called by the depositor account after the deposits are complete. - function depositsComplete() external { - if (msg.sender != DEPOSITOR_ACCOUNT()) revert NotDepositor(); - - // Set the isDeposit flag to false. - assembly { - sstore(IS_DEPOSIT_SLOT, 0) - } - } - - /// @notice Sets static configuration options for the L2 system. Can only be called by the special - /// depositor account. - /// @param _type The type of configuration to set. - /// @param _value The encoded value with which to set the configuration. - function setConfig(ConfigType _type, bytes calldata _value) external { - if (msg.sender != DEPOSITOR_ACCOUNT()) revert NotDepositor(); - - if (_type == ConfigType.ADD_DEPENDENCY) { - _addDependency(_value); - } else if (_type == ConfigType.REMOVE_DEPENDENCY) { - _removeDependency(_value); - } - } - - /// @notice Internal method to add a dependency to the interop dependency set. - /// @param _value The encoded value with which to add the dependency. - function _addDependency(bytes calldata _value) internal { - uint256 chainId = StaticConfig.decodeAddDependency(_value); - - if (dependencySet.length() == type(uint8).max) revert DependencySetSizeTooLarge(); - - if (chainId == block.chainid || !dependencySet.add(chainId)) revert AlreadyDependency(); - - emit DependencyAdded(chainId); - } - - /// @notice Internal method to remove a dependency from the interop dependency set. - /// @param _value The encoded value with which to remove the dependency. - function _removeDependency(bytes calldata _value) internal { - uint256 chainId = StaticConfig.decodeRemoveDependency(_value); - - if (chainId == block.chainid) revert CantRemovedDependency(); - - if (!dependencySet.remove(chainId)) revert NotDependency(); - - emit DependencyRemoved(chainId); - } -} diff --git a/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol b/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol index fc1f74e99c..1b77cc2c15 100644 --- a/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol +++ b/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol @@ -9,7 +9,6 @@ import { TransientReentrancyAware } from "src/libraries/TransientContext.sol"; // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; -import { IDependencySet } from "interfaces/L2/IDependencySet.sol"; import { ICrossL2Inbox, Identifier } from "interfaces/L2/ICrossL2Inbox.sol"; /// @notice Thrown when a non-written slot in transient storage is attempted to be read from. @@ -27,9 +26,6 @@ error MessageDestinationSameChain(); /// @notice Thrown when attempting to relay a message whose destination chain is not the chain relaying it. error MessageDestinationNotRelayChain(); -/// @notice Thrown when attempting to relay a message whose target is CrossL2Inbox. -error MessageTargetCrossL2Inbox(); - /// @notice Thrown when attempting to relay a message whose target is L2ToL2CrossDomainMessenger. error MessageTargetL2ToL2CrossDomainMessenger(); @@ -39,12 +35,6 @@ error MessageAlreadyRelayed(); /// @notice Thrown when a reentrant call is detected. error ReentrantCall(); -/// @notice Thrown when a call to the target contract during message relay fails. -error TargetCallFailed(); - -/// @notice Thrown when attempting to use a chain ID that is not in the dependency set. -error InvalidChainId(); - /// @custom:proxied true /// @custom:predeploy 0x4200000000000000000000000000000000000023 /// @title L2ToL2CrossDomainMessenger @@ -71,8 +61,8 @@ contract L2ToL2CrossDomainMessenger is ISemver, TransientReentrancyAware { uint16 public constant messageVersion = uint16(0); /// @notice Semantic version. - /// @custom:semver 1.0.0-beta.14 - string public constant version = "1.0.0-beta.14"; + /// @custom:semver 1.0.0-beta.16 + string public constant version = "1.0.0-beta.16"; /// @notice Mapping of message hashes to boolean receipt values. Note that a message will only be present in this /// mapping if it has successfully been relayed on this chain, and can therefore not be relayed again. @@ -86,7 +76,7 @@ contract L2ToL2CrossDomainMessenger is ISemver, TransientReentrancyAware { /// @notice Emitted whenever a message is sent to a destination /// @param destination Chain ID of the destination chain. /// @param target Target contract or wallet address. - /// @param messageNonce Nonce associated with the messsage sent + /// @param messageNonce Nonce associated with the message sent /// @param sender Address initiating this message call /// @param message Message payload to call target with. event SentMessage( @@ -134,9 +124,7 @@ contract L2ToL2CrossDomainMessenger is ISemver, TransientReentrancyAware { /// @return The hash of the message being sent, used to track whether the message has successfully been relayed. function sendMessage(uint256 _destination, address _target, bytes calldata _message) external returns (bytes32) { if (_destination == block.chainid) revert MessageDestinationSameChain(); - if (_target == Predeploys.CROSS_L2_INBOX) revert MessageTargetCrossL2Inbox(); if (_target == Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER) revert MessageTargetL2ToL2CrossDomainMessenger(); - if (!IDependencySet(Predeploys.L1_BLOCK_ATTRIBUTES).isInDependencySet(_destination)) revert InvalidChainId(); uint256 nonce = messageNonce(); emit SentMessage(_destination, _target, nonce, msg.sender, _message); @@ -157,7 +145,7 @@ contract L2ToL2CrossDomainMessenger is ISemver, TransientReentrancyAware { /// via cross chain call from the other messenger OR if the message was already received once and is /// currently being replayed. /// @param _id Identifier of the SentMessage event to be relayed - /// @param _sentMessage Message payload of the `SentMessage` event + /// @param _sentMessage Payload of the `SentMessage` event /// @return returnData_ Return data from the target contract call. function relayMessage( Identifier calldata _id, @@ -168,8 +156,7 @@ contract L2ToL2CrossDomainMessenger is ISemver, TransientReentrancyAware { nonReentrant returns (bytes memory returnData_) { - // Ensure the log came from the messenger. Since the log origin is the CDM, there isn't a scenario where - // this can be invoked from the CrossL2Inbox as the SentMessage log is not calldata for this function + // Ensure the log came from the messenger. if (_id.origin != Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER) { revert IdOriginNotL2ToL2CrossDomainMessenger(); } @@ -183,8 +170,6 @@ contract L2ToL2CrossDomainMessenger is ISemver, TransientReentrancyAware { // Assert invariants on the message if (destination != block.chainid) revert MessageDestinationNotRelayChain(); - if (target == Predeploys.CROSS_L2_INBOX) revert MessageTargetCrossL2Inbox(); - if (target == Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER) revert MessageTargetL2ToL2CrossDomainMessenger(); uint256 source = _id.chainId; bytes32 messageHash = Hashing.hashL2toL2CrossDomainMessage({ @@ -200,16 +185,18 @@ contract L2ToL2CrossDomainMessenger is ISemver, TransientReentrancyAware { revert MessageAlreadyRelayed(); } + successfulMessages[messageHash] = true; _storeMessageMetadata(source, sender); bool success; (success, returnData_) = target.call{ value: msg.value }(message); if (!success) { - revert TargetCallFailed(); + assembly { + revert(add(32, returnData_), mload(returnData_)) + } } - successfulMessages[messageHash] = true; emit RelayedMessage(source, nonce, messageHash); _storeMessageMetadata(0, address(0)); @@ -227,11 +214,23 @@ contract L2ToL2CrossDomainMessenger is ISemver, TransientReentrancyAware { /// @param _sender Address of the sender of the message. function _storeMessageMetadata(uint256 _source, address _sender) internal { assembly { - tstore(CROSS_DOMAIN_MESSAGE_SENDER_SLOT, _sender) tstore(CROSS_DOMAIN_MESSAGE_SOURCE_SLOT, _source) + tstore(CROSS_DOMAIN_MESSAGE_SENDER_SLOT, _sender) } } + /// @notice Decodes the payload of a SentMessage event. + /// @dev The payload format is as follows: + /// encodePacked( + /// encode(event selector, destination, target, nonce), + /// encode(sender, message) + /// ) + /// @param _payload Payload of the SentMessage event. + /// @return destination_ Destination chain ID. + /// @return target_ Target contract of the message. + /// @return nonce_ Nonce associated with the messsage sent. + /// @return sender_ Address initiating this message call. + /// @return message_ Message payload to call target with. function _decodeSentMessagePayload(bytes calldata _payload) internal pure diff --git a/packages/contracts-bedrock/src/L2/SuperchainERC20.sol b/packages/contracts-bedrock/src/L2/SuperchainERC20.sol index 061c2d867b..b4e47874fb 100644 --- a/packages/contracts-bedrock/src/L2/SuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/SuperchainERC20.sol @@ -14,13 +14,16 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; import { IERC7802, IERC165 } from "interfaces/L2/IERC7802.sol"; /// @title SuperchainERC20 -/// @notice A standard ERC20 extension implementing IERC7802 for unified cross-chain fungibility across -/// the Superchain. Allows the SuperchainTokenBridge to mint and burn tokens as needed. +/// @notice A standard ERC20 extension implementing IERC7802 for unified cross-chain fungibility +/// across the Superchain. Gives the SuperchainTokenBridge mint and burn permissions. +/// @dev This contract inherits from Solady@v0.0.245 ERC20. Carefully review Solady's, +/// documentation including all warnings, comments and natSpec, before extending or +/// interacting with this contract. abstract contract SuperchainERC20 is ERC20, IERC7802, ISemver { /// @notice Semantic version. - /// @custom:semver 1.0.0-beta.8 + /// @custom:semver 1.0.0-beta.9 function version() external view virtual returns (string memory) { - return "1.0.0-beta.8"; + return "1.0.0-beta.9"; } /// @notice Allows the SuperchainTokenBridge to mint tokens. diff --git a/packages/contracts-bedrock/src/L2/SuperchainWETH.sol b/packages/contracts-bedrock/src/L2/SuperchainWETH.sol index 989d6d55ca..69f92857ee 100644 --- a/packages/contracts-bedrock/src/L2/SuperchainWETH.sol +++ b/packages/contracts-bedrock/src/L2/SuperchainWETH.sol @@ -20,13 +20,16 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @custom:proxied true /// @custom:predeploy 0x4200000000000000000000000000000000000024 /// @title SuperchainWETH -/// @notice SuperchainWETH is a version of WETH that can be freely transfrered between chains +/// @notice SuperchainWETH is a version of WETH that can be freely transferred between chains /// within the superchain. SuperchainWETH can be converted into native ETH on chains that /// do not use a custom gas token. contract SuperchainWETH is WETH98, IERC7802, ISemver { /// @notice Thrown when attempting to relay a message and the cross domain message sender is not SuperchainWETH. error InvalidCrossDomainSender(); + /// @notice Thrown when trying to approve Permit2 with a non-infinite allowance. + error Permit2AllowanceIsFixedAtInfinity(); + /// @notice Emitted when ETH is sent from one chain to another. /// @param from Address of the sender. /// @param to Address of the recipient. @@ -42,8 +45,8 @@ contract SuperchainWETH is WETH98, IERC7802, ISemver { event RelayETH(address indexed from, address indexed to, uint256 amount, uint256 source); /// @notice Semantic version. - /// @custom:semver 1.0.0-beta.14 - string public constant version = "1.0.0-beta.14"; + /// @custom:semver 1.0.0-beta.15 + string public constant version = "1.0.0-beta.15"; /// @inheritdoc WETH98 function allowance(address owner, address spender) public view override returns (uint256) { @@ -67,6 +70,12 @@ contract SuperchainWETH is WETH98, IERC7802, ISemver { emit Transfer(_from, address(0), _amount); } + /// @inheritdoc WETH98 + function approve(address guy, uint256 wad) public virtual override returns (bool) { + if (guy == Preinstalls.Permit2 && wad != type(uint256).max) revert Permit2AllowanceIsFixedAtInfinity(); + return super.approve(guy, wad); + } + /// @notice Allows the SuperchainTokenBridge to mint tokens. /// @param _to Address to mint tokens to. /// @param _amount Amount of tokens to mint. diff --git a/packages/contracts-bedrock/src/dispute/AnchorStateRegistry.sol b/packages/contracts-bedrock/src/dispute/AnchorStateRegistry.sol index 60cd9ba31e..4368e44d0a 100644 --- a/packages/contracts-bedrock/src/dispute/AnchorStateRegistry.sol +++ b/packages/contracts-bedrock/src/dispute/AnchorStateRegistry.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.15; import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; // Libraries -import { GameType, OutputRoot, Claim, GameStatus, Hash } from "src/dispute/lib/Types.sol"; +import { GameType, Proposal, Claim, GameStatus, Hash } from "src/dispute/lib/Types.sol"; // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; @@ -13,7 +13,6 @@ import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; /// @custom:proxied true /// @title AnchorStateRegistry @@ -23,8 +22,11 @@ import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; /// be initialized with a more recent starting state which reduces the amount of required offchain computation. contract AnchorStateRegistry is Initializable, ISemver { /// @notice Semantic version. - /// @custom:semver 2.2.2 - string public constant version = "2.2.2"; + /// @custom:semver 3.1.0 + string public constant version = "3.1.0"; + + /// @notice The dispute game finality delay in seconds. + uint256 internal immutable DISPUTE_GAME_FINALITY_DELAY_SECONDS; /// @notice Address of the SuperchainConfig contract. ISuperchainConfig public superchainConfig; @@ -32,61 +34,105 @@ contract AnchorStateRegistry is Initializable, ISemver { /// @notice Address of the DisputeGameFactory contract. IDisputeGameFactory public disputeGameFactory; - /// @notice Address of the OptimismPortal contract. - IOptimismPortal2 public portal; - /// @notice The game whose claim is currently being used as the anchor state. IFaultDisputeGame public anchorGame; /// @notice The starting anchor root. - OutputRoot internal startingAnchorRoot; + Proposal internal startingAnchorRoot; + + /// @notice Mapping of blacklisted dispute games. + mapping(IDisputeGame => bool) public disputeGameBlacklist; + + /// @notice The respected game type. + GameType public respectedGameType; - /// @notice Emitted when an anchor state is not updated. - /// @param game Game that was not used as the new anchor game. - event AnchorNotUpdated(IFaultDisputeGame indexed game); + /// @notice The retirement timestamp. All games created before or at this timestamp are + /// considered retired and are therefore not valid games. Retirement is used as a + /// blanket invalidation mechanism if games resolve incorrectly. + uint64 public retirementTimestamp; /// @notice Emitted when an anchor state is updated. /// @param game Game that was used as the new anchor game. event AnchorUpdated(IFaultDisputeGame indexed game); - /// @notice Thrown when an unauthorized caller attempts to set the anchor state. - error AnchorStateRegistry_Unauthorized(); + /// @notice Emitted when the respected game type is set. + /// @param gameType The new respected game type. + event RespectedGameTypeSet(GameType gameType); - /// @notice Thrown when an invalid anchor game is provided. - error AnchorStateRegistry_InvalidAnchorGame(); + /// @notice Emitted when the retirement timestamp is set. + /// @param timestamp The new retirement timestamp. + event RetirementTimestampSet(uint256 timestamp); + + /// @notice Emitted when a dispute game is blacklisted. + /// @param disputeGame The dispute game that was blacklisted. + event DisputeGameBlacklisted(IDisputeGame indexed disputeGame); /// @notice Thrown when the anchor root is requested, but the anchor game is blacklisted. error AnchorStateRegistry_AnchorGameBlacklisted(); - /// @notice Constructor to disable initializers. - constructor() { + /// @notice Thrown when an invalid anchor game is provided. + error AnchorStateRegistry_InvalidAnchorGame(); + + /// @notice Thrown when an unauthorized caller attempts to set the anchor state. + error AnchorStateRegistry_Unauthorized(); + + /// @param _disputeGameFinalityDelaySeconds The dispute game finality delay in seconds. + constructor(uint256 _disputeGameFinalityDelaySeconds) { + DISPUTE_GAME_FINALITY_DELAY_SECONDS = _disputeGameFinalityDelaySeconds; _disableInitializers(); } /// @notice Initializes the contract. /// @param _superchainConfig The address of the SuperchainConfig contract. /// @param _disputeGameFactory The address of the DisputeGameFactory contract. - /// @param _portal The address of the OptimismPortal contract. /// @param _startingAnchorRoot The starting anchor root. function initialize( ISuperchainConfig _superchainConfig, IDisputeGameFactory _disputeGameFactory, - IOptimismPortal2 _portal, - OutputRoot memory _startingAnchorRoot + Proposal memory _startingAnchorRoot, + GameType _startingRespectedGameType ) external initializer { superchainConfig = _superchainConfig; disputeGameFactory = _disputeGameFactory; - portal = _portal; startingAnchorRoot = _startingAnchorRoot; + respectedGameType = _startingRespectedGameType; + retirementTimestamp = uint64(block.timestamp); + } + + /// @notice Returns whether the contract is paused. + function paused() public view returns (bool) { + return superchainConfig.paused(); } - /// @notice Returns the respected game type. - /// @return The respected game type. - function respectedGameType() public view returns (GameType) { - return portal.respectedGameType(); + /// @notice Returns the dispute game finality delay in seconds. + function disputeGameFinalityDelaySeconds() external view returns (uint256) { + return DISPUTE_GAME_FINALITY_DELAY_SECONDS; + } + + /// @notice Allows the Guardian to set the respected game type. + /// @param _gameType The new respected game type. + function setRespectedGameType(GameType _gameType) external { + if (msg.sender != superchainConfig.guardian()) revert AnchorStateRegistry_Unauthorized(); + respectedGameType = _gameType; + emit RespectedGameTypeSet(_gameType); + } + + /// @notice Allows the Guardian to update the retirement timestamp. + function updateRetirementTimestamp() external { + if (msg.sender != superchainConfig.guardian()) revert AnchorStateRegistry_Unauthorized(); + retirementTimestamp = uint64(block.timestamp); + emit RetirementTimestampSet(block.timestamp); + } + + /// @notice Allows the Guardian to blacklist a dispute game. + /// @param _disputeGame Dispute game to blacklist. + function blacklistDisputeGame(IDisputeGame _disputeGame) external { + if (msg.sender != superchainConfig.guardian()) revert AnchorStateRegistry_Unauthorized(); + disputeGameBlacklist[_disputeGame] = true; + emit DisputeGameBlacklisted(_disputeGame); } /// @custom:legacy @@ -103,11 +149,11 @@ contract AnchorStateRegistry is Initializable, ISemver { function getAnchorRoot() public view returns (Hash, uint256) { // Return the starting anchor root if there is no anchor game. if (address(anchorGame) == address(0)) { - return (startingAnchorRoot.root, startingAnchorRoot.l2BlockNumber); + return (startingAnchorRoot.root, startingAnchorRoot.l2SequenceNumber); } // Otherwise, return the anchor root. - return (Hash.wrap(anchorGame.rootClaim().raw()), anchorGame.l2BlockNumber()); + return (Hash.wrap(anchorGame.rootClaim().raw()), anchorGame.l2SequenceNumber()); } /// @notice Determines whether a game is registered in the DisputeGameFactory. @@ -129,6 +175,9 @@ contract AnchorStateRegistry is Initializable, ISemver { /// @param _game The game to check. /// @return Whether the game is of a respected game type. function isGameRespected(IDisputeGame _game) public view returns (bool) { + // We don't do a try/catch here for legacy games because by the time this code is live on + // mainnet, users won't be using legacy games anymore. Avoiding the try/catch simplifies + // the logic. return _game.wasRespectedGameTypeWhenCreated(); } @@ -136,7 +185,7 @@ contract AnchorStateRegistry is Initializable, ISemver { /// @param _game The game to check. /// @return Whether the game is blacklisted. function isGameBlacklisted(IDisputeGame _game) public view returns (bool) { - return portal.disputeGameBlacklist(_game); + return disputeGameBlacklist[_game]; } /// @notice Determines whether a game is retired. @@ -146,7 +195,7 @@ contract AnchorStateRegistry is Initializable, ISemver { // Must be created after the respectedGameTypeUpdatedAt timestamp. Note that this means all // games created in the same block as the respectedGameTypeUpdatedAt timestamp are // considered retired. - return _game.createdAt().raw() <= portal.respectedGameTypeUpdatedAt(); + return _game.createdAt().raw() <= retirementTimestamp; } /// @notice Returns whether a game is resolved. @@ -186,6 +235,11 @@ contract AnchorStateRegistry is Initializable, ISemver { return false; } + // Must not be paused, temporarily causes game to be considered improper. + if (paused()) { + return false; + } + return true; } @@ -200,7 +254,7 @@ contract AnchorStateRegistry is Initializable, ISemver { // Game must be beyond the "airgap period" - time since resolution must be at least // "dispute game finality delay" seconds in the past. - if (block.timestamp - _game.resolvedAt().raw() <= portal.disputeGameFinalityDelaySeconds()) { + if (block.timestamp - _game.resolvedAt().raw() <= DISPUTE_GAME_FINALITY_DELAY_SECONDS) { return false; } @@ -251,7 +305,7 @@ contract AnchorStateRegistry is Initializable, ISemver { // Must be newer than the current anchor game. (, uint256 anchorL2BlockNumber) = getAnchorRoot(); - if (game.l2BlockNumber() <= anchorL2BlockNumber) { + if (game.l2SequenceNumber() <= anchorL2BlockNumber) { revert AnchorStateRegistry_InvalidAnchorGame(); } diff --git a/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol b/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol index 2ee9bcd58c..09314a013a 100644 --- a/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol +++ b/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol @@ -17,7 +17,7 @@ import { Duration, Timestamp, Hash, - OutputRoot, + Proposal, LibClock, LocalPreimageKey, VMStatuses @@ -170,9 +170,9 @@ contract FaultDisputeGame is Clone, ISemver { uint256 internal constant HEADER_BLOCK_NUMBER_INDEX = 8; /// @notice Semantic version. - /// @custom:semver 1.4.1 + /// @custom:semver 1.5.0 function version() public pure virtual returns (string memory) { - return "1.4.1"; + return "1.5.0"; } /// @notice The starting timestamp of the game @@ -213,7 +213,7 @@ contract FaultDisputeGame is Clone, ISemver { mapping(uint256 => ResolutionCheckpoint) public resolutionCheckpoints; /// @notice The latest finalized output root, serving as the anchor for output bisection. - OutputRoot public startingOutputRoot; + Proposal public startingOutputRoot; /// @notice A boolean for whether or not the game type was respected when the game was created. bool public wasRespectedGameTypeWhenCreated; @@ -302,8 +302,8 @@ contract FaultDisputeGame is Clone, ISemver { // Should only happen if this is a new game type that hasn't been set up yet. if (root.raw() == bytes32(0)) revert AnchorRootNotFound(); - // Set the starting output root. - startingOutputRoot = OutputRoot({ l2BlockNumber: rootBlockNumber, root: root }); + // Set the starting proposal. + startingOutputRoot = Proposal({ l2SequenceNumber: rootBlockNumber, root: root }); // Revert if the calldata size is not the expected length. // @@ -616,7 +616,7 @@ contract FaultDisputeGame is Clone, ISemver { // We add the index at depth + 1 to the starting block number to get the disputed L2 // block number. - uint256 l2Number = startingOutputRoot.l2BlockNumber + disputedPos.traceIndex(SPLIT_DEPTH) + 1; + uint256 l2Number = startingOutputRoot.l2SequenceNumber + disputedPos.traceIndex(SPLIT_DEPTH) + 1; // Choose the minimum between the `l2BlockNumber` claim and the bisected-to L2 block number. l2Number = l2Number < l2BlockNumber() ? l2Number : l2BlockNumber(); @@ -647,9 +647,14 @@ contract FaultDisputeGame is Clone, ISemver { l2BlockNumber_ = _getArgUint256(0x54); } + /// @notice The l2SequenceNumber of the disputed output root in the `L2OutputOracle` (in this case - block number). + function l2SequenceNumber() public pure returns (uint256 l2SequenceNumber_) { + l2SequenceNumber_ = l2BlockNumber(); + } + /// @notice Only the starting block number of the game. function startingBlockNumber() external view returns (uint256 startingBlockNumber_) { - startingBlockNumber_ = startingOutputRoot.l2BlockNumber; + startingBlockNumber_ = startingOutputRoot.l2SequenceNumber; } /// @notice Starting output root and block number of the game. diff --git a/packages/contracts-bedrock/src/dispute/PermissionedDisputeGame.sol b/packages/contracts-bedrock/src/dispute/PermissionedDisputeGame.sol index f12fb2fce2..0e1c8a5528 100644 --- a/packages/contracts-bedrock/src/dispute/PermissionedDisputeGame.sol +++ b/packages/contracts-bedrock/src/dispute/PermissionedDisputeGame.sol @@ -32,9 +32,9 @@ contract PermissionedDisputeGame is FaultDisputeGame { } /// @notice Semantic version. - /// @custom:semver 1.4.1 + /// @custom:semver 1.5.0 function version() public pure override returns (string memory) { - return "1.4.1"; + return "1.5.0"; } /// @param _params Parameters for creating a new FaultDisputeGame. diff --git a/packages/contracts-bedrock/src/dispute/SuperFaultDisputeGame.sol b/packages/contracts-bedrock/src/dispute/SuperFaultDisputeGame.sol index bd96a4b406..f08df98f09 100644 --- a/packages/contracts-bedrock/src/dispute/SuperFaultDisputeGame.sol +++ b/packages/contracts-bedrock/src/dispute/SuperFaultDisputeGame.sol @@ -14,7 +14,7 @@ import { Duration, Timestamp, Hash, - OutputRoot, + Proposal, LibClock, LocalPreimageKey, VMStatuses @@ -163,9 +163,9 @@ contract SuperFaultDisputeGame is Clone, ISemver { Position internal constant ROOT_POSITION = Position.wrap(1); /// @notice Semantic version. - /// @custom:semver 0.1.0-beta.1 + /// @custom:semver 0.2.0-beta.1 function version() public pure virtual returns (string memory) { - return "0.1.0-beta.1"; + return "0.2.0-beta.1"; } /// @notice The starting timestamp of the game @@ -199,7 +199,7 @@ contract SuperFaultDisputeGame is Clone, ISemver { mapping(uint256 => ResolutionCheckpoint) public resolutionCheckpoints; /// @notice The latest finalized output root, serving as the anchor for output bisection. - OutputRoot public startingOutputRoot; + Proposal public startingProposal; /// @notice A boolean for whether or not the game type was respected when the game was created. bool public wasRespectedGameTypeWhenCreated; @@ -278,7 +278,7 @@ contract SuperFaultDisputeGame is Clone, ISemver { // // Explicit checks: // - The game must not have already been initialized. - // - An output root cannot be proposed at or before the starting block number. + // - An output root cannot be proposed at or before the starting l2SequenceNumber. // INVARIANT: The game must not have already been initialized. if (initialized) revert AlreadyInitialized(); @@ -292,8 +292,8 @@ contract SuperFaultDisputeGame is Clone, ISemver { // Prevent initializing right away with an invalid claim state that is used as convention if (rootClaim().raw() == INVALID_ROOT_CLAIM) revert SuperFaultDisputeGameInvalidRootClaim(); - // Set the starting output root. - startingOutputRoot = OutputRoot({ l2BlockNumber: rootBlockNumber, root: root }); + // Set the starting Proposal. + startingProposal = Proposal({ l2SequenceNumber: rootBlockNumber, root: root }); // Revert if the calldata size is not the expected length. // @@ -318,7 +318,7 @@ contract SuperFaultDisputeGame is Clone, ISemver { // Do not allow the game to be initialized if the root claim corresponds to a block at or before the // configured starting block number. - if (l2BlockNumber() <= rootBlockNumber) revert UnexpectedRootClaim(rootClaim()); + if (l2SequenceNumber() <= rootBlockNumber) revert UnexpectedRootClaim(rootClaim()); // Set the root claim claimData.push( @@ -597,7 +597,7 @@ contract SuperFaultDisputeGame is Clone, ISemver { // Load the disputed proposal's output root oracle.loadLocalData(_ident, uuid.raw(), disputed.raw(), 32, _partOffset); } else if (_ident == LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER) { - oracle.loadLocalData(_ident, uuid.raw(), bytes32(l2BlockNumber() << 0xC0), 8, _partOffset); + oracle.loadLocalData(_ident, uuid.raw(), bytes32(l2SequenceNumber() << 0xC0), 8, _partOffset); } else { revert InvalidLocalIdent(); } @@ -615,19 +615,19 @@ contract SuperFaultDisputeGame is Clone, ISemver { numRemainingChildren_ = challengeIndicesLen - checkpoint.subgameIndex; } - /// @notice The l2BlockNumber of the disputed output root in the `L2OutputOracle`. - function l2BlockNumber() public pure returns (uint256 l2BlockNumber_) { - l2BlockNumber_ = _getArgUint256(0x54); + /// @notice The l2SequenceNumber (timestamp) of the disputed super root in game root claim. + function l2SequenceNumber() public pure returns (uint256 l2SequenceNumber_) { + l2SequenceNumber_ = _getArgUint256(0x54); } /// @notice Only the starting block number of the game. - function startingBlockNumber() external view returns (uint256 startingBlockNumber_) { - startingBlockNumber_ = startingOutputRoot.l2BlockNumber; + function startingSequenceNumber() external view returns (uint256 startingSequenceNumber_) { + startingSequenceNumber_ = startingProposal.l2SequenceNumber; } - /// @notice Starting output root and block number of the game. + /// @notice Starting super root and block number of the game. function startingRootHash() external view returns (Hash startingRootHash_) { - startingRootHash_ = startingOutputRoot.root; + startingRootHash_ = startingProposal.root; } //////////////////////////////////////////////////////////////// @@ -1060,7 +1060,7 @@ contract SuperFaultDisputeGame is Clone, ISemver { view { // The root claim of an execution trace bisection sub-game must: - // 1. Signal that the VM panicked or resulted in an invalid transition if the disputed output root + // 1. Signal that the VM panicked or resulted in an invalid transition if the disputed super root // was made by the opposing party. // 2. Signal that the VM resulted in a valid transition if the disputed output root was made by the same party. @@ -1169,7 +1169,7 @@ contract SuperFaultDisputeGame is Clone, ISemver { ClaimData storage starting = _findTraceAncestor(Position.wrap(outputPos.raw() - 1), claimIdx, true); (startingClaim_, startingPos_) = (starting.claim, starting.position); } else { - startingClaim_ = Claim.wrap(startingOutputRoot.root.raw()); + startingClaim_ = Claim.wrap(startingProposal.root.raw()); } (disputedClaim_, disputedPos_) = (claim.claim, claim.position); } else { diff --git a/packages/contracts-bedrock/src/dispute/SuperPermissionedDisputeGame.sol b/packages/contracts-bedrock/src/dispute/SuperPermissionedDisputeGame.sol index 1042408de7..deb9f3f1ec 100644 --- a/packages/contracts-bedrock/src/dispute/SuperPermissionedDisputeGame.sol +++ b/packages/contracts-bedrock/src/dispute/SuperPermissionedDisputeGame.sol @@ -33,9 +33,9 @@ contract SuperPermissionedDisputeGame is SuperFaultDisputeGame { } /// @notice Semantic version. - /// @custom:semver 0.1.0-beta.1 + /// @custom:semver 0.2.0-beta.1 function version() public pure override returns (string memory) { - return "0.1.0-beta.1"; + return "0.2.0-beta.1"; } /// @param _params Parameters for creating a new FaultDisputeGame. diff --git a/packages/contracts-bedrock/src/dispute/lib/Types.sol b/packages/contracts-bedrock/src/dispute/lib/Types.sol index 18c197836a..35da47eb62 100644 --- a/packages/contracts-bedrock/src/dispute/lib/Types.sol +++ b/packages/contracts-bedrock/src/dispute/lib/Types.sol @@ -37,12 +37,13 @@ enum BondDistributionMode { REFUND } -/// @notice Represents an L2 output root and the L2 block number at which it was generated. +/// @notice Represents an L2 root and the L2 sequence number at which it was generated. /// @custom:field root The output root. -/// @custom:field l2BlockNumber The L2 block number at which the output root was generated. -struct OutputRoot { +/// @custom:field l2SequenceNumber The L2 Sequence Number ( e.g. block number / timestamp) at which the root was +/// generated. +struct Proposal { Hash root; - uint256 l2BlockNumber; + uint256 l2SequenceNumber; } /// @title GameTypes diff --git a/packages/contracts-bedrock/src/libraries/Encoding.sol b/packages/contracts-bedrock/src/libraries/Encoding.sol index 00c20ea459..3e6ebaa471 100644 --- a/packages/contracts-bedrock/src/libraries/Encoding.sol +++ b/packages/contracts-bedrock/src/libraries/Encoding.sol @@ -9,6 +9,12 @@ import { RLPWriter } from "src/libraries/rlp/RLPWriter.sol"; /// @title Encoding /// @notice Encoding handles Optimism's various different encoding schemes. library Encoding { + /// @notice Thrown when a provided Super Root proof has an invalid version. + error Encoding_InvalidSuperRootVersion(); + + /// @notice Thrown when a provided Super Root proof has no Output Roots. + error Encoding_EmptySuperRoot(); + /// @notice RLP encodes the L2 transaction that would be generated when a given deposit is sent /// to the L2 system. Useful for searching for a deposit in the L2 system. The /// transaction is prefixed with 0x7e to identify its EIP-2718 type. @@ -223,43 +229,29 @@ library Encoding { ); } - /// @notice Returns an appropriately encoded call to L1Block.setL1BlockValuesInterop - /// @param _baseFeeScalar L1 base fee Scalar - /// @param _blobBaseFeeScalar L1 blob base fee Scalar - /// @param _sequenceNumber Number of L2 blocks since epoch start. - /// @param _timestamp L1 timestamp. - /// @param _number L1 blocknumber. - /// @param _baseFee L1 base fee. - /// @param _blobBaseFee L1 blob base fee. - /// @param _hash L1 blockhash. - /// @param _batcherHash Versioned hash to authenticate batcher by. - function encodeSetL1BlockValuesInterop( - uint32 _baseFeeScalar, - uint32 _blobBaseFeeScalar, - uint64 _sequenceNumber, - uint64 _timestamp, - uint64 _number, - uint256 _baseFee, - uint256 _blobBaseFee, - bytes32 _hash, - bytes32 _batcherHash - ) - internal - pure - returns (bytes memory) - { - bytes4 functionSignature = bytes4(keccak256("setL1BlockValuesInterop()")); - return abi.encodePacked( - functionSignature, - _baseFeeScalar, - _blobBaseFeeScalar, - _sequenceNumber, - _timestamp, - _number, - _baseFee, - _blobBaseFee, - _hash, - _batcherHash - ); + /// @notice Encodes a super root proof into the preimage of a Super Root. + /// @param _superRootProof Super root proof to encode. + /// @return Encoded super root proof. + function encodeSuperRootProof(Types.SuperRootProof memory _superRootProof) internal pure returns (bytes memory) { + // Version must match the expected version. + if (_superRootProof.version != 0x01) { + revert Encoding_InvalidSuperRootVersion(); + } + + // Output roots must not be empty. + if (_superRootProof.outputRoots.length == 0) { + revert Encoding_EmptySuperRoot(); + } + + // Start with version byte and timestamp. + bytes memory encoded = bytes.concat(bytes1(0x01), bytes8(_superRootProof.timestamp)); + + // Add each output root (chainId + root) + for (uint256 i = 0; i < _superRootProof.outputRoots.length; i++) { + Types.OutputRootWithChainId memory outputRoot = _superRootProof.outputRoots[i]; + encoded = bytes.concat(encoded, bytes32(outputRoot.chainId), outputRoot.root); + } + + return encoded; } } diff --git a/packages/contracts-bedrock/src/libraries/Hashing.sol b/packages/contracts-bedrock/src/libraries/Hashing.sol index b736ad9e4b..782bbbe4f9 100644 --- a/packages/contracts-bedrock/src/libraries/Hashing.sol +++ b/packages/contracts-bedrock/src/libraries/Hashing.sol @@ -146,4 +146,11 @@ library Hashing { { return keccak256(abi.encode(_destination, _source, _nonce, _sender, _target, _message)); } + + /// @notice Hashes a Super Root proof into a Super Root. + /// @param _superRootProof Super Root proof to hash. + /// @return Hashed super root proof. + function hashSuperRootProof(Types.SuperRootProof memory _superRootProof) internal pure returns (bytes32) { + return keccak256(Encoding.encodeSuperRootProof(_superRootProof)); + } } diff --git a/packages/contracts-bedrock/src/libraries/L1BlockErrors.sol b/packages/contracts-bedrock/src/libraries/L1BlockErrors.sol index 44e156e158..6b5d2e5704 100644 --- a/packages/contracts-bedrock/src/libraries/L1BlockErrors.sol +++ b/packages/contracts-bedrock/src/libraries/L1BlockErrors.sol @@ -3,18 +3,3 @@ pragma solidity ^0.8.0; /// @notice Error returns when a non-depositor account tries to set L1 block values. error NotDepositor(); - -/// @notice Error when a non-cross L2 Inbox sender tries to call the `isDeposit()` method. -error NotCrossL2Inbox(); - -/// @notice Error when a chain ID is not in the interop dependency set. -error NotDependency(); - -/// @notice Error when the interop dependency set size is too large. -error DependencySetSizeTooLarge(); - -/// @notice Error when a chain ID already in the interop dependency set is attempted to be added. -error AlreadyDependency(); - -/// @notice Error when the chain's chain ID is attempted to be removed from the interop dependency set. -error CantRemovedDependency(); diff --git a/packages/contracts-bedrock/src/libraries/PortalErrors.sol b/packages/contracts-bedrock/src/libraries/PortalErrors.sol deleted file mode 100644 index 9096a2938f..0000000000 --- a/packages/contracts-bedrock/src/libraries/PortalErrors.sol +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -/// @notice Error for when a deposit or withdrawal is to a bad target. -error BadTarget(); -/// @notice Error for when a deposit has too much calldata. -error LargeCalldata(); -/// @notice Error for when a deposit has too small of a gas limit. -error SmallGasLimit(); -/// @notice Error for when a withdrawal transfer fails. -error TransferFailed(); -/// @notice Error for when a method cannot be called with non zero CALLVALUE. -error NoValue(); -/// @notice Error for an unauthorized CALLER. -error Unauthorized(); -/// @notice Error for when a method cannot be called when paused. This could be renamed -/// to `Paused` in the future, but it collides with the `Paused` event. -error CallPaused(); -/// @notice Error for special gas estimation. -error GasEstimation(); -/// @notice Error for when a method is being reentered. -error NonReentrant(); -/// @notice Error for invalid proof. -error InvalidProof(); -/// @notice Error for invalid game type. -error InvalidGameType(); -/// @notice Error for an invalid dispute game. -error InvalidDisputeGame(); -/// @notice Error for an invalid merkle proof. -error InvalidMerkleProof(); -/// @notice Error for when a dispute game has been blacklisted. -error Blacklisted(); -/// @notice Error for when trying to withdrawal without first proven. -error Unproven(); -/// @notice Error for when a proposal is not validated. -error ProposalNotValidated(); -/// @notice Error for when a withdrawal has already been finalized. -error AlreadyFinalized(); -/// @notice Error for when a game is a legacy game. -error LegacyGame(); diff --git a/packages/contracts-bedrock/src/libraries/Preinstalls.sol b/packages/contracts-bedrock/src/libraries/Preinstalls.sol index 3017f6fc7f..c8685e0b73 100644 --- a/packages/contracts-bedrock/src/libraries/Preinstalls.sol +++ b/packages/contracts-bedrock/src/libraries/Preinstalls.sol @@ -59,13 +59,13 @@ library Preinstalls { /// @notice Address of history storage contract, introduced in the Prague upgrade. /// See HISTORY_STORAGE_ADDRESS in EIP-2935. - /// This contract is introduced in L2 through an Ecotone upgrade transaction, if not already in genesis. - address internal constant HistoryStorage = 0x0F792be4B0c0cb4DAE440Ef133E90C0eCD48CCCC; + /// This contract is introduced in L2 through an Isthmus upgrade transaction, if not already in genesis. + address internal constant HistoryStorage = 0x0000F90827F1C53a10cb7A02335B175320002935; /// @notice See https://eips.ethereum.org/EIPS/eip-2935, this is the address of the sender of the deployment tx. /// The nonce of this account must be non-zero, to ensure the Ishtmus upgrade tx is still successful /// if the code is already in place. - address internal constant HistoryStorageSender = 0xE9f0662359Bb2c8111840eFFD73B9AFA77CbDE10; + address internal constant HistoryStorageSender = 0x3462413Af4609098e1E27A490f554f260213D685; // @notice Permit2 code is templated. The template is a copy of the Mainnet Ethereum L1 Permit2 deployment. // This deployed bytecode contains two immutable values _CACHED_CHAIN_ID and _CACHED_DOMAIN_SEPARATOR, diff --git a/packages/contracts-bedrock/src/libraries/Types.sol b/packages/contracts-bedrock/src/libraries/Types.sol index 7e9a65654b..3c18c4e1bc 100644 --- a/packages/contracts-bedrock/src/libraries/Types.sol +++ b/packages/contracts-bedrock/src/libraries/Types.sol @@ -29,6 +29,24 @@ library Types { bytes32 latestBlockhash; } + /// @notice Struct representing an output root with a chain id. + /// @custom:field chainId The chain ID of the L2 chain that the output root commits to. + /// @custom:field root The output root. + struct OutputRootWithChainId { + uint256 chainId; + bytes32 root; + } + + /// @notice Struct representing a super root proof. + /// @custom:field version The version of the super root proof. + /// @custom:field timestamp The timestamp of the super root proof. + /// @custom:field outputRoots The output roots that are included in the super root proof. + struct SuperRootProof { + bytes1 version; + uint64 timestamp; + OutputRootWithChainId[] outputRoots; + } + /// @notice Struct representing a deposit transaction (L1 => L2 transaction) created by an end /// user (as opposed to a system deposit transaction generated by the system). /// @custom:field from Address of the sender of the transaction. diff --git a/packages/contracts-bedrock/src/periphery/monitoring/DisputeMonitorHelper.sol b/packages/contracts-bedrock/src/periphery/monitoring/DisputeMonitorHelper.sol new file mode 100644 index 0000000000..8f5dee3a1c --- /dev/null +++ b/packages/contracts-bedrock/src/periphery/monitoring/DisputeMonitorHelper.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +// Interfaces +import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; +import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; +import { Timestamp, GameType, Claim } from "src/dispute/lib/Types.sol"; + +/// @title DisputeMonitorHelper +/// @notice Peripheral contract that can help to monitor dispute games. Supplements offchain tools +/// by simplifying certain queries about dispute games. +contract DisputeMonitorHelper { + /// @notice Thrown when the end index is less than the start index. + error DisputeMonitorHelper_InvalidSearchRange(); + + /// @notice Enum representing the direction of the search. + enum SearchDirection { + OLDER_THAN_OR_EQ, + NEWER_THAN_OR_EQ + } + + /// @notice Checks if a game was created by the provided factory. + /// @param _factory The factory of the dispute games. + /// @param _game The game to check. + /// @return isValid_ True if the game was created by the factory, false otherwise. + function isGameRegistered(IDisputeGameFactory _factory, IDisputeGame _game) public view returns (bool isValid_) { + // Grab the game and game data. + (GameType gameType, Claim rootClaim, bytes memory extraData) = _game.gameData(); + + // Grab the verified address of the game based on the game data. + (IDisputeGame _factoryRegisteredGame,) = + _factory.games({ _gameType: gameType, _rootClaim: rootClaim, _extraData: extraData }); + + // Return whether the game is factory registered. + isValid_ = address(_factoryRegisteredGame) == address(_game); + } + + /// @notice Finds all unresolved games in a given time range. + /// @param _factory The factory of the dispute games. + /// @param _creationRangeStart Start of the range of game creation timestamps. + /// @param _creationRangeEnd End of the range of game creation timestamps. + /// @return unresolvedGames_ The array of unresolved games. + function getUnresolvedGames( + IDisputeGameFactory _factory, + uint256 _creationRangeStart, + uint256 _creationRangeEnd + ) + public + view + returns (IDisputeGame[] memory unresolvedGames_) + { + // Check that the max + if (_creationRangeEnd < _creationRangeStart) { + revert DisputeMonitorHelper_InvalidSearchRange(); + } + + // If there are no games, return an empty array. In theory we could error here too but it's + // easier for offchain tooling if this case just returns empty. Either is fine, but this is + // likely to be a common standard case and it'd be nicer if it didn't error. + if (_factory.gameCount() == 0) { + return new IDisputeGame[](0); + } + + // Try to find a suitable start and end index. If startIdx is type(uint256).max then we did + // not find any newer games and the creation range start must be after the timestamp of the + // latest game. Similarly, if endIdx is type(uint256).max then we did not find any older + // games and the creation range end must be before the timestamp of the earliest game. In + // either case, we can return an empty array. + uint256 startIdx = search(_factory, _creationRangeStart, SearchDirection.NEWER_THAN_OR_EQ); + uint256 endIdx = search(_factory, _creationRangeEnd, SearchDirection.OLDER_THAN_OR_EQ); + if (startIdx == type(uint256).max || endIdx == type(uint256).max) { + return new IDisputeGame[](0); + } + + // Additionally, if the end index is less than the start index, then the range is between + // two dispute games. We return an empty array in this case. + if (endIdx < startIdx) { + return new IDisputeGame[](0); + } + + // Allocate the array and fill it + unresolvedGames_ = new IDisputeGame[](endIdx - startIdx + 1); + uint256 unresolvedGameCount = 0; + for (uint256 i = startIdx; i <= endIdx; i++) { + (,, IDisputeGame game) = _factory.gameAtIndex(i); + if (game.resolvedAt().raw() == 0) { + unresolvedGames_[unresolvedGameCount] = game; + unresolvedGameCount++; + } + } + + // Clobber the size of the array to return the right size. + assembly { + mstore(unresolvedGames_, unresolvedGameCount) + } + } + + /// @notice Searches for a game by timestamp, returning the index of the game that best matches the + /// given search direction. + /// @param _factory The factory of the dispute games. + /// @param _targetTimestamp The timestamp to search for. + /// @param _direction The direction to search in (older or newer). + /// @return index_ The index of the matching game, if it exists. + function search( + IDisputeGameFactory _factory, + uint256 _targetTimestamp, + SearchDirection _direction + ) + public + view + returns (uint256 index_) + { + uint256 gameCount = _factory.gameCount(); + // If there are no games, return max to indicate "not found." + if (gameCount == 0) { + return type(uint256).max; + } + + uint256 left = 0; + uint256 right = gameCount - 1; + + // We'll store the candidate here. If it remains max, no suitable game was found. + index_ = type(uint256).max; + + while (left <= right) { + uint256 mid = left + (right - left) / 2; + (, Timestamp timestamp,) = _factory.gameAtIndex(mid); + uint256 gameTimestamp = uint64(timestamp.raw()); + + if (_direction == SearchDirection.OLDER_THAN_OR_EQ) { + // Rightmost index where timestamp <= _targetTimestamp + if (gameTimestamp <= _targetTimestamp) { + index_ = mid; + left = mid + 1; + } else { + if (mid == 0) { + // Prevent underflow + break; + } + right = mid - 1; + } + } else { + // Leftmost index where timestamp >= _targetTimestamp + if (gameTimestamp >= _targetTimestamp) { + index_ = mid; + if (mid == 0) { + // Prevent underflow + break; + } + right = mid - 1; + } else { + left = mid + 1; + } + } + } + } +} diff --git a/packages/contracts-bedrock/src/safe/DeputyGuardianModule.sol b/packages/contracts-bedrock/src/safe/DeputyGuardianModule.sol index a742c452ef..2a0be015c0 100644 --- a/packages/contracts-bedrock/src/safe/DeputyGuardianModule.sol +++ b/packages/contracts-bedrock/src/safe/DeputyGuardianModule.sol @@ -6,14 +6,11 @@ import { GnosisSafe as Safe } from "safe-contracts/GnosisSafe.sol"; import { Enum } from "safe-contracts/common/Enum.sol"; // Libraries -import { Unauthorized } from "src/libraries/PortalErrors.sol"; import { GameType, Timestamp } from "src/dispute/lib/Types.sol"; // Interfaces import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; -import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; @@ -24,7 +21,10 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; /// authorization at any time by disabling this module. contract DeputyGuardianModule is ISemver { /// @notice Error message for failed transaction execution - error ExecutionFailed(string); + error DeputyGuardianModule_ExecutionFailed(string); + + /// @notice Thrown when the caller is not the deputy guardian. + error DeputyGuardianModule_Unauthorized(); /// @notice Emitted when the SuperchainConfig is paused event Paused(string identifier); @@ -38,6 +38,9 @@ contract DeputyGuardianModule is ISemver { /// @notice Emitted when the respected game type is set event RespectedGameTypeSet(GameType indexed gameType, Timestamp indexed updatedAt); + /// @notice Emitted when the retirement timestamp is updated + event RetirementTimestampUpdated(Timestamp indexed updatedAt); + /// @notice The Safe contract instance Safe internal immutable SAFE; @@ -48,8 +51,8 @@ contract DeputyGuardianModule is ISemver { address internal immutable DEPUTY_GUARDIAN; /// @notice Semantic version. - /// @custom:semver 2.0.1-beta.5 - string public constant version = "2.0.1-beta.5"; + /// @custom:semver 3.0.0 + string public constant version = "3.0.0"; // Constructor to initialize the Safe and baseModule instances constructor(Safe _safe, ISuperchainConfig _superchainConfig, address _deputyGuardian) { @@ -79,7 +82,7 @@ contract DeputyGuardianModule is ISemver { /// @notice Internal function to ensure that only the deputy guardian can call certain functions. function _onlyDeputyGuardian() internal view { if (msg.sender != DEPUTY_GUARDIAN) { - revert Unauthorized(); + revert DeputyGuardianModule_Unauthorized(); } } @@ -93,7 +96,7 @@ contract DeputyGuardianModule is ISemver { (bool success, bytes memory returnData) = SAFE.execTransactionFromModuleReturnData(address(SUPERCHAIN_CONFIG), 0, data, Enum.Operation.Call); if (!success) { - revert ExecutionFailed(string(returnData)); + revert DeputyGuardianModule_ExecutionFailed(string(returnData)); } emit Paused("Deputy Guardian"); } @@ -108,58 +111,58 @@ contract DeputyGuardianModule is ISemver { (bool success, bytes memory returnData) = SAFE.execTransactionFromModuleReturnData(address(SUPERCHAIN_CONFIG), 0, data, Enum.Operation.Call); if (!success) { - revert ExecutionFailed(string(returnData)); + revert DeputyGuardianModule_ExecutionFailed(string(returnData)); } emit Unpaused(); } /// @notice Calls the Security Council Safe's `execTransactionFromModuleReturnData()`, with the arguments - /// necessary to call `setAnchorState()` on the `AnchorStateRegistry` contract. + /// necessary to call `blacklistDisputeGame()` on the `AnchorStateRegistry` contract. /// Only the deputy guardian can call this function. - /// @param _registry The `IAnchorStateRegistry` contract instance. - /// @param _game The `IFaultDisputeGame` contract instance. - function setAnchorState(IAnchorStateRegistry _registry, IFaultDisputeGame _game) external { + /// @param _anchorStateRegistry The `AnchorStateRegistry` contract instance. + /// @param _game The `IDisputeGame` contract instance. + function blacklistDisputeGame(IAnchorStateRegistry _anchorStateRegistry, IDisputeGame _game) external { _onlyDeputyGuardian(); - bytes memory data = abi.encodeCall(IAnchorStateRegistry.setAnchorState, (_game)); + bytes memory data = abi.encodeCall(IAnchorStateRegistry.blacklistDisputeGame, (_game)); (bool success, bytes memory returnData) = - SAFE.execTransactionFromModuleReturnData(address(_registry), 0, data, Enum.Operation.Call); + SAFE.execTransactionFromModuleReturnData(address(_anchorStateRegistry), 0, data, Enum.Operation.Call); if (!success) { - revert ExecutionFailed(string(returnData)); + revert DeputyGuardianModule_ExecutionFailed(string(returnData)); } + emit DisputeGameBlacklisted(_game); } /// @notice Calls the Security Council Safe's `execTransactionFromModuleReturnData()`, with the arguments - /// necessary to call `blacklistDisputeGame()` on the `OptimismPortal2` contract. + /// necessary to call `setRespectedGameType()` on the `AnchorStateRegistry` contract. /// Only the deputy guardian can call this function. - /// @param _portal The `OptimismPortal2` contract instance. - /// @param _game The `IDisputeGame` contract instance. - function blacklistDisputeGame(IOptimismPortal2 _portal, IDisputeGame _game) external { + /// @param _anchorStateRegistry The `AnchorStateRegistry` contract instance. + /// @param _gameType The `GameType` to set as the respected game type. + function setRespectedGameType(IAnchorStateRegistry _anchorStateRegistry, GameType _gameType) external { _onlyDeputyGuardian(); - bytes memory data = abi.encodeCall(IOptimismPortal2.blacklistDisputeGame, (_game)); + bytes memory data = abi.encodeCall(IAnchorStateRegistry.setRespectedGameType, (_gameType)); (bool success, bytes memory returnData) = - SAFE.execTransactionFromModuleReturnData(address(_portal), 0, data, Enum.Operation.Call); + SAFE.execTransactionFromModuleReturnData(address(_anchorStateRegistry), 0, data, Enum.Operation.Call); if (!success) { - revert ExecutionFailed(string(returnData)); + revert DeputyGuardianModule_ExecutionFailed(string(returnData)); } - emit DisputeGameBlacklisted(_game); + emit RespectedGameTypeSet(_gameType, Timestamp.wrap(uint64(block.timestamp))); } /// @notice Calls the Security Council Safe's `execTransactionFromModuleReturnData()`, with the arguments - /// necessary to call `setRespectedGameType()` on the `OptimismPortal2` contract. + /// necessary to call `updateRetirementTimestamp()` on the `AnchorStateRegistry` contract. /// Only the deputy guardian can call this function. - /// @param _portal The `OptimismPortal2` contract instance. - /// @param _gameType The `GameType` to set as the respected game type. - function setRespectedGameType(IOptimismPortal2 _portal, GameType _gameType) external { + /// @param _anchorStateRegistry The `AnchorStateRegistry` contract instance. + function updateRetirementTimestamp(IAnchorStateRegistry _anchorStateRegistry) external { _onlyDeputyGuardian(); - bytes memory data = abi.encodeCall(IOptimismPortal2.setRespectedGameType, (_gameType)); + bytes memory data = abi.encodeCall(IAnchorStateRegistry.updateRetirementTimestamp, ()); (bool success, bytes memory returnData) = - SAFE.execTransactionFromModuleReturnData(address(_portal), 0, data, Enum.Operation.Call); + SAFE.execTransactionFromModuleReturnData(address(_anchorStateRegistry), 0, data, Enum.Operation.Call); if (!success) { - revert ExecutionFailed(string(returnData)); + revert DeputyGuardianModule_ExecutionFailed(string(returnData)); } - emit RespectedGameTypeSet(_gameType, Timestamp.wrap(uint64(block.timestamp))); + emit RetirementTimestampUpdated(Timestamp.wrap(uint64(block.timestamp))); } } diff --git a/packages/contracts-bedrock/src/universal/ReinitializableBase.sol b/packages/contracts-bedrock/src/universal/ReinitializableBase.sol new file mode 100644 index 0000000000..056a15986e --- /dev/null +++ b/packages/contracts-bedrock/src/universal/ReinitializableBase.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +/// @title ReinitializableBase +/// @notice A base contract for reinitializable contracts that exposes a version number. +abstract contract ReinitializableBase { + /// @notice Thrown when the initialization version is zero. + error ReinitializableBase_ZeroInitVersion(); + + /// @notice Current initialization version. + uint8 internal immutable INIT_VERSION; + + /// @param _initVersion Current initialization version. + constructor(uint8 _initVersion) { + // Sanity check, we should never have a zero init version. + if (_initVersion == 0) revert ReinitializableBase_ZeroInitVersion(); + INIT_VERSION = _initVersion; + } + + /// @notice Getter for the current initialization version. + /// @return The current initialization version. + function initVersion() public view returns (uint8) { + return INIT_VERSION; + } +} diff --git a/packages/contracts-bedrock/src/universal/WETH98.sol b/packages/contracts-bedrock/src/universal/WETH98.sol index c2909e6fa6..6c066ca7ff 100644 --- a/packages/contracts-bedrock/src/universal/WETH98.sol +++ b/packages/contracts-bedrock/src/universal/WETH98.sol @@ -21,6 +21,7 @@ pragma solidity 0.8.15; /// @title WETH98 /// @notice WETH98 is a version of WETH9 upgraded for Solidity 0.8.x. +/// Some functions were changed to virtual to allow for overriding in other WETH implementations. contract WETH98 { /// @notice Returns the number of decimals the token uses. /// @return The number of decimals the token uses. @@ -113,7 +114,7 @@ contract WETH98 { /// @param guy The address that is approved to transfer the WETH. /// @param wad The amount that is approved to transfer. /// @return True if the approval was successful. - function approve(address guy, uint256 wad) external returns (bool) { + function approve(address guy, uint256 wad) public virtual returns (bool) { _allowance[msg.sender][guy] = wad; emit Approval(msg.sender, guy, wad); return true; diff --git a/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol b/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol new file mode 100644 index 0000000000..962dba0fb7 --- /dev/null +++ b/packages/contracts-bedrock/test/L1/ETHLockbox.t.sol @@ -0,0 +1,528 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing utilities +import { Constants } from "src/libraries/Constants.sol"; + +// Interfaces +import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPortal2.sol"; + +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; + +import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; +import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; +import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; + +// Test +import { CommonTest } from "test/setup/CommonTest.sol"; + +import { ProxyAdmin } from "src/universal/ProxyAdmin.sol"; + +contract ETHLockboxTest is CommonTest { + error InvalidInitialization(); + + event ETHLocked(IOptimismPortal indexed portal, uint256 amount); + event ETHUnlocked(IOptimismPortal indexed portal, uint256 amount); + event PortalAuthorized(IOptimismPortal indexed portal); + event LockboxAuthorized(IETHLockbox indexed lockbox); + event LiquidityMigrated(IETHLockbox indexed lockbox, uint256 amount); + event LiquidityReceived(IETHLockbox indexed lockbox, uint256 amount); + + ProxyAdmin public proxyAdmin; + address public proxyAdminOwner; + + function setUp() public virtual override { + super.setUp(); + + // If not on the last upgrade network, we skip the test since the `ETHLockbox` won't be yet deployed + // TODO(#14691): Remove this check once Upgrade 15 is deployed on Mainnet. + if (isForkTest() && !deploy.cfg().useUpgradedFork()) vm.skip(true); + + proxyAdmin = ProxyAdmin(artifacts.mustGetAddress("ProxyAdmin")); + proxyAdminOwner = proxyAdmin.owner(); + } + + /// @notice Tests the superchain config was correctly set during initialization. + function test_initialization_succeeds() public view { + assertEq(address(ethLockbox.superchainConfig()), address(superchainConfig)); + assertEq(ethLockbox.authorizedPortals(optimismPortal2), true); + } + + /// @notice Tests it reverts when the contract is already initialized. + function test_initialize_alreadyInitialized_reverts() public { + vm.expectRevert("Initializable: contract is already initialized"); + IOptimismPortal2[] memory _portals = new IOptimismPortal2[](1); + ethLockbox.initialize(superchainConfig, _portals); + } + + /// @notice Tests the proxy admin owner is correctly returned. + function test_proxyProxyAdminOwner_succeeds() public view { + assertEq(ethLockbox.proxyAdminOwner(), proxyAdminOwner); + } + + /// @notice Tests the paused status is correctly returned. + function test_paused_succeeds() public { + // Assert the paused status is false + assertEq(ethLockbox.paused(), false); + + // Mock the superchain config to return true for the paused status + vm.mockCall(address(superchainConfig), abi.encodeCall(ISuperchainConfig.paused, ()), abi.encode(true)); + + // Assert the paused status is true + assertEq(ethLockbox.paused(), true); + } + + /// @notice Tests that the version function returns a valid string. We avoid testing the + /// specific value of the string as it changes frequently. + function test_version_succeeds() public view { + assert(bytes(ethLockbox.version()).length > 0); + } + + /// @notice Tests the liquidity is correctly received. + function testFuzz_receiveLiquidity_succeeds(address _lockbox, uint256 _value) public { + // Since on the fork the `_lockbox` fuzzed address doesn't exist, we skip the test + if (isForkTest()) vm.skip(true); + assumeNotForgeAddress(_lockbox); + vm.assume(address(_lockbox) != address(ethLockbox)); + + // Deal the value to the lockbox + deal(address(_lockbox), _value); + + // Mock the admin owner of the lockbox to be the same as the current lockbox proxy admin owner + vm.mockCall( + address(_lockbox), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) + ); + + // Authorize the lockbox if needed + if (!ethLockbox.authorizedLockboxes(IETHLockbox(_lockbox))) { + vm.prank(proxyAdminOwner); + ethLockbox.authorizeLockbox(IETHLockbox(_lockbox)); + } + + // Get the balance of the lockbox before the receive + uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; + + // Expect the `LiquidityReceived` event to be emitted + vm.expectEmit(address(ethLockbox)); + emit LiquidityReceived(IETHLockbox(_lockbox), _value); + + // Call the `receiveLiquidity` function + vm.prank(address(_lockbox)); + ethLockbox.receiveLiquidity{ value: _value }(); + + // Assert the lockbox's balance increased by the amount received + assertEq(address(ethLockbox).balance, ethLockboxBalanceBefore + _value); + } + + /// @notice Tests it reverts when the caller is not an authorized portal. + function testFuzz_lockETH_unauthorizedPortal_reverts(address _caller) public { + vm.assume(!ethLockbox.authorizedPortals(IOptimismPortal2(payable(_caller)))); + + // Expect the revert with `Unauthorized` selector + vm.expectRevert(IETHLockbox.ETHLockbox_Unauthorized.selector); + + // Call the `lockETH` function with an unauthorized caller + vm.prank(_caller); + ethLockbox.lockETH(); + } + + /// @notice Tests the ETH is correctly locked when the caller is an authorized portal. + function testFuzz_lockETH_succeeds(uint256 _amount) public { + // Prevent overflow on an upgrade context + _amount = bound(_amount, 0, type(uint256).max - address(ethLockbox).balance); + + // Deal the ETH amount to the portal + vm.deal(address(optimismPortal2), _amount); + + // Get the balance of the portal and lockbox before the lock to compare later on the assertions + uint256 portalBalanceBefore = address(optimismPortal2).balance; + uint256 lockboxBalanceBefore = address(ethLockbox).balance; + + // Look for the emit of the `ETHLocked` event + vm.expectEmit(address(ethLockbox)); + emit ETHLocked(optimismPortal2, _amount); + + // Call the `lockETH` function with the portal + vm.prank(address(optimismPortal2)); + ethLockbox.lockETH{ value: _amount }(); + + // Assert the portal's balance decreased and the lockbox's balance increased by the amount locked + assertEq(address(optimismPortal2).balance, portalBalanceBefore - _amount); + assertEq(address(ethLockbox).balance, lockboxBalanceBefore + _amount); + } + + /// @notice Tests the ETH is correctly locked when the caller is an authorized portal with different portals. + function testFuzz_lockETH_multiplePortals_succeeds(IOptimismPortal2 _portal, uint256 _amount) public { + // Since on the fork the `_portal` fuzzed address doesn't exist, we skip the test + if (isForkTest()) vm.skip(true); + assumeNotForgeAddress(address(_portal)); + vm.assume(address(_portal) != address(ethLockbox)); + + // Mock the admin owner of the portal to be the same as the current lockbox proxy admin owner + vm.mockCall( + address(_portal), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) + ); + + // Mock the SuperchainConfig on the portal to be the same as the SuperchainConfig on the + // lockbox. + vm.mockCall( + address(_portal), abi.encodeCall(IOptimismPortal.superchainConfig, ()), abi.encode(superchainConfig) + ); + + // Set the portal as an authorized portal if needed + if (!ethLockbox.authorizedPortals(_portal)) { + vm.prank(proxyAdminOwner); + ethLockbox.authorizePortal(_portal); + } + + // Deal the ETH amount to the portal + vm.deal(address(_portal), _amount); + + // Get the balance of the lockbox before the lock to compare later on the assertions + uint256 lockboxBalanceBefore = address(ethLockbox).balance; + + // Look for the emit of the `ETHLocked` event + vm.expectEmit(address(ethLockbox)); + emit ETHLocked(_portal, _amount); + + // Call the `lockETH` function with the portal + vm.prank(address(_portal)); + ethLockbox.lockETH{ value: _amount }(); + + // Assert the portal's balance decreased and the lockbox's balance increased by the amount locked + assertEq(address(ethLockbox).balance, lockboxBalanceBefore + _amount); + } + + /// @notice Tests `unlockETH` reverts when the contract is paused. + function testFuzz_unlockETH_paused_reverts(address _caller, uint256 _value) public { + // Mock the superchain config to return true for the paused status + vm.mockCall(address(superchainConfig), abi.encodeCall(ISuperchainConfig.paused, ()), abi.encode(true)); + + // Expect the revert with `Paused` selector + vm.expectRevert(IETHLockbox.ETHLockbox_Paused.selector); + + // Call the `unlockETH` function with the caller + vm.prank(_caller); + ethLockbox.unlockETH(_value); + } + + /// @notice Tests it reverts when the caller is not an authorized portal. + function testFuzz_unlockETH_unauthorizedPortal_reverts(address _caller, uint256 _value) public { + vm.assume(!ethLockbox.authorizedPortals(IOptimismPortal2(payable(_caller)))); + + // Expect the revert with `Unauthorized` selector + vm.expectRevert(IETHLockbox.ETHLockbox_Unauthorized.selector); + + // Call the `unlockETH` function with an unauthorized caller + vm.prank(_caller); + ethLockbox.unlockETH(_value); + } + + /// @notice Tests `unlockETH` reverts when the `_value` input is greater than the balance of the lockbox. + function testFuzz_unlockETH_insufficientBalance_reverts(uint256 _value) public { + _value = bound(_value, address(ethLockbox).balance + 1, type(uint256).max); + + // Expect the revert with `InsufficientBalance` selector + vm.expectRevert(IETHLockbox.ETHLockbox_InsufficientBalance.selector); + + // Call the `unlockETH` function with the portal + vm.prank(address(optimismPortal2)); + ethLockbox.unlockETH(_value); + } + + /// @notice Tests `unlockETH` reverts when the portal is not the L2 sender to prevent unlocking ETH from the lockbox + /// through a withdrawal transaction. + function testFuzz_unlockETH_withdrawalTransaction_reverts(uint256 _value, address _l2Sender) public { + _value = bound(_value, 0, address(ethLockbox).balance); + vm.assume(_l2Sender != Constants.DEFAULT_L2_SENDER); + + // Mock the L2 sender + vm.mockCall(address(optimismPortal2), abi.encodeCall(IOptimismPortal.l2Sender, ()), abi.encode(_l2Sender)); + + // Expect the revert with `NoWithdrawalTransactions` selector + vm.expectRevert(IETHLockbox.ETHLockbox_NoWithdrawalTransactions.selector); + + // Call the `unlockETH` function with the portal + vm.prank(address(optimismPortal2)); + ethLockbox.unlockETH(_value); + } + + /// @notice Tests the ETH is correctly unlocked when the caller is an authorized portal. + function testFuzz_unlockETH_succeeds(uint256 _value) public { + // Deal the ETH amount to the lockbox + vm.deal(address(ethLockbox), _value); + + // Get the balance of the portal and lockbox before the unlock to compare later on the assertions + uint256 portalBalanceBefore = address(optimismPortal2).balance; + uint256 lockboxBalanceBefore = address(ethLockbox).balance; + + // Expect `donateETH` function to be called on Portal + vm.expectCall(address(optimismPortal2), abi.encodeCall(IOptimismPortal.donateETH, ())); + + // Look for the emit of the `ETHUnlocked` event + vm.expectEmit(address(ethLockbox)); + emit ETHUnlocked(optimismPortal2, _value); + + // Call the `unlockETH` function with the portal + vm.prank(address(optimismPortal2)); + ethLockbox.unlockETH(_value); + + // Assert the portal's balance increased and the lockbox's balance decreased by the amount unlocked + assertEq(address(optimismPortal2).balance, portalBalanceBefore + _value); + assertEq(address(ethLockbox).balance, lockboxBalanceBefore - _value); + } + + /// @notice Tests the ETH is correctly unlocked when the caller is an authorized portal. + function testFuzz_unlockETH_multiplePortals_succeeds(IOptimismPortal2 _portal, uint256 _value) public { + assumeNotForgeAddress(address(_portal)); + + // Mock the admin owner of the portal to be the same as the current lockbox proxy admin owner + vm.mockCall( + address(_portal), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) + ); + + // Mock the SuperchainConfig on the portal to be the same as the SuperchainConfig on the + // lockbox. + vm.mockCall( + address(_portal), abi.encodeCall(IOptimismPortal.superchainConfig, ()), abi.encode(superchainConfig) + ); + + // Set the portal as an authorized portal if needed + if (!ethLockbox.authorizedPortals(_portal)) { + vm.prank(proxyAdminOwner); + ethLockbox.authorizePortal(_portal); + } + + // Deal the ETH amount to the lockbox + vm.deal(address(ethLockbox), _value); + + // Get the balance of the portal and lockbox before the unlock to compare later on the assertions + uint256 portalBalanceBefore = address(optimismPortal2).balance; + uint256 lockboxBalanceBefore = address(ethLockbox).balance; + + // Expect `donateETH` function to be called on Portal + vm.expectCall(address(optimismPortal2), abi.encodeCall(IOptimismPortal.donateETH, ())); + + // Look for the emit of the `ETHUnlocked` event + vm.expectEmit(address(ethLockbox)); + emit ETHUnlocked(optimismPortal2, _value); + + // Call the `unlockETH` function with the portal + vm.prank(address(optimismPortal2)); + ethLockbox.unlockETH(_value); + + // Assert the portal's balance increased and the lockbox's balance decreased by the amount unlocked + assertEq(address(optimismPortal2).balance, portalBalanceBefore + _value); + assertEq(address(ethLockbox).balance, lockboxBalanceBefore - _value); + } + + /// @notice Tests the `authorizePortal` function reverts when the caller is not the proxy admin. + function testFuzz_authorizePortal_unauthorized_reverts(address _caller) public { + vm.assume(_caller != proxyAdminOwner); + + // Expect the revert with `Unauthorized` selector + vm.expectRevert(IETHLockbox.ETHLockbox_Unauthorized.selector); + + // Call the `authorizePortal` function with an unauthorized caller + vm.prank(_caller); + ethLockbox.authorizePortal(optimismPortal2); + } + + /// @notice Tests the `authorizePortal` function reverts when the proxy admin owner of the portal is not the same as + /// the one of the lockbox. + function testFuzz_authorizePortal_differentProxyAdminOwner_reverts(IOptimismPortal2 _portal) public { + assumeNotForgeAddress(address(_portal)); + vm.mockCall(address(_portal), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(address(0))); + + // Expect the revert with `DifferentOwner` selector + vm.expectRevert(IETHLockbox.ETHLockbox_DifferentProxyAdminOwner.selector); + + // Call the `authorizePortal` function + vm.prank(proxyAdminOwner); + ethLockbox.authorizePortal(_portal); + } + + /// @notice Tests the authorizePortal function reverts when the portal has a different + /// SuperchainConfig than the one configured in the lockbox. + /// @param _portal The portal to authorize. + function testFuzz_authorizePortal_differentSuperchainConfig_reverts(IOptimismPortal2 _portal) public { + assumeNotForgeAddress(address(_portal)); + + // Mock the portal to have the right proxyAdminOwner. + vm.mockCall( + address(_portal), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) + ); + + // Mock the portal to have the wrong SuperchainConfig. + vm.mockCall(address(_portal), abi.encodeCall(IOptimismPortal.superchainConfig, ()), abi.encode(address(0))); + + // Expect the revert with `DifferentSuperchainConfig` selector + vm.expectRevert(IETHLockbox.ETHLockbox_DifferentSuperchainConfig.selector); + + // Call the `authorizePortal` function + vm.prank(proxyAdminOwner); + ethLockbox.authorizePortal(_portal); + } + + /// @notice Tests the `authorizeLockbox` function succeeds using the `optimismPortal2` address as the portal. + function test_authorizePortal_succeeds() public { + // Calculate the correct storage slot for the mapping value + bytes32 mappingSlot = bytes32(uint256(1)); // position on the layout + address key = address(optimismPortal2); + bytes32 slot = keccak256(abi.encode(key, mappingSlot)); + + // Reset the authorization status to false + vm.store(address(ethLockbox), slot, bytes32(0)); + + // Expect the `PortalAuthorized` event to be emitted + vm.expectEmit(address(ethLockbox)); + emit PortalAuthorized(optimismPortal2); + + // Call the `authorizePortal` function with the portal + vm.prank(proxyAdminOwner); + ethLockbox.authorizePortal(optimismPortal2); + + // Assert the portal is authorized + assertTrue(ethLockbox.authorizedPortals(optimismPortal2)); + } + + /// @notice Tests the `authorizeLockbox` function succeeds + function testFuzz_authorizePortal_succeeds(IOptimismPortal2 _portal) public { + assumeNotForgeAddress(address(_portal)); + + // Mock the admin owner of the portal to be the same as the current lockbox proxy admin owner + vm.mockCall( + address(_portal), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) + ); + + // Mock the SuperchainConfig on the portal to be the same as the SuperchainConfig on the + // Lockbox. + vm.mockCall( + address(_portal), abi.encodeCall(IOptimismPortal.superchainConfig, ()), abi.encode(superchainConfig) + ); + + // Expect the `PortalAuthorized` event to be emitted + vm.expectEmit(address(ethLockbox)); + emit PortalAuthorized(_portal); + + // Call the `authorizePortal` function with the portal + vm.prank(proxyAdminOwner); + ethLockbox.authorizePortal(_portal); + + // Assert the portal is authorized + assertTrue(ethLockbox.authorizedPortals(_portal)); + } + + /// @notice Tests the `authorizeLockbox` function reverts when the caller is not the proxy admin. + function testFuzz_authorizeLockbox_unauthorized_reverts(address _caller) public { + vm.assume(_caller != proxyAdminOwner); + + // Expect the revert with `Unauthorized` selector + vm.expectRevert(IETHLockbox.ETHLockbox_Unauthorized.selector); + + // Call the `authorizeLockbox` function with an unauthorized caller + vm.prank(_caller); + ethLockbox.authorizeLockbox(ethLockbox); + } + + /// @notice Tests the `authorizeLockbox` function reverts when the proxy admin owner of the lockbox is not the same + /// as the proxy admin owner of + /// the proxy admin. + function testFuzz_authorizeLockbox_differentProxyAdminOwner_reverts(address _lockbox) public { + assumeNotForgeAddress(_lockbox); + + vm.mockCall(address(_lockbox), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(address(0))); + + // Expect the revert with `ETHLockbox_DifferentProxyAdminOwner` selector + vm.expectRevert(IETHLockbox.ETHLockbox_DifferentProxyAdminOwner.selector); + + // Call the `authorizeLockbox` function with the lockbox + vm.prank(proxyAdminOwner); + ethLockbox.authorizeLockbox(IETHLockbox(_lockbox)); + } + + /// @notice Tests the `authorizeLockbox` function succeeds + function testFuzz_authorizeLockbox_succeeds(address _lockbox) public { + assumeNotForgeAddress(_lockbox); + + // Mock the admin owner of the lockbox to be the same as the current lockbox proxy admin owner + vm.mockCall( + address(_lockbox), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) + ); + + // Expect the `LockboxAuthorized` event to be emitted + vm.expectEmit(address(ethLockbox)); + emit LockboxAuthorized(IETHLockbox(_lockbox)); + + // Authorize the lockbox + vm.prank(proxyAdminOwner); + ethLockbox.authorizeLockbox(IETHLockbox(_lockbox)); + + // Assert the lockbox is authorized + assertTrue(ethLockbox.authorizedLockboxes(IETHLockbox(_lockbox))); + } + + /// @notice Tests the `migrateLiquidity` function reverts when the caller is not the proxy admin. + function testFuzz_migrateLiquidity_unauthorized_reverts(address _caller) public { + vm.assume(_caller != proxyAdminOwner); + + // Expect the revert with `Unauthorized` selector + vm.expectRevert(IETHLockbox.ETHLockbox_Unauthorized.selector); + + // Call the `migrateLiquidity` function with an unauthorized caller + vm.prank(_caller); + ethLockbox.migrateLiquidity(ethLockbox); + } + + /// @notice Tests the `migrateLiquidity` function reverts when the proxy admin owner of the lockbox is not the same + /// as the proxy admin owner of + /// the proxy admin. + function testFuzz_migrateLiquidity_differentProxyAdminOwner_reverts(address _lockbox) public { + assumeNotForgeAddress(_lockbox); + + vm.mockCall(address(_lockbox), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(address(0))); + + // Expect the revert with `ETHLockbox_DifferentProxyAdminOwner` selector + vm.expectRevert(IETHLockbox.ETHLockbox_DifferentProxyAdminOwner.selector); + + // Call the `migrateLiquidity` function with the lockbox + vm.prank(proxyAdminOwner); + ethLockbox.migrateLiquidity(IETHLockbox(_lockbox)); + } + + /// @notice Tests the `migrateLiquidity` function succeeds + function testFuzz_migrateLiquidity_succeeds(uint256 _balance, address _lockbox) public { + _balance = bound(_balance, 0, type(uint256).max - address(ethLockbox).balance); + + // Since on the fork the `_lockbox` fuzzed address doesn't exist, we skip the test + if (isForkTest()) vm.skip(true); + assumeNotForgeAddress(_lockbox); + vm.assume(address(_lockbox) != address(ethLockbox)); + + // Mock on the lockbox that will receive the migration for it to succeed + vm.mockCall( + address(_lockbox), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) + ); + vm.mockCall(address(_lockbox), abi.encodeCall(IETHLockbox.authorizedLockboxes, (ethLockbox)), abi.encode(true)); + vm.mockCall(address(_lockbox), abi.encodeCall(IETHLockbox.receiveLiquidity, ()), abi.encode(true)); + + // Deal the balance to the lockbox + deal(address(_lockbox), _balance); + + // Get balances before the migration + uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; + uint256 newLockboxBalanceBefore = address(_lockbox).balance; + + // Expect the `LiquidityMigrated` event to be emitted + vm.expectEmit(address(ethLockbox)); + emit LiquidityMigrated(IETHLockbox(_lockbox), ethLockboxBalanceBefore); + + // Call the `migrateLiquidity` function with the lockbox + vm.prank(proxyAdminOwner); + ethLockbox.migrateLiquidity(IETHLockbox(_lockbox)); + + // Assert the liquidity was migrated + assertEq(address(ethLockbox).balance, 0); + assertEq(address(_lockbox).balance, newLockboxBalanceBefore + ethLockboxBalanceBefore); + } +} diff --git a/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol b/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol index 093c87c5e1..8776c0f7a8 100644 --- a/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol +++ b/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol @@ -163,7 +163,8 @@ contract L1StandardBridge_Initialize_TestFail is CommonTest { } contract L1StandardBridge_Receive_Test is CommonTest { /// @dev Tests receive bridges ETH successfully. function test_receive_succeeds() external { - uint256 balanceBefore = address(optimismPortal2).balance; + uint256 portalBalanceBefore = address(optimismPortal2).balance; + uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; // The legacy event must be emitted for backwards compatibility vm.expectEmit(address(l1StandardBridge)); @@ -187,7 +188,8 @@ contract L1StandardBridge_Receive_Test is CommonTest { vm.prank(alice, alice); (bool success,) = address(l1StandardBridge).call{ value: 100 }(hex""); assertEq(success, true); - assertEq(address(optimismPortal2).balance, balanceBefore + 100); + assertEq(address(optimismPortal2).balance, portalBalanceBefore); + assertEq(address(ethLockbox).balance, ethLockboxBalanceBefore + 100); } } @@ -196,7 +198,7 @@ contract PreBridgeETH is CommonTest { /// on whether the bridge call is legacy or not. function _preBridgeETH(bool isLegacy, uint256 value) internal { if (!isForkTest()) { - assertEq(address(optimismPortal2).balance, 0); + assertEq(address(optimismPortal2).balance, 0, "OptimismPortal2 balance should be 0"); } uint256 nonce = l1CrossDomainMessenger.messageNonce(); uint256 version = 0; // Internal constant in the OptimismPortal: DEPOSIT_VERSION @@ -266,9 +268,11 @@ contract L1StandardBridge_DepositETH_Test is PreBridgeETH { /// ETH ends up in the optimismPortal. function test_depositETH_fromEOA_succeeds() external { _preBridgeETH({ isLegacy: true, value: 500 }); - uint256 balanceBefore = address(optimismPortal2).balance; + uint256 portalBalanceBefore = address(optimismPortal2).balance; + uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; l1StandardBridge.depositETH{ value: 500 }(50000, hex"dead"); - assertEq(address(optimismPortal2).balance, balanceBefore + 500); + assertEq(address(optimismPortal2).balance, portalBalanceBefore); + assertEq(address(ethLockbox).balance, ethLockboxBalanceBefore + 500); } /// @dev Tests that depositing ETH succeeds for an EOA using 7702 delegation. @@ -277,9 +281,11 @@ contract L1StandardBridge_DepositETH_Test is PreBridgeETH { vm.etch(alice, abi.encodePacked(hex"EF0100", address(0))); _preBridgeETH({ isLegacy: true, value: 500 }); - uint256 balanceBefore = address(optimismPortal2).balance; + uint256 portalBalanceBefore = address(optimismPortal2).balance; + uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; l1StandardBridge.depositETH{ value: 500 }(50000, hex"dead"); - assertEq(address(optimismPortal2).balance, balanceBefore + 500); + assertEq(address(optimismPortal2).balance, portalBalanceBefore); + assertEq(address(ethLockbox).balance, ethLockboxBalanceBefore + 500); } } @@ -301,9 +307,11 @@ contract L1StandardBridge_BridgeETH_Test is PreBridgeETH { /// ETH ends up in the optimismPortal. function test_bridgeETH_succeeds() external { _preBridgeETH({ isLegacy: false, value: 500 }); - uint256 balanceBefore = address(optimismPortal2).balance; + uint256 portalBalanceBefore = address(optimismPortal2).balance; + uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; l1StandardBridge.bridgeETH{ value: 500 }(50000, hex"dead"); - assertEq(address(optimismPortal2).balance, balanceBefore + 500); + assertEq(address(optimismPortal2).balance, portalBalanceBefore); + assertEq(address(ethLockbox).balance, ethLockboxBalanceBefore + 500); } } @@ -381,9 +389,11 @@ contract L1StandardBridge_DepositETHTo_Test is PreBridgeETHTo { /// ETH ends up in the optimismPortal. function test_depositETHTo_succeeds() external { _preBridgeETHTo({ isLegacy: true, value: 600 }); - uint256 balanceBefore = address(optimismPortal2).balance; + uint256 portalBalanceBefore = address(optimismPortal2).balance; + uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; l1StandardBridge.depositETHTo{ value: 600 }(bob, 60000, hex"dead"); - assertEq(address(optimismPortal2).balance, balanceBefore + 600); + assertEq(address(optimismPortal2).balance, portalBalanceBefore); + assertEq(address(ethLockbox).balance, ethLockboxBalanceBefore + 600); } } @@ -395,9 +405,11 @@ contract L1StandardBridge_BridgeETHTo_Test is PreBridgeETHTo { /// ETH ends up in the optimismPortal. function test_bridgeETHTo_succeeds() external { _preBridgeETHTo({ isLegacy: false, value: 600 }); - uint256 balanceBefore = address(optimismPortal2).balance; + uint256 portalBalanceBefore = address(optimismPortal2).balance; + uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; l1StandardBridge.bridgeETHTo{ value: 600 }(bob, 60000, hex"dead"); - assertEq(address(optimismPortal2).balance, balanceBefore + 600); + assertEq(address(optimismPortal2).balance, portalBalanceBefore); + assertEq(address(ethLockbox).balance, ethLockboxBalanceBefore + 600); } } diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index 6f742180c6..ee8bbb7c84 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -27,24 +27,27 @@ import { IOptimismMintableERC20Factory } from "interfaces/universal/IOptimismMin import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.sol"; import { IMIPS } from "interfaces/cannon/IMIPS.sol"; import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; +import { IProxy } from "interfaces/universal/IProxy.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; import { IPreimageOracle } from "interfaces/cannon/IPreimageOracle.sol"; +import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { IPermissionedDisputeGame } from "interfaces/dispute/IPermissionedDisputeGame.sol"; import { IDelayedWETH } from "interfaces/dispute/IDelayedWETH.sol"; import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; -import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IOPContractsManager, + IOPCMImplementationsWithoutLockbox, IOPContractsManagerGameTypeAdder, IOPContractsManagerDeployer, IOPContractsManagerUpgrader, IOPContractsManagerContractsContainer } from "interfaces/L1/IOPContractsManager.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; // Contracts import { @@ -57,7 +60,7 @@ import { import { Blueprint } from "src/libraries/Blueprint.sol"; import { IBigStepper } from "interfaces/dispute/IBigStepper.sol"; import { GameType, Duration, Hash, Claim } from "src/dispute/lib/LibUDT.sol"; -import { OutputRoot, GameTypes } from "src/dispute/lib/Types.sol"; +import { Proposal, GameTypes } from "src/dispute/lib/Types.sol"; // Exposes internal functions for testing. contract OPContractsManager_Harness is OPContractsManager { @@ -242,6 +245,7 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { address upgrader; IOPContractsManager.OpChainConfig[] opChainConfigs; Claim absolutePrestate; + string public opChain = vm.envOr("FORK_OP_CHAIN", string("op")); function setUp() public virtual override { super.disableUpgradedFork(); @@ -288,10 +292,26 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { emit Upgraded(impl); } - function runV200UpgradeAndChecks(address _delegateCaller) public { + function runUpgrade13UpgradeAndChecks(address _delegateCaller) public { // The address below corresponds with the address of the v2.0.0-rc.1 OPCM on mainnet. - IOPContractsManager deployedOPCM = IOPContractsManager(address(0x026b2F158255Beac46c1E7c6b8BbF29A4b6A7B76)); - IOPContractsManager.Implementations memory impls = deployedOPCM.implementations(); + address OPCM_ADDRESS = 0x026b2F158255Beac46c1E7c6b8BbF29A4b6A7B76; + + IOPContractsManager deployedOPCM = IOPContractsManager(OPCM_ADDRESS); + IOPCMImplementationsWithoutLockbox.Implementations memory impls = + IOPCMImplementationsWithoutLockbox(address(deployedOPCM)).implementations(); + + address mainnetPAO = artifacts.mustGetAddress("SuperchainConfigProxy"); + + // If the delegate caller is not the mainnet PAO, we need to call upgrade as the mainnet PAO first. + if (_delegateCaller != mainnetPAO) { + IOPContractsManager.OpChainConfig[] memory opmChain = new IOPContractsManager.OpChainConfig[](0); + ISuperchainConfig superchainConfig = ISuperchainConfig(mainnetPAO); + + address opmUpgrader = IProxyAdmin(EIP1967Helper.getAdmin(address(superchainConfig))).owner(); + vm.etch(opmUpgrader, vm.getDeployedCode("test/mocks/Callers.sol:DelegateCaller")); + + DelegateCaller(opmUpgrader).dcForward(OPCM_ADDRESS, abi.encodeCall(IOPContractsManager.upgrade, (opmChain))); + } // Cache the old L1xDM address so we can look for it in the AddressManager's event address oldL1CrossDomainMessenger = addressManager.getAddress("OVM_L1CrossDomainMessenger"); @@ -340,6 +360,7 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { vm.expectEmit(false, true, true, true, address(disputeGameFactory)); emit ImplementationSet(address(0), GameTypes.CANNON); } + vm.expectEmit(address(_delegateCaller)); emit Upgraded(l2ChainId, opChainConfigs[0].systemConfigProxy, address(_delegateCaller)); @@ -396,7 +417,24 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { } function runUpgrade14UpgradeAndChecks(address _delegateCaller) public { - IOPContractsManager.Implementations memory impls = opcm.implementations(); + address OPCM_ADDRESS = 0x3A1f523a4bc09cd344A2745a108Bb0398288094F; + + IOPContractsManager deployedOPCM = IOPContractsManager(OPCM_ADDRESS); + IOPCMImplementationsWithoutLockbox.Implementations memory impls = + IOPCMImplementationsWithoutLockbox(address(deployedOPCM)).implementations(); + + address mainnetPAO = artifacts.mustGetAddress("SuperchainConfigProxy"); + + // If the delegate caller is not the mainnet PAO, we need to call upgrade as the mainnet PAO first. + if (_delegateCaller != mainnetPAO) { + IOPContractsManager.OpChainConfig[] memory opmChain = new IOPContractsManager.OpChainConfig[](0); + ISuperchainConfig superchainConfig = ISuperchainConfig(mainnetPAO); + + address opmUpgrader = IProxyAdmin(EIP1967Helper.getAdmin(address(superchainConfig))).owner(); + vm.etch(opmUpgrader, vm.getDeployedCode("test/mocks/Callers.sol:DelegateCaller")); + + DelegateCaller(opmUpgrader).dcForward(OPCM_ADDRESS, abi.encodeCall(IOPContractsManager.upgrade, (opmChain))); + } // sanity check IPermissionedDisputeGame oldPDG = @@ -427,7 +465,7 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { vm.etch(_delegateCaller, vm.getDeployedCode("test/mocks/Callers.sol:DelegateCaller")); DelegateCaller(_delegateCaller).dcForward( - address(opcm), abi.encodeCall(IOPContractsManager.upgrade, (opChainConfigs)) + address(deployedOPCM), abi.encodeCall(IOPContractsManager.upgrade, (opChainConfigs)) ); VmSafe.Gas memory gas = vm.lastCallGas(); @@ -458,19 +496,138 @@ contract OPContractsManager_Upgrade_Harness is CommonTest { } } + function runUpgrade15UpgradeAndChecks(address _delegateCaller) public { + IOPContractsManager.Implementations memory impls = opcm.implementations(); + + // Predict the address of the new AnchorStateRegistry proxy. + // Subcontext to avoid stack too deep. + address newAsrProxy; + { + // Compute the salt using the system config address. + bytes32 salt = keccak256( + abi.encode( + l2ChainId, + string.concat(string(bytes.concat(bytes32(uint256(uint160(address(systemConfig))))))), + "AnchorStateRegistry-SOT" + ) + ); + + // Use the actual proxy instead of the local code so we can reuse this test. + address proxyBp = opcm.blueprints().proxy; + Blueprint.Preamble memory preamble = Blueprint.parseBlueprintPreamble(proxyBp.code); + bytes memory initCode = bytes.concat(preamble.initcode, abi.encode(proxyAdmin)); + newAsrProxy = vm.computeCreate2Address(salt, keccak256(initCode), _delegateCaller); + vm.label(newAsrProxy, "NewAnchorStateRegistryProxy"); + } + + // Grab the PermissionedDisputeGame and FaultDisputeGame implementations before upgrade. + address oldPDGImpl = address(disputeGameFactory.gameImpls(GameTypes.PERMISSIONED_CANNON)); + address oldFDGImpl = address(disputeGameFactory.gameImpls(GameTypes.CANNON)); + IPermissionedDisputeGame oldPDG = IPermissionedDisputeGame(oldPDGImpl); + IFaultDisputeGame oldFDG = IFaultDisputeGame(oldFDGImpl); + + // Expect the SystemConfig and OptimismPortal to be upgraded. + expectEmitUpgraded(impls.systemConfigImpl, address(systemConfig)); + expectEmitUpgraded(impls.optimismPortalImpl, address(optimismPortal2)); + + // We always expect the PermissionedDisputeGame to be deployed. We don't yet know the + // address of the new permissionedGame which will be deployed by the + // OPContractsManager.upgrade() call, so ignore the first topic. + vm.expectEmit(false, true, true, true, address(disputeGameFactory)); + emit ImplementationSet(address(0), GameTypes.PERMISSIONED_CANNON); + + // If the old FaultDisputeGame exists, we expect it to be upgraded. + if (address(oldFDG) != address(0)) { + // Ignore the first topic for the same reason as the previous comment. + vm.expectEmit(false, true, true, true, address(disputeGameFactory)); + emit ImplementationSet(address(0), GameTypes.CANNON); + } + + vm.expectEmit(address(_delegateCaller)); + emit Upgraded(l2ChainId, systemConfig, address(_delegateCaller)); + + // Temporarily replace the upgrader with a DelegateCaller so we can test the upgrade, + // then reset its code to the original code. + bytes memory delegateCallerCode = address(_delegateCaller).code; + vm.etch(_delegateCaller, vm.getDeployedCode("test/mocks/Callers.sol:DelegateCaller")); + + // Execute the upgrade. + // We use the new format here, not the legacy one. + DelegateCaller(_delegateCaller).dcForward( + address(opcm), abi.encodeCall(IOPContractsManager.upgrade, (opChainConfigs)) + ); + + // Less than 90% of the gas target of 20M to account for the gas used by using Safe. + VmSafe.Gas memory gas = vm.lastCallGas(); + assertLt(gas.gasTotalUsed, 0.9 * 20_000_000, "Upgrade exceeds gas target of 15M"); + + // Reset the upgrader's code to the original code. + vm.etch(_delegateCaller, delegateCallerCode); + + // Grab the new implementations. + address newPDGImpl = address(disputeGameFactory.gameImpls(GameTypes.PERMISSIONED_CANNON)); + IPermissionedDisputeGame pdg = IPermissionedDisputeGame(newPDGImpl); + address newFDGImpl = address(disputeGameFactory.gameImpls(GameTypes.CANNON)); + IFaultDisputeGame fdg = IFaultDisputeGame(newFDGImpl); + + // Check that the PermissionedDisputeGame is upgraded to the expected version, references + // the correct anchor state and has the mipsImpl. Although Upgrade 15 doesn't actually + // change any of this, we might as well check it again. + assertEq(ISemver(address(pdg)).version(), "1.5.0"); + assertEq(address(pdg.vm()), impls.mipsImpl); + assertEq(pdg.l2ChainId(), oldPDG.l2ChainId()); + + // If the old FaultDisputeGame exists, we expect it to be upgraded. Check same as above. + if (address(oldFDG) != address(0)) { + assertEq(ISemver(address(fdg)).version(), "1.5.0"); + assertEq(address(fdg.vm()), impls.mipsImpl); + assertEq(fdg.l2ChainId(), oldFDG.l2ChainId()); + } + + // 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.0.0"); + assertEq(impls.systemConfigImpl, EIP1967Helper.getImplementation(address(systemConfig))); + assertEq(systemConfig.l2ChainId(), l2ChainId); + DeployUtils.assertInitialized({ _contractAddress: address(systemConfig), _isProxy: true, _slot: 0, _offset: 0 }); + + // Make sure that the OptimismPortal is upgraded to the right version. It must also have a + // reference to the new AnchorStateRegistry. + assertEq(ISemver(address(optimismPortal2)).version(), "4.1.0"); + assertEq(impls.optimismPortalImpl, EIP1967Helper.getImplementation(address(optimismPortal2))); + assertEq(address(optimismPortal2.anchorStateRegistry()), address(newAsrProxy)); + DeployUtils.assertInitialized({ + _contractAddress: address(optimismPortal2), + _isProxy: true, + _slot: 0, + _offset: 0 + }); + + // Make sure the new AnchorStateRegistry has the right version and is initialized. + assertEq(ISemver(address(newAsrProxy)).version(), "3.1.0"); + vm.prank(address(proxyAdmin)); + assertEq(IProxy(payable(newAsrProxy)).admin(), address(proxyAdmin)); + DeployUtils.assertInitialized({ _contractAddress: address(newAsrProxy), _isProxy: true, _slot: 0, _offset: 0 }); + } + function runUpgradeTestAndChecks(address _delegateCaller) public { - runV200UpgradeAndChecks(_delegateCaller); + // TODO(#14691): Remove this function once Upgrade 15 is deployed on Mainnet. + runUpgrade13UpgradeAndChecks(_delegateCaller); + // TODO(#14691): Remove this function once Upgrade 15 is deployed on Mainnet. runUpgrade14UpgradeAndChecks(_delegateCaller); + runUpgrade15UpgradeAndChecks(_delegateCaller); } } contract OPContractsManager_Upgrade_Test is OPContractsManager_Upgrade_Harness { function test_upgradeOPChainOnly_succeeds() public { + skipIfNotOpFork("test_upgradeOPChainOnly_succeeds"); // Run the upgrade test and checks runUpgradeTestAndChecks(upgrader); } function test_isRcFalseAfterCalledByUpgrader_works() public { + skipIfNotOpFork("test_isRcFalseAfterCalledByUpgrader_works"); assertTrue(opcm.isRC()); bytes memory releaseBytes = bytes(opcm.l1ContractsRelease()); assertEq(Bytes.slice(releaseBytes, releaseBytes.length - 3, 3), "-rc", "release should end with '-rc'"); @@ -487,6 +644,7 @@ contract OPContractsManager_Upgrade_Test is OPContractsManager_Upgrade_Harness { ) public { + skipIfNotOpFork("testFuzz_upgrade_nonUpgradeControllerDelegatecallerShouldNotSetIsRCToFalse_works"); if ( _nonUpgradeController == upgrader || _nonUpgradeController == address(0) || _nonUpgradeController < address(0x4200000000000000000000000000000000000000) @@ -515,6 +673,8 @@ contract OPContractsManager_Upgrade_Test is OPContractsManager_Upgrade_Harness { } function test_upgrade_duplicateL2ChainId_succeeds() public { + skipIfNotOpFork("test_upgrade_duplicateL2ChainId_succeeds"); + // Deploy a new OPChain with the same L2 chain ID as the current OPChain Deploy deploy = Deploy(address(uint160(uint256(keccak256(abi.encode("optimism.deploy")))))); IOPContractsManager.DeployInput memory deployInput = deploy.getDeployInput(); @@ -525,13 +685,87 @@ contract OPContractsManager_Upgrade_Test is OPContractsManager_Upgrade_Harness { // Try to upgrade the current OPChain runUpgradeTestAndChecks(upgrader); } + + /// @notice Tests that the absolute prestate can be overridden using the upgrade config. + function test_upgrade_absolutePrestateOverride_succeeds() public { + // Run Upgrade 13 and 14 to get us to a state where we can run Upgrade 15. + // Can remove these two calls as Upgrade 13 and 14 are executed in prod. + runUpgrade13UpgradeAndChecks(upgrader); + runUpgrade14UpgradeAndChecks(upgrader); + + // Get the pdg and fdg before the upgrade + Claim pdgPrestateBefore = IPermissionedDisputeGame( + address(disputeGameFactory.gameImpls(GameTypes.PERMISSIONED_CANNON)) + ).absolutePrestate(); + Claim fdgPrestateBefore = + IFaultDisputeGame(address(disputeGameFactory.gameImpls(GameTypes.CANNON))).absolutePrestate(); + + // Assert that the prestate is not zero. + assertNotEq(pdgPrestateBefore.raw(), bytes32(0)); + assertNotEq(fdgPrestateBefore.raw(), bytes32(0)); + + // Set the absolute prestate input to something non-zero. + opChainConfigs[0].absolutePrestate = Claim.wrap(bytes32(uint256(1))); + + // Now run Upgrade 15. + runUpgrade15UpgradeAndChecks(upgrader); + + // Get the absolute prestate after the upgrade + Claim pdgPrestateAfter = IPermissionedDisputeGame( + address(disputeGameFactory.gameImpls(GameTypes.PERMISSIONED_CANNON)) + ).absolutePrestate(); + Claim fdgPrestateAfter = + IFaultDisputeGame(address(disputeGameFactory.gameImpls(GameTypes.CANNON))).absolutePrestate(); + + // Assert that the absolute prestate is the non-zero value we set. + assertEq(pdgPrestateAfter.raw(), bytes32(uint256(1))); + assertEq(fdgPrestateAfter.raw(), bytes32(uint256(1))); + } + + /// @notice Tests that the old absolute prestate is used if the upgrade config does not set an + /// absolute prestate. + function test_upgrade_absolutePrestateNotSet_succeeds() public { + // Run Upgrade 13 and 14 to get us to a state where we can run Upgrade 15. + // Can remove these two calls as Upgrade 13 and 14 are executed in prod. + runUpgrade13UpgradeAndChecks(upgrader); + runUpgrade14UpgradeAndChecks(upgrader); + + // Get the pdg and fdg before the upgrade + Claim pdgPrestateBefore = IPermissionedDisputeGame( + address(disputeGameFactory.gameImpls(GameTypes.PERMISSIONED_CANNON)) + ).absolutePrestate(); + Claim fdgPrestateBefore = + IFaultDisputeGame(address(disputeGameFactory.gameImpls(GameTypes.CANNON))).absolutePrestate(); + + // Assert that the prestate is not zero. + assertNotEq(pdgPrestateBefore.raw(), bytes32(0)); + assertNotEq(fdgPrestateBefore.raw(), bytes32(0)); + + // Set the absolute prestate input to zero. + opChainConfigs[0].absolutePrestate = Claim.wrap(bytes32(0)); + + // Now run Upgrade 15. + runUpgrade15UpgradeAndChecks(upgrader); + + // Get the absolute prestate after the upgrade + Claim pdgPrestateAfter = IPermissionedDisputeGame( + address(disputeGameFactory.gameImpls(GameTypes.PERMISSIONED_CANNON)) + ).absolutePrestate(); + Claim fdgPrestateAfter = + IFaultDisputeGame(address(disputeGameFactory.gameImpls(GameTypes.CANNON))).absolutePrestate(); + + // Assert that the absolute prestate is the same as before the upgrade. + assertEq(pdgPrestateAfter.raw(), pdgPrestateBefore.raw()); + assertEq(fdgPrestateAfter.raw(), fdgPrestateBefore.raw()); + } } contract OPContractsManager_Upgrade_TestFails is OPContractsManager_Upgrade_Harness { // Upgrade to U14 first function setUp() public override { + skipIfNotOpFork("test_upgrade_notDelegateCalled_reverts"); super.setUp(); - runV200UpgradeAndChecks(upgrader); + runUpgrade13UpgradeAndChecks(upgrader); } function test_upgrade_notDelegateCalled_reverts() public { @@ -553,8 +787,24 @@ contract OPContractsManager_Upgrade_TestFails is OPContractsManager_Upgrade_Harn ); } + /// @notice Tests that upgrade reverts when absolutePrestate is zero and the existing game also + /// has an absolute prestate of zero. function test_upgrade_absolutePrestateNotSet_reverts() public { + // Set the config to try to update the absolutePrestate to zero. opChainConfigs[0].absolutePrestate = Claim.wrap(bytes32(0)); + + // Get the address of the PermissionedDisputeGame. + IPermissionedDisputeGame pdg = + IPermissionedDisputeGame(address(disputeGameFactory.gameImpls(GameTypes.PERMISSIONED_CANNON))); + + // Mock the PDG to return a prestate of zero. + vm.mockCall( + address(pdg), + abi.encodeCall(IPermissionedDisputeGame.absolutePrestate, ()), + abi.encode(Claim.wrap(bytes32(0))) + ); + + // Expect the upgrade to revert with PrestateNotSet. vm.expectRevert(IOPContractsManager.PrestateNotSet.selector); DelegateCaller(upgrader).dcForward(address(opcm), abi.encodeCall(IOPContractsManager.upgrade, (opChainConfigs))); } @@ -563,6 +813,8 @@ contract OPContractsManager_Upgrade_TestFails is OPContractsManager_Upgrade_Harn contract OPContractsManager_SetRC_Test is OPContractsManager_Upgrade_Harness { /// @notice Tests the setRC function can be set by the upgrade controller. function test_setRC_succeeds(bool _isRC) public { + skipIfNotOpFork("test_setRC_succeeds"); + vm.prank(upgrader); opcm.setRC(_isRC); @@ -646,7 +898,11 @@ contract OPContractsManager_AddGameType_Test is Test { }), optimismPortalImpl: DeployUtils.create1({ _name: "OptimismPortal2", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IOptimismPortal2.__constructor__, (1, 1))) + _args: DeployUtils.encodeConstructor(abi.encodeCall(IOptimismPortal2.__constructor__, (1))) + }), + ethLockboxImpl: DeployUtils.create1({ + _name: "ETHLockbox", + _args: DeployUtils.encodeConstructor(abi.encodeCall(IETHLockbox.__constructor__, ())) }), systemConfigImpl: DeployUtils.create1({ _name: "SystemConfig", @@ -670,7 +926,7 @@ contract OPContractsManager_AddGameType_Test is Test { }), anchorStateRegistryImpl: DeployUtils.create1({ _name: "AnchorStateRegistry", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IAnchorStateRegistry.__constructor__, ())) + _args: DeployUtils.encodeConstructor(abi.encodeCall(IAnchorStateRegistry.__constructor__, (1))) }), delayedWETHImpl: DeployUtils.create1({ _name: "DelayedWETH", @@ -765,9 +1021,9 @@ contract OPContractsManager_AddGameType_Test is Test { basefeeScalar: 1, blobBasefeeScalar: 1, startingAnchorRoot: abi.encode( - OutputRoot({ + Proposal({ root: Hash.wrap(0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef), - l2BlockNumber: 0 + l2SequenceNumber: 0 }) ), l2ChainId: 100, @@ -1009,7 +1265,11 @@ contract OPContractsManager_UpdatePrestate_Test is Test { }), optimismPortalImpl: DeployUtils.create1({ _name: "OptimismPortal2", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IOptimismPortal2.__constructor__, (1, 1))) + _args: DeployUtils.encodeConstructor(abi.encodeCall(IOptimismPortal2.__constructor__, (1))) + }), + ethLockboxImpl: DeployUtils.create1({ + _name: "ETHLockbox", + _args: DeployUtils.encodeConstructor(abi.encodeCall(IETHLockbox.__constructor__, ())) }), systemConfigImpl: DeployUtils.create1({ _name: "SystemConfig", @@ -1033,7 +1293,7 @@ contract OPContractsManager_UpdatePrestate_Test is Test { }), anchorStateRegistryImpl: DeployUtils.create1({ _name: "AnchorStateRegistry", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IAnchorStateRegistry.__constructor__, ())) + _args: DeployUtils.encodeConstructor(abi.encodeCall(IAnchorStateRegistry.__constructor__, (1))) }), delayedWETHImpl: DeployUtils.create1({ _name: "DelayedWETH", @@ -1117,9 +1377,9 @@ contract OPContractsManager_UpdatePrestate_Test is Test { basefeeScalar: 1, blobBasefeeScalar: 1, startingAnchorRoot: abi.encode( - OutputRoot({ + Proposal({ root: Hash.wrap(0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef), - l2BlockNumber: 0 + l2SequenceNumber: 0 }) ), l2ChainId: 100, diff --git a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol index 6b6f60bf77..35b8fb014c 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol @@ -1,14 +1,21 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing +// Forge import { VmSafe } from "forge-std/Vm.sol"; +import { console2 as console } from "forge-std/console2.sol"; + +// Testing import { CommonTest } from "test/setup/CommonTest.sol"; import { NextImpl } from "test/mocks/NextImpl.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; +// Scripts +import { ForgeArtifacts, StorageSlot } from "scripts/libraries/ForgeArtifacts.sol"; + // Contracts import { SuperchainConfig } from "src/L1/SuperchainConfig.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; // Libraries import { Types } from "src/libraries/Types.sol"; @@ -17,14 +24,15 @@ import { Constants } from "src/libraries/Constants.sol"; import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import "src/dispute/lib/Types.sol"; -import "src/libraries/PortalErrors.sol"; // Interfaces import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; +import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPortal2.sol"; import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; +import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; contract OptimismPortal2_Test is CommonTest { address depositor; @@ -44,23 +52,26 @@ contract OptimismPortal2_Test is CommonTest { /// @notice Marked virtual to be overridden in /// test/kontrol/deployment/DeploymentSummary.t.sol function test_constructor_succeeds() external virtual { - IOptimismPortal2 opImpl = IOptimismPortal2(payable(EIP1967Helper.getImplementation(address(optimismPortal2)))); - assertEq(address(opImpl.disputeGameFactory()), address(0)); + IOptimismPortal opImpl = IOptimismPortal(payable(EIP1967Helper.getImplementation(address(optimismPortal2)))); + assertEq(address(opImpl.anchorStateRegistry()), address(0)); assertEq(address(opImpl.systemConfig()), address(0)); assertEq(address(opImpl.superchainConfig()), address(0)); - assertEq(opImpl.respectedGameType().raw(), deploy.cfg().respectedGameType()); assertEq(opImpl.l2Sender(), address(0)); + assertEq(address(opImpl.anchorStateRegistry()), address(0)); + assertEq(address(opImpl.ethLockbox()), address(0)); } /// @dev Tests that the initializer sets the correct values. /// @notice Marked virtual to be overridden in /// test/kontrol/deployment/DeploymentSummary.t.sol function test_initialize_succeeds() external virtual { + assertEq(address(optimismPortal2.anchorStateRegistry()), address(anchorStateRegistry)); assertEq(address(optimismPortal2.disputeGameFactory()), address(disputeGameFactory)); assertEq(address(optimismPortal2.superchainConfig()), address(superchainConfig)); assertEq(optimismPortal2.l2Sender(), Constants.DEFAULT_L2_SENDER); assertEq(optimismPortal2.paused(), false); assertEq(address(optimismPortal2.systemConfig()), address(systemConfig)); + assertEq(address(optimismPortal2.ethLockbox()), address(ethLockbox)); returnIfForkTest( "OptimismPortal2_Initialize_Test: Do not check guardian and respectedGameType on forked networks" @@ -73,6 +84,32 @@ contract OptimismPortal2_Test is CommonTest { assertEq(optimismPortal2.respectedGameType().raw(), deploy.cfg().respectedGameType()); } + /// @dev Tests that the upgrade function succeeds. + function testFuzz_upgrade_succeeds(address _newAnchorStateRegistry, uint256 _balance) external { + // Prevent overflow on an upgrade context + _balance = bound(_balance, 0, type(uint256).max - address(ethLockbox).balance); + + // Set the initialize state of the portal to false. + vm.store(address(optimismPortal2), bytes32(uint256(0)), bytes32(uint256(0))); + + // Set the balance of the portal and get the lockbox balance before the upgrade. + deal(address(optimismPortal2), _balance); + uint256 lockboxBalanceBefore = address(ethLockbox).balance; + + // Expect the ETH to be migrated to the lockbox. + vm.expectCall(address(ethLockbox), _balance, abi.encodeCall(ethLockbox.lockETH, ())); + + // Call the upgrade function. + vm.prank(Predeploys.PROXY_ADMIN); + optimismPortal2.upgrade(IAnchorStateRegistry(_newAnchorStateRegistry), IETHLockbox(ethLockbox)); + + // Assert the portal is properly upgraded. + assertEq(address(optimismPortal2.ethLockbox()), address(ethLockbox)); + assertEq(address(optimismPortal2.anchorStateRegistry()), _newAnchorStateRegistry); + assertEq(address(optimismPortal2).balance, 0); + assertEq(address(ethLockbox).balance, lockboxBalanceBefore + _balance); + } + /// @dev Tests that `pause` successfully pauses /// when called by the GUARDIAN. function test_pause_succeeds() external { @@ -136,7 +173,10 @@ contract OptimismPortal2_Test is CommonTest { /// @dev Tests that `receive` successdully deposits ETH. function testFuzz_receive_succeeds(uint256 _value) external { + // Prevent overflow on an upgrade context + _value = bound(_value, 0, type(uint256).max - address(ethLockbox).balance); uint256 balanceBefore = address(optimismPortal2).balance; + uint256 lockboxBalanceBefore = address(ethLockbox).balance; _value = bound(_value, 0, type(uint256).max - balanceBefore); vm.expectEmit(address(optimismPortal2)); @@ -150,20 +190,24 @@ contract OptimismPortal2_Test is CommonTest { _data: hex"" }); + // Expect call to the ETHLockbox to lock the funds only if the value is greater than 0. + vm.expectCall(address(ethLockbox), _value, abi.encodeCall(ethLockbox.lockETH, ()), _value > 0 ? 1 : 0); + // give alice money and send as an eoa vm.deal(alice, _value); vm.prank(alice, alice); (bool s,) = address(optimismPortal2).call{ value: _value }(hex""); assertTrue(s); - assertEq(address(optimismPortal2).balance, balanceBefore + _value); + assertEq(address(optimismPortal2).balance, balanceBefore); + assertEq(address(ethLockbox).balance, lockboxBalanceBefore + _value); } /// @dev Tests that `depositTransaction` reverts when the destination address is non-zero /// for a contract creation deposit. function test_depositTransaction_contractCreation_reverts() external { // contract creation must have a target of address(0) - vm.expectRevert(BadTarget.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_BadTarget.selector); optimismPortal2.depositTransaction(address(1), 1, 0, true, hex""); } @@ -172,7 +216,7 @@ contract OptimismPortal2_Test is CommonTest { function test_depositTransaction_largeData_reverts() external { uint256 size = 120_001; uint64 gasLimit = optimismPortal2.minimumGasLimit(uint64(size)); - vm.expectRevert(LargeCalldata.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_CalldataTooLarge.selector); optimismPortal2.depositTransaction({ _to: address(0), _value: 0, @@ -184,7 +228,7 @@ contract OptimismPortal2_Test is CommonTest { /// @dev Tests that `depositTransaction` reverts when the gas limit is too small. function test_depositTransaction_smallGasLimit_reverts() external { - vm.expectRevert(SmallGasLimit.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_GasLimitTooLow.selector); optimismPortal2.depositTransaction({ _to: address(1), _value: 0, _gasLimit: 0, _isCreation: false, _data: hex"" }); } @@ -194,7 +238,7 @@ contract OptimismPortal2_Test is CommonTest { uint64 gasLimit = optimismPortal2.minimumGasLimit(uint64(_data.length)); if (_shouldFail) { gasLimit = uint64(bound(gasLimit, 0, gasLimit - 1)); - vm.expectRevert(SmallGasLimit.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_GasLimitTooLow.selector); } optimismPortal2.depositTransaction({ @@ -226,6 +270,8 @@ contract OptimismPortal2_Test is CommonTest { ) external { + // Prevent overflow on an upgrade context + _mint = bound(_mint, 0, type(uint256).max - address(ethLockbox).balance); _gasLimit = uint64( bound( _gasLimit, @@ -236,6 +282,7 @@ contract OptimismPortal2_Test is CommonTest { if (_isCreation) _to = address(0); uint256 balanceBefore = address(optimismPortal2).balance; + uint256 lockboxBalanceBefore = address(ethLockbox).balance; _mint = bound(_mint, 0, type(uint256).max - balanceBefore); // EOA emulation @@ -250,6 +297,9 @@ contract OptimismPortal2_Test is CommonTest { _data: _data }); + // Expect call to the ETHLockbox to lock the funds only if the value is greater than 0. + vm.expectCall(address(ethLockbox), _mint, abi.encodeCall(ethLockbox.lockETH, ()), _mint > 0 ? 1 : 0); + vm.deal(depositor, _mint); vm.prank(depositor, depositor); optimismPortal2.depositTransaction{ value: _mint }({ @@ -259,7 +309,9 @@ contract OptimismPortal2_Test is CommonTest { _isCreation: _isCreation, _data: _data }); - assertEq(address(optimismPortal2).balance, balanceBefore + _mint); + + assertEq(address(optimismPortal2).balance, balanceBefore); + assertEq(address(ethLockbox).balance, lockboxBalanceBefore + _mint); } /// @dev Tests that `depositTransaction` succeeds for an EOA using 7702 delegation. @@ -274,6 +326,10 @@ contract OptimismPortal2_Test is CommonTest { ) external { + assumeNotForgeAddress(_7702Target); + // Prevent overflow on an upgrade context + _mint = bound(_mint, 0, type(uint256).max - address(ethLockbox).balance); + _gasLimit = uint64( bound( _gasLimit, @@ -283,8 +339,9 @@ contract OptimismPortal2_Test is CommonTest { ); if (_isCreation) _to = address(0); - uint256 balanceBefore = address(optimismPortal2).balance; - _mint = bound(_mint, 0, type(uint256).max - balanceBefore); + uint256 portalBalanceBefore = address(optimismPortal2).balance; + uint256 lockboxBalanceBefore = address(ethLockbox).balance; + _mint = bound(_mint, 0, type(uint256).max - portalBalanceBefore); // EOA emulation vm.expectEmit(address(optimismPortal2)); @@ -310,7 +367,8 @@ contract OptimismPortal2_Test is CommonTest { _isCreation: _isCreation, _data: _data }); - assertEq(address(optimismPortal2).balance, balanceBefore + _mint); + assertEq(address(optimismPortal2).balance, portalBalanceBefore); + assertEq(address(ethLockbox).balance, lockboxBalanceBefore + _mint); } /// @dev Tests that `depositTransaction` succeeds for a contract. @@ -324,6 +382,8 @@ contract OptimismPortal2_Test is CommonTest { ) external { + // Prevent overflow on an upgrade context + _mint = bound(_mint, 0, type(uint256).max - address(ethLockbox).balance); _gasLimit = uint64( bound( _gasLimit, @@ -334,6 +394,7 @@ contract OptimismPortal2_Test is CommonTest { if (_isCreation) _to = address(0); uint256 balanceBefore = address(optimismPortal2).balance; + uint256 lockboxBalanceBefore = address(ethLockbox).balance; _mint = bound(_mint, 0, type(uint256).max - balanceBefore); vm.expectEmit(address(optimismPortal2)); @@ -347,6 +408,9 @@ contract OptimismPortal2_Test is CommonTest { _data: _data }); + // Expect call to the ETHLockbox to lock the funds only if the value is greater than 0. + vm.expectCall(address(ethLockbox), _mint, abi.encodeCall(ethLockbox.lockETH, ()), _mint > 0 ? 1 : 0); + vm.deal(address(this), _mint); vm.prank(address(this)); optimismPortal2.depositTransaction{ value: _mint }({ @@ -356,7 +420,8 @@ contract OptimismPortal2_Test is CommonTest { _isCreation: _isCreation, _data: _data }); - assertEq(address(optimismPortal2).balance, balanceBefore + _mint); + assertEq(address(optimismPortal2).balance, balanceBefore); + assertEq(address(ethLockbox).balance, lockboxBalanceBefore + _mint); } /// @dev Tests that the donateETH function donates ETH and does no state read/write @@ -365,6 +430,7 @@ contract OptimismPortal2_Test is CommonTest { vm.deal(alice, _amount); uint256 preBalance = address(optimismPortal2).balance; + uint256 lockboxBalanceBefore = address(ethLockbox).balance; _amount = bound(_amount, 0, type(uint256).max - preBalance); vm.startStateDiffRecording(); @@ -373,6 +439,8 @@ contract OptimismPortal2_Test is CommonTest { // not necessary since it's checked below assertEq(address(optimismPortal2).balance, preBalance + _amount); + // check that the ETHLockbox balance is unchanged + assertEq(address(ethLockbox).balance, lockboxBalanceBefore); // 0 for extcodesize of proxy before being called by this test, // 1 for the call to the proxy by the pranked address @@ -400,12 +468,59 @@ contract OptimismPortal2_Test is CommonTest { // storage accesses of delegate call of proxy to impl is empty (No storage read or write!) assertEq(accountAccesses[2].storageAccesses.length, 0); } + + /// @dev Tests that `migrateToSuperRoots` reverts if the caller is not the proxy admin owner. + function testFuzz_migrateToSuperRoots_notProxyAdminOwner_reverts(address _caller) external { + vm.assume(_caller != optimismPortal2.proxyAdminOwner()); + vm.expectRevert(IOptimismPortal.OptimismPortal_Unauthorized.selector); + + vm.prank(_caller); + optimismPortal2.migrateToSuperRoots(IETHLockbox(address(1)), IAnchorStateRegistry(address(1))); + } + + /// @dev Tests that `migrateToSuperRoots` reverts if the new registry is the same as the + /// current one. + /// @param _newLockbox The new ETHLockbox to migrate to. + function testFuzz_migrateToSuperRoots_usingSameRegistry_reverts(address _newLockbox) external { + vm.assume(_newLockbox != address(optimismPortal2.ethLockbox())); + + // Use the same registry as the current one. + IAnchorStateRegistry newAnchorStateRegistry = optimismPortal2.anchorStateRegistry(); + + // Trigger the call from the right address. + address caller = optimismPortal2.proxyAdminOwner(); + + // Expect the migration to revert. + vm.expectRevert(IOptimismPortal.OptimismPortal_MigratingToSameRegistry.selector); + vm.prank(caller); + optimismPortal2.migrateToSuperRoots(IETHLockbox(_newLockbox), newAnchorStateRegistry); + } + + /// @dev Tests that `migrateToSuperRoots` updates the ETHLockbox contract, updates the + /// AnchorStateRegistry, and sets the superRootsActive flag to true. + /// @param _newLockbox The new ETHLockbox to migrate to. + /// @param _newAnchorStateRegistry The new AnchorStateRegistry to migrate to. + function testFuzz_migrateToSuperRoots_succeeds(address _newLockbox, address _newAnchorStateRegistry) external { + address oldLockbox = address(optimismPortal2.ethLockbox()); + address oldAnchorStateRegistry = address(optimismPortal2.anchorStateRegistry()); + vm.assume(_newLockbox != oldLockbox); + vm.assume(_newAnchorStateRegistry != oldAnchorStateRegistry); + + vm.expectEmit(address(optimismPortal2)); + emit PortalMigrated(oldLockbox, _newLockbox, oldAnchorStateRegistry, _newAnchorStateRegistry); + + vm.prank(optimismPortal2.proxyAdminOwner()); + optimismPortal2.migrateToSuperRoots(IETHLockbox(_newLockbox), IAnchorStateRegistry(_newAnchorStateRegistry)); + + assertEq(address(optimismPortal2.ethLockbox()), _newLockbox); + assertEq(address(optimismPortal2.anchorStateRegistry()), _newAnchorStateRegistry); + assertTrue(optimismPortal2.superRootsActive()); + } } contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Reusable default values for a test withdrawal Types.WithdrawalTransaction _defaultTx; - IFaultDisputeGame game; uint256 _proposedGameIndex; uint256 _proposedBlockNumber; @@ -428,6 +543,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { gasLimit: 100_000, data: hex"aa" // includes calldata for ERC20 withdrawal test }); + // Get withdrawal proof data we can use for testing. (_stateRoot, _storageRoot, _outputRoot, _withdrawalHash, _withdrawalProof) = ffi.getProveWithdrawalTransactionInputs(_defaultTx); @@ -455,12 +571,13 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { address(disputeGameFactory), keccak256(abi.encode(GameType.wrap(0), uint256(102))), bytes32(uint256(0)) ); } else { - // Warp forward in time to ensure that the game is created after the retirement timestamp. - vm.warp(optimismPortal2.respectedGameTypeUpdatedAt() + 1 seconds); - // Set up the dummy game. _proposedBlockNumber = 0xFF; } + + // Warp forward in time to ensure that the game is created after the retirement timestamp. + vm.warp(anchorStateRegistry.retirementTimestamp() + 1); + GameType respectedGameType = optimismPortal2.respectedGameType(); game = IFaultDisputeGame( payable( @@ -479,12 +596,12 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.warp(block.timestamp + game.maxClockDuration().raw() + 1 seconds); // Fund the portal so that we can withdraw ETH. - vm.deal(address(optimismPortal2), 0xFFFFFFFF); + vm.deal(address(ethLockbox), 0xFFFFFFFF); } /// @dev Asserts that the reentrant call will revert. function callPortalAndExpectRevert() external payable { - vm.expectRevert(NonReentrant.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_NoReentrancy.selector); // Arguments here don't matter, as the require check is the first thing that happens. // We assume that this has already been proven. optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); @@ -492,62 +609,33 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { assertFalse(optimismPortal2.finalizedWithdrawals(Hashing.hashWithdrawal(_defaultTx))); } - /// @dev Tests that `blacklistDisputeGame` reverts when called by a non-guardian. - function testFuzz_blacklist_onlyGuardian_reverts(address _act) external { - vm.assume(_act != address(optimismPortal2.guardian())); - - vm.expectRevert(Unauthorized.selector); - optimismPortal2.blacklistDisputeGame(IDisputeGame(address(0xdead))); - } - - /// @dev Tests that the guardian role can blacklist any dispute game. - function testFuzz_blacklist_guardian_succeeds(IDisputeGame _addr) external { - vm.expectEmit(address(optimismPortal2)); - emit DisputeGameBlacklisted(_addr); - - vm.prank(optimismPortal2.guardian()); - optimismPortal2.blacklistDisputeGame(_addr); + /// @dev Tests that `finalizeWithdrawalTransaction` reverts when the target is the portal contract or the lockbox. + function test_finalizeWithdrawalTransaction_badTarget_reverts() external { + _defaultTx.target = address(optimismPortal2); + vm.expectRevert(IOptimismPortal.OptimismPortal_BadTarget.selector); + optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); - assertTrue(optimismPortal2.disputeGameBlacklist(_addr)); + _defaultTx.target = address(ethLockbox); + vm.expectRevert(IOptimismPortal.OptimismPortal_BadTarget.selector); + optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } - /// @dev Tests that `setRespectedGameType` reverts when called by a non-guardian. - function testFuzz_setRespectedGameType_onlyGuardian_reverts(address _act, GameType _ty) external { - vm.assume(_act != address(optimismPortal2.guardian())); + /// @notice Sets the supeRootsActive variable to the provided value. + /// @param _superRootsActive The value to set the superRootsActive variable to. + function setSuperRootsActive(bool _superRootsActive) public { + // Get the slot for superRootsActive. + StorageSlot memory slot = ForgeArtifacts.getSlot("OptimismPortal2", "superRootsActive"); - vm.prank(_act); - vm.expectRevert(Unauthorized.selector); - optimismPortal2.setRespectedGameType(_ty); - } + // Load the existing storage slot value. + bytes32 existingValue = vm.load(address(optimismPortal2), bytes32(slot.slot)); - /// @dev Tests that the guardian role can set the respected game type to anything they want. - function testFuzz_setRespectedGameType_guardianCanSetRespectedGameType_succeeds(GameType _ty) external { - vm.assume(_ty.raw() != type(uint32).max); - uint64 respectedGameTypeUpdatedAt = optimismPortal2.respectedGameTypeUpdatedAt(); - vm.expectEmit(address(optimismPortal2)); - emit RespectedGameTypeSet(_ty, Timestamp.wrap(respectedGameTypeUpdatedAt)); - vm.prank(optimismPortal2.guardian()); - optimismPortal2.setRespectedGameType(_ty); - // GameType changes, but the timestamp doesn't. - assertEq(optimismPortal2.respectedGameType().raw(), _ty.raw()); - assertEq(optimismPortal2.respectedGameTypeUpdatedAt(), respectedGameTypeUpdatedAt); - } + // Inject the bool into the existing storage slot value with a bitwise OR. + // Shift the bool left by the offset of the storage slot and OR with existing value. + bytes32 newValue = + bytes32(uint256(uint8(_superRootsActive ? 1 : 0)) << slot.offset * 8 | uint256(existingValue)); - /// @dev Tests that the guardian can set the `respectedGameTypeUpdatedAt` timestamp to current timestamp. - function testFuzz_setRespectedGameType_guardianCanSetRespectedGameTypeUpdatedAt_succeeds(uint64 _elapsed) - external - { - _elapsed = uint64(bound(_elapsed, 0, type(uint64).max - uint64(block.timestamp))); - GameType _ty = GameType.wrap(type(uint32).max); - uint64 _newRespectedGameTypeUpdatedAt = uint64(block.timestamp) + _elapsed; - GameType _existingGameType = optimismPortal2.respectedGameType(); - vm.warp(_newRespectedGameTypeUpdatedAt); - emit RespectedGameTypeSet(_existingGameType, Timestamp.wrap(_newRespectedGameTypeUpdatedAt)); - vm.prank(optimismPortal2.guardian()); - optimismPortal2.setRespectedGameType(_ty); - // GameType doesn't change, but the timestamp does. - assertEq(optimismPortal2.respectedGameType().raw(), _existingGameType.raw()); - assertEq(optimismPortal2.respectedGameTypeUpdatedAt(), _newRespectedGameTypeUpdatedAt); + // Store the new value at the correct slot/offset. + vm.store(address(optimismPortal2), bytes32(slot.slot), newValue); } /// @dev Tests that `proveWithdrawalTransaction` reverts when paused. @@ -555,7 +643,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.prank(optimismPortal2.guardian()); superchainConfig.pause("identifier"); - vm.expectRevert(CallPaused.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_CallPaused.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -567,7 +655,35 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { /// @dev Tests that `proveWithdrawalTransaction` reverts when the target is the portal contract. function test_proveWithdrawalTransaction_onSelfCall_reverts() external { _defaultTx.target = address(optimismPortal2); - vm.expectRevert(BadTarget.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_BadTarget.selector); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + _defaultTx.target = address(ethLockbox); + vm.expectRevert(IOptimismPortal.OptimismPortal_BadTarget.selector); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `proveWithdrawalTransaction` reverts when the current timestamp is less + /// than or equal to the creation timestamp of the dispute game. + function testFuzz_proveWithdrawalTransaction_timestampLessThanOrEqualToCreation_reverts(uint64 _timestamp) + external + { + // Set the timestamp to be less than or equal to the creation timestamp of the dispute game. + _timestamp = uint64(bound(_timestamp, 0, game.createdAt().raw())); + vm.warp(_timestamp); + + // Should revert. + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidProofTimestamp.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -580,7 +696,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { function test_proveWithdrawalTransaction_onInvalidOutputRootProof_reverts() external { // Modify the version to invalidate the withdrawal proof. _outputRootProof.version = bytes32(uint256(1)); - vm.expectRevert(InvalidProof.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidOutputRootProof.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -624,25 +740,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.mockCall(address(game), abi.encodeCall(game.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)); vm.mockCall(address(game2), abi.encodeCall(game.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)); - vm.expectRevert(InvalidDisputeGame.selector); - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); - } - - /// @dev Tests that `proveWithdrawalTransaction` reverts if the dispute game being proven against is not of the - /// respected game type. - function test_proveWithdrawalTransaction_badGameType_reverts() external { - vm.mockCall( - address(disputeGameFactory), - abi.encodeCall(disputeGameFactory.gameAtIndex, (_proposedGameIndex)), - abi.encode(GameType.wrap(0xFF), Timestamp.wrap(uint64(block.timestamp)), IDisputeGame(address(game))) - ); - - vm.expectRevert(InvalidGameType.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidDisputeGame.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -654,7 +752,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { /// @dev Tests that `proveWithdrawalTransaction` reverts if the game was not the respected game type when created. function test_proveWithdrawalTransaction_wasNotRespectedGameTypeWhenCreated_reverts() external { vm.mockCall(address(game), abi.encodeCall(game.wasRespectedGameTypeWhenCreated, ()), abi.encode(false)); - vm.expectRevert(InvalidGameType.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidDisputeGame.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -667,7 +765,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { /// `wasRespectedGameTypeWhenCreated`. function test_proveWithdrawalTransaction_legacyGame_reverts() external { vm.mockCallRevert(address(game), abi.encodeCall(game.wasRespectedGameTypeWhenCreated, ()), ""); - vm.expectRevert(LegacyGame.selector); + vm.expectRevert(); // nosemgrep: sol-safety-expectrevert-no-args optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -679,7 +777,8 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { /// @dev Tests that `proveWithdrawalTransaction` succeeds if the game was created after the /// game retirement timestamp. function testFuzz_proveWithdrawalTransaction_createdAfterRetirementTimestamp_succeeds(uint64 _createdAt) external { - _createdAt = uint64(bound(_createdAt, optimismPortal2.respectedGameTypeUpdatedAt() + 1, type(uint64).max)); + _createdAt = uint64(bound(_createdAt, optimismPortal2.respectedGameTypeUpdatedAt() + 1, type(uint64).max - 1)); + vm.warp(_createdAt + 1); vm.mockCall(address(game), abi.encodeCall(game.createdAt, ()), abi.encode(uint64(_createdAt))); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, @@ -696,45 +795,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { { _createdAt = uint64(bound(_createdAt, 0, optimismPortal2.respectedGameTypeUpdatedAt())); vm.mockCall(address(game), abi.encodeCall(game.createdAt, ()), abi.encode(uint64(_createdAt))); - vm.expectRevert("OptimismPortal: dispute game created before respected game type was updated"); - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); - } - - /// @dev Tests that `proveWithdrawalTransaction` can be re-executed if the dispute game proven against has been - /// blacklisted. - function test_proveWithdrawalTransaction_replayProveBlacklisted_succeeds() external { - vm.expectEmit(true, true, true, true); - emit WithdrawalProven(_withdrawalHash, alice, bob); - vm.expectEmit(true, true, true, true); - emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); - - // Blacklist the dispute dispute game. - vm.prank(optimismPortal2.guardian()); - optimismPortal2.blacklistDisputeGame(IDisputeGame(address(game))); - - // Mock the status of the dispute game we just proved against to be CHALLENGER_WINS. - vm.mockCall(address(game), abi.encodeCall(game.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)); - // Create a new game to re-prove against - disputeGameFactory.create( - optimismPortal2.respectedGameType(), Claim.wrap(_outputRoot), abi.encode(_proposedBlockNumber + 1) - ); - _proposedGameIndex = disputeGameFactory.gameCount() - 1; - - vm.expectEmit(true, true, true, true); - emit WithdrawalProven(_withdrawalHash, alice, bob); - vm.expectEmit(true, true, true, true); - emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); + vm.expectRevert(IOptimismPortal.OptimismPortal_ImproperDisputeGame.selector); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -759,12 +820,16 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Mock the status of the dispute game we just proved against to be CHALLENGER_WINS. vm.mockCall(address(game), abi.encodeCall(game.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)); + // Create a new game to re-prove against disputeGameFactory.create( optimismPortal2.respectedGameType(), Claim.wrap(_outputRoot), abi.encode(_proposedBlockNumber + 1) ); _proposedGameIndex = disputeGameFactory.gameCount() - 1; + // Warp 1 second into the future so we're not in the same block as the dispute game. + vm.warp(block.timestamp + 1 seconds); + vm.expectEmit(true, true, true, true); emit WithdrawalProven(_withdrawalHash, alice, bob); vm.expectEmit(true, true, true, true); @@ -798,7 +863,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Update the respected game type to 0xbeef. vm.prank(optimismPortal2.guardian()); - optimismPortal2.setRespectedGameType(GameType.wrap(0xbeef)); + anchorStateRegistry.setRespectedGameType(GameType.wrap(0xbeef)); // Create a new game and mock the game type as 0xbeef in the factory. vm.mockCall( @@ -807,6 +872,9 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { abi.encode(GameType.wrap(0xbeef), Timestamp.wrap(uint64(block.timestamp)), IDisputeGame(address(newGame))) ); + // Warp 1 second into the future so we're not in the same block as the dispute game. + vm.warp(block.timestamp + 1 seconds); + // Re-proving should be successful against the new game. vm.expectEmit(true, true, true, true); emit WithdrawalProven(_withdrawalHash, alice, bob); @@ -820,6 +888,216 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { }); } + /// @dev Tests that `proveWithdrawalTransaction` reverts when using the Output Roots version of + /// `proveWithdrawalTransaction` when `superRootsActive` is true. + function test_proveWithdrawalTransaction_outputRootVersionWhenSuperRootsActive_reverts() external { + // Set superRootsActive to true. + setSuperRootsActive(true); + + // Should revert. + vm.expectRevert(IOptimismPortal.OptimismPortal_WrongProofMethod.selector); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `proveWithdrawalTransaction` reverts when using the Super Roots version of + /// `proveWithdrawalTransaction` when `superRootsActive` is false. + function test_proveWithdrawalTransaction_superRootsVersionWhenSuperRootsInactive_reverts() external { + // Set up a dummy super root proof. + Types.OutputRootWithChainId[] memory outputRootWithChainIdArr = new Types.OutputRootWithChainId[](1); + outputRootWithChainIdArr[0] = + Types.OutputRootWithChainId({ root: _outputRoot, chainId: systemConfig.l2ChainId() }); + Types.SuperRootProof memory superRootProof = Types.SuperRootProof({ + version: 0x01, + timestamp: uint64(block.timestamp), + outputRoots: outputRootWithChainIdArr + }); + + // Should revert. + vm.expectRevert(IOptimismPortal.OptimismPortal_WrongProofMethod.selector); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameProxy: game, + _outputRootIndex: 0, + _superRootProof: superRootProof, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `proveWithdrawalTransaction` reverts when using the Super Roots version of + /// `proveWithdrawalTransaction` when the provided proof is invalid. + function test_proveWithdrawalTransaction_superRootsVersionBadProof_reverts() external { + // Enable super roots. + setSuperRootsActive(true); + + // Set up a dummy super root proof. + Types.OutputRootWithChainId[] memory outputRootWithChainIdArr = new Types.OutputRootWithChainId[](1); + outputRootWithChainIdArr[0] = + Types.OutputRootWithChainId({ root: _outputRoot, chainId: systemConfig.l2ChainId() }); + Types.SuperRootProof memory superRootProof = Types.SuperRootProof({ + version: 0x01, + timestamp: uint64(block.timestamp), + outputRoots: outputRootWithChainIdArr + }); + + // Should revert because the proof is wrong. + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidSuperRootProof.selector); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameProxy: game, + _outputRootIndex: 0, + _superRootProof: superRootProof, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `proveWithdrawalTransaction` reverts when using the Super Roots version of + /// `proveWithdrawalTransaction` when the provided proof is valid but the index is out of + /// bounds. + function test_proveWithdrawalTransaction_superRootsVersionBadIndex_reverts() external { + // Enable super roots. + setSuperRootsActive(true); + + // Set up a dummy super root proof. + Types.OutputRootWithChainId[] memory outputRootWithChainIdArr = new Types.OutputRootWithChainId[](1); + outputRootWithChainIdArr[0] = + Types.OutputRootWithChainId({ root: _outputRoot, chainId: systemConfig.l2ChainId() }); + Types.SuperRootProof memory superRootProof = Types.SuperRootProof({ + version: 0x01, + timestamp: uint64(block.timestamp), + outputRoots: outputRootWithChainIdArr + }); + + // Figure out what the right hash would be. + bytes32 expectedSuperRoot = Hashing.hashSuperRootProof(superRootProof); + + // Mock the game to return the expected super root. + vm.mockCall(address(game), abi.encodeCall(game.rootClaim, ()), abi.encode(expectedSuperRoot)); + + // Should revert because the proof is wrong. + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidOutputRootIndex.selector); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameProxy: game, + _outputRootIndex: outputRootWithChainIdArr.length, // out of bounds + _superRootProof: superRootProof, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `proveWithdrawalTransaction` reverts when using the Super Roots version of + /// `proveWithdrawalTransaction` when the provided proof is valid, index is correct, but + /// the output root has the wrong chain id. + function test_proveWithdrawalTransaction_superRootsVersionBadChainId_reverts() external { + // Enable super roots. + setSuperRootsActive(true); + + // Set up a dummy super root proof. + Types.OutputRootWithChainId[] memory outputRootWithChainIdArr = new Types.OutputRootWithChainId[](1); + outputRootWithChainIdArr[0] = Types.OutputRootWithChainId({ + root: _outputRoot, + chainId: systemConfig.l2ChainId() + 1 // wrong chain id + }); + Types.SuperRootProof memory superRootProof = Types.SuperRootProof({ + version: 0x01, + timestamp: uint64(block.timestamp), + outputRoots: outputRootWithChainIdArr + }); + + // Figure out what the right hash would be. + bytes32 expectedSuperRoot = Hashing.hashSuperRootProof(superRootProof); + + // Mock the game to return the expected super root. + vm.mockCall(address(game), abi.encodeCall(game.rootClaim, ()), abi.encode(expectedSuperRoot)); + + // Should revert because the proof is wrong. + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidOutputRootChainId.selector); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameProxy: game, + _outputRootIndex: 0, + _superRootProof: superRootProof, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `proveWithdrawalTransaction` reverts when using the Super Roots version of + /// `proveWithdrawalTransaction` when the provided proof is valid, index is correct, chain + /// id is correct, but the output root proof is invalid. + function test_proveWithdrawalTransaction_superRootsVersionBadOutputRootProof_reverts() external { + // Enable super roots. + setSuperRootsActive(true); + + // Set up a dummy super root proof. + Types.OutputRootWithChainId[] memory outputRootWithChainIdArr = new Types.OutputRootWithChainId[](1); + outputRootWithChainIdArr[0] = Types.OutputRootWithChainId({ + root: keccak256(abi.encode(_outputRoot)), // random root so the proof is wrong + chainId: systemConfig.l2ChainId() + }); + Types.SuperRootProof memory superRootProof = Types.SuperRootProof({ + version: 0x01, + timestamp: uint64(block.timestamp), + outputRoots: outputRootWithChainIdArr + }); + + // Figure out what the right hash would be. + bytes32 expectedSuperRoot = Hashing.hashSuperRootProof(superRootProof); + + // Mock the game to return the expected super root. + vm.mockCall(address(game), abi.encodeCall(game.rootClaim, ()), abi.encode(expectedSuperRoot)); + + // Should revert because the proof is wrong. + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidOutputRootProof.selector); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameProxy: game, + _outputRootIndex: 0, + _superRootProof: superRootProof, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + /// @dev Tests that `proveWithdrawalTransaction` succeeds when all parameters are valid. + function test_proveWithdrawalTransaction_superRootsVersion_succeeds() external { + // Enable super roots. + setSuperRootsActive(true); + + // Set up a dummy super root proof. + Types.OutputRootWithChainId[] memory outputRootWithChainIdArr = new Types.OutputRootWithChainId[](1); + outputRootWithChainIdArr[0] = + Types.OutputRootWithChainId({ root: _outputRoot, chainId: systemConfig.l2ChainId() }); + Types.SuperRootProof memory superRootProof = Types.SuperRootProof({ + version: 0x01, + timestamp: uint64(block.timestamp), + outputRoots: outputRootWithChainIdArr + }); + + // Figure out what the right hash would be. + bytes32 expectedSuperRoot = Hashing.hashSuperRootProof(superRootProof); + + // Mock the game to return the expected super root. + vm.mockCall(address(game), abi.encodeCall(game.rootClaim, ()), abi.encode(expectedSuperRoot)); + + // Should succeed. + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameProxy: game, + _outputRootIndex: 0, + _superRootProof: superRootProof, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + /// @dev Tests that `proveWithdrawalTransaction` succeeds. function test_proveWithdrawalTransaction_validWithdrawalProof_succeeds() external { vm.expectEmit(true, true, true, true); @@ -873,7 +1151,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { emit WithdrawalFinalized(_withdrawalHash, true); optimismPortal2.finalizeWithdrawalTransactionExternalProof(_defaultTx, address(0xb0b)); - vm.expectRevert(AlreadyFinalized.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_AlreadyFinalized.selector); optimismPortal2.finalizeWithdrawalTransactionExternalProof(_defaultTx, address(this)); assert(address(bob).balance == bobBalanceBefore + 100); @@ -895,7 +1173,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1 seconds); vm.startPrank(alice, Constants.ESTIMATION_ADDRESS); - vm.expectRevert(GasEstimation.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_GasEstimation.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } @@ -940,7 +1218,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.warp(block.timestamp + game_noData.maxClockDuration().raw() + 1 seconds); // Fund the portal so that we can withdraw ETH. vm.store(address(optimismPortal2), bytes32(uint256(61)), bytes32(uint256(0xFFFFFFFF))); - vm.deal(address(optimismPortal2), 0xFFFFFFFF); + vm.deal(address(ethLockbox), 0xFFFFFFFF); uint256 bobBalanceBefore = bob.balance; @@ -1005,7 +1283,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { ); // Warp 1 second into the future so that the proof is submitted after the timestamp of game creation. - vm.warp(block.timestamp + 1 seconds); + vm.warp(block.timestamp + 1); // Prove the withdrawal transaction against the invalid dispute game, as 0xb0b. vm.expectEmit(true, true, true, true); @@ -1044,7 +1322,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Ensure both proofs are registered successfully. assertEq(optimismPortal2.numProofSubmitters(_withdrawalHash), 2); - vm.expectRevert(ProposalNotValidated.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidRootClaim.selector); vm.prank(address(0xb0b)); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); @@ -1060,7 +1338,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.prank(optimismPortal2.guardian()); superchainConfig.pause("identifier"); - vm.expectRevert(CallPaused.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_CallPaused.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } @@ -1068,7 +1346,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { function test_finalizeWithdrawalTransaction_ifWithdrawalNotProven_reverts() external { uint256 bobBalanceBefore = address(bob).balance; - vm.expectRevert(Unproven.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_Unproven.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); assert(address(bob).balance == bobBalanceBefore); @@ -1090,7 +1368,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { _withdrawalProof: _withdrawalProof }); - vm.expectRevert("OptimismPortal: proven withdrawal has not matured yet"); + vm.expectRevert(IOptimismPortal.OptimismPortal_ProofNotOldEnough.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); assert(address(bob).balance == bobBalanceBefore); @@ -1120,7 +1398,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.mockCall(address(game), abi.encodeCall(game.createdAt, ()), abi.encode(block.timestamp + 1)); // Attempt to finalize the withdrawal - vm.expectRevert("OptimismPortal: withdrawal timestamp less than dispute game creation timestamp"); + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidProofTimestamp.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); // Ensure that bob's balance has remained the same @@ -1148,7 +1426,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); // Attempt to finalize the withdrawal - vm.expectRevert(ProposalNotValidated.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidRootClaim.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); // Ensure that bob's balance has remained the same @@ -1206,7 +1484,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { emit WithdrawalFinalized(_withdrawalHash, true); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); - vm.expectRevert(AlreadyFinalized.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_AlreadyFinalized.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } @@ -1324,7 +1602,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Total ETH supply is currently about 120M ETH. uint256 value = bound(_value, 0, 200_000_000 ether); - vm.deal(address(optimismPortal2), value); + vm.deal(address(ethLockbox), value); uint256 gasLimit = bound(_gasLimit, 0, 50_000_000); uint256 nonce = l2ToL1MessagePasser.messageNonce(); @@ -1404,7 +1682,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Total ETH supply is currently about 120M ETH. uint256 value = bound(_value, 0, 200_000_000 ether); - vm.deal(address(optimismPortal2), value); + vm.deal(address(ethLockbox), value); uint256 gasLimit = bound(_gasLimit, 0, 50_000_000); uint256 nonce = l2ToL1MessagePasser.messageNonce(); @@ -1455,7 +1733,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Change the respectedGameType vm.prank(optimismPortal2.guardian()); - optimismPortal2.setRespectedGameType(_newGameType); + anchorStateRegistry.setRespectedGameType(_newGameType); // Withdrawal transaction still finalizable vm.expectCallMinGas(_tx.target, _tx.value, uint64(_tx.gasLimit), _tx.data); @@ -1481,11 +1759,11 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { game.resolve(); vm.prank(optimismPortal2.guardian()); - optimismPortal2.blacklistDisputeGame(IDisputeGame(address(game))); + anchorStateRegistry.blacklistDisputeGame(IDisputeGame(address(game))); vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); - vm.expectRevert(Blacklisted.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidRootClaim.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } @@ -1511,7 +1789,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { game.resolve(); // Attempt to finalize the withdrawal directly after the game resolves. This should fail. - vm.expectRevert("OptimismPortal: output proposal in air-gap"); + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidRootClaim.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); // Finalize the withdrawal transaction. This should succeed. @@ -1546,9 +1824,10 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Set respectedGameTypeUpdatedAt. vm.prank(optimismPortal2.guardian()); - optimismPortal2.setRespectedGameType(GameType.wrap(type(uint32).max)); + anchorStateRegistry.updateRetirementTimestamp(); - vm.expectRevert("OptimismPortal: dispute game created before respected game type was updated"); + // Should revert. + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidRootClaim.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } @@ -1578,7 +1857,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { vm.mockCall(address(game), abi.encodeCall(game.wasRespectedGameTypeWhenCreated, ()), abi.encode(false)); - vm.expectRevert(InvalidGameType.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidRootClaim.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } @@ -1607,9 +1886,11 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Warp past the dispute game finality delay. vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds() + 1); + // Mock the wasRespectedGameTypeWhenCreated call to revert. vm.mockCallRevert(address(game), abi.encodeCall(game.wasRespectedGameTypeWhenCreated, ()), ""); - vm.expectRevert(LegacyGame.selector); + // Should revert. + vm.expectRevert(); // nosemgrep: sol-safety-expectrevert-no-args optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } @@ -1629,13 +1910,13 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { // Attempt to finalize the withdrawal transaction 1 second before the proof has matured. This should fail. vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds()); - vm.expectRevert("OptimismPortal: proven withdrawal has not matured yet"); + vm.expectRevert(IOptimismPortal.OptimismPortal_ProofNotOldEnough.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); // Warp 1 second in the future, past the proof maturity delay, and attempt to finalize the withdrawal. // This should also fail, since the dispute game has not resolved yet. vm.warp(block.timestamp + 1 seconds); - vm.expectRevert(ProposalNotValidated.selector); + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidRootClaim.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); // Finalize the dispute game and attempt to finalize the withdrawal again. This should also fail, since the @@ -1643,7 +1924,7 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { game.resolveClaim(0, 0); game.resolve(); vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds()); - vm.expectRevert("OptimismPortal: output proposal in air-gap"); + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidRootClaim.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); // Warp 1 second in the future, past the air gap dispute game delay, and attempt to finalize the withdrawal. @@ -1652,6 +1933,140 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); assertTrue(optimismPortal2.finalizedWithdrawals(_withdrawalHash)); } + + /// @notice Tests that checkWithdrawal succeeds if the withdrawal has been proven, the dispute + /// game has been finalized, and the root claim is valid. + function test_checkWithdrawal_succeeds() external { + // Prove the withdrawal transaction. + vm.expectEmit(true, true, true, true); + emit WithdrawalProven(_withdrawalHash, alice, bob); + vm.expectEmit(true, true, true, true); + emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Warp past the finalization period. + vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); + + // Mark the dispute game as CHALLENGER_WINS. + vm.mockCall(address(game), abi.encodeCall(game.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)); + + // Mock isGameClaimValid to return true. + vm.mockCall( + address(anchorStateRegistry), abi.encodeCall(anchorStateRegistry.isGameClaimValid, (game)), abi.encode(true) + ); + + // Should succeed. + optimismPortal2.checkWithdrawal(_withdrawalHash, address(this)); + } + + /// @notice Tests that checkWithdrawal reverts if the withdrawal has already been finalized. + function test_checkWithdrawal_ifAlreadyFinalized_reverts() external { + // Prove the withdrawal transaction. + vm.expectEmit(true, true, true, true); + emit WithdrawalProven(_withdrawalHash, alice, bob); + vm.expectEmit(true, true, true, true); + emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Warp and resolve the dispute game. + game.resolveClaim(0, 0); + game.resolve(); + vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); + + // Finalize the withdrawal. + optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); + + // Should revert. + vm.expectRevert(IOptimismPortal.OptimismPortal_AlreadyFinalized.selector); + optimismPortal2.checkWithdrawal(_withdrawalHash, address(this)); + } + + /// @notice Tests that checkWithdrawal reverts if the withdrawal has not been proven. + function test_checkWithdrawal_ifUnproven_reverts() external { + // Don't prove the withdrawal transaction. + // Should revert. + vm.expectRevert(IOptimismPortal.OptimismPortal_Unproven.selector); + optimismPortal2.checkWithdrawal(_withdrawalHash, address(this)); + } + + /// @notice Tests that checkWithdrawal reverts if the proof timestamp is greater than the game + /// creation timestamp. + function testFuzz_checkWithdrawal_ifInvalidProofTimestamp_reverts(uint64 _createdAt) external { + // Prove the withdrawal transaction. + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Mock the game creation timestamp to be greater than the proof timestamp. + _createdAt = uint64(bound(_createdAt, block.timestamp, type(uint64).max)); + vm.mockCall(address(game), abi.encodeCall(game.createdAt, ()), abi.encode(_createdAt)); + + // Warp beyond the proof maturity delay. + vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); + + // Mark the dispute game as CHALLENGER_WINS. + vm.mockCall(address(game), abi.encodeCall(game.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)); + + // Mock isGameClaimValid to return true. + vm.mockCall( + address(anchorStateRegistry), abi.encodeCall(anchorStateRegistry.isGameClaimValid, (game)), abi.encode(true) + ); + + // Should revert. + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidProofTimestamp.selector); + optimismPortal2.checkWithdrawal(_withdrawalHash, address(this)); + } + + /// @notice Tests that checkWithdrawal reverts if the proof timestamp is less than the proof + /// maturity delay. + function test_checkWithdrawal_ifProofNotOldEnough_reverts() external { + // Prove but don't warp ahead past the proof maturity delay. + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Should revert. + vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() - 1); + vm.expectRevert(IOptimismPortal.OptimismPortal_ProofNotOldEnough.selector); + optimismPortal2.checkWithdrawal(_withdrawalHash, address(this)); + } + + /// @notice Tests that checkWithdrawal reverts if the root claim is invalid. + function test_checkWithdrawal_ifInvalidRootClaim_reverts() external { + // Prove the withdrawal. + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Warp past the proof maturity delay. + vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); + + // Mock the game to have CHALLENGER_WINS status + vm.mockCall(address(game), abi.encodeCall(game.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)); + + // Should revert. + vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidRootClaim.selector); + optimismPortal2.checkWithdrawal(_withdrawalHash, address(this)); + } } contract OptimismPortal2_Upgradeable_Test is CommonTest { @@ -1682,7 +2097,7 @@ contract OptimismPortal2_Upgradeable_Test is CommonTest { // The value passed to the initialize must be larger than the last value // that initialize was called with. IProxy(payable(address(optimismPortal2))).upgradeToAndCall( - address(nextImpl), abi.encodeCall(NextImpl.initialize, (2)) + address(nextImpl), abi.encodeCall(NextImpl.initialize, (3)) ); assertEq(IProxy(payable(address(optimismPortal2))).implementation(), address(nextImpl)); @@ -1693,6 +2108,104 @@ contract OptimismPortal2_Upgradeable_Test is CommonTest { } } +contract OptimismPortal2_LiquidityMigration_Test is CommonTest { + function setUp() public override { + super.setUp(); + } + + /// @notice Tests the liquidity migration from the portal to the lockbox reverts if not called by the admin owner. + function testFuzz_migrateLiquidity_notProxyAdminOwner_reverts(address _caller) external { + vm.assume(_caller != optimismPortal2.proxyAdminOwner()); + vm.expectRevert(IOptimismPortal.OptimismPortal_Unauthorized.selector); + vm.prank(_caller); + optimismPortal2.migrateLiquidity(); + } + + /// @notice Tests that the liquidity migration from the portal to the lockbox succeeds. + function test_migrateLiquidity_succeeds(uint256 _portalBalance) external { + _portalBalance = uint256(bound(_portalBalance, 0, type(uint256).max - address(ethLockbox).balance)); + vm.deal(address(optimismPortal2), _portalBalance); + + uint256 lockboxBalanceBefore = address(ethLockbox).balance; + address proxyAdminOwner = optimismPortal2.proxyAdminOwner(); + + vm.expectCall(address(ethLockbox), _portalBalance, abi.encodeCall(ethLockbox.lockETH, ())); + + vm.expectEmit(address(optimismPortal2)); + emit ETHMigrated(address(ethLockbox), _portalBalance); + + vm.prank(proxyAdminOwner); + optimismPortal2.migrateLiquidity(); + + assertEq(address(optimismPortal2).balance, 0); + assertEq(address(ethLockbox).balance, lockboxBalanceBefore + _portalBalance); + } +} + +/// @title OptimismPortal2_upgrade_Test +/// @notice Reusable test for the current upgrade() function in the OptimismPortal2 contract. If +/// the upgrade() function is changed, tests inside of this contract should be updated to +/// reflect the new function. If the upgrade() function is removed, remove the +/// corresponding tests but leave this contract in place so it's easy to add tests back +/// in the future. +contract OptimismPortal2_upgrade_Test is CommonTest { + function setUp() public override { + super.setUp(); + } + + /// @notice Tests that the upgrade() function succeeds. + function test_upgrade_succeeds() external { + // Get the slot for _initialized. + StorageSlot memory slot = ForgeArtifacts.getSlot("OptimismPortal2", "_initialized"); + + // Set the initialized slot to 0. + vm.store(address(optimismPortal2), bytes32(slot.slot), bytes32(0)); + + // Trigger upgrade(). + optimismPortal2.upgrade(IAnchorStateRegistry(address(0xdeadbeef)), IETHLockbox(ethLockbox)); + + // Verify that the initialized slot was updated. + bytes32 initializedSlotAfter = vm.load(address(optimismPortal2), bytes32(slot.slot)); + assertEq(initializedSlotAfter, bytes32(uint256(2))); + + // Verify that the AnchorStateRegistry was set. + assertEq(address(optimismPortal2.anchorStateRegistry()), address(0xdeadbeef)); + } + + /// @notice Tests that the upgrade() function reverts if called a second time. + function test_upgrade_upgradeTwice_reverts() external { + // Get the slot for _initialized. + StorageSlot memory slot = ForgeArtifacts.getSlot("OptimismPortal2", "_initialized"); + + // Set the initialized slot to 0. + vm.store(address(optimismPortal2), bytes32(slot.slot), bytes32(0)); + + // Trigger first upgrade. + optimismPortal2.upgrade(IAnchorStateRegistry(address(0xdeadbeef)), IETHLockbox(ethLockbox)); + + // Try to trigger second upgrade. + vm.expectRevert("Initializable: contract is already initialized"); + optimismPortal2.upgrade(IAnchorStateRegistry(address(0xdeadbeef)), IETHLockbox(ethLockbox)); + } + + /// @notice Tests that the upgrade() function reverts if called after initialization. + function test_upgrade_afterInitialization_reverts() external { + // Get the slot for _initialized. + StorageSlot memory slot = ForgeArtifacts.getSlot("OptimismPortal2", "_initialized"); + + // Slot value should be set to 2 (already initialized). + bytes32 initializedSlotBefore = vm.load(address(optimismPortal2), bytes32(slot.slot)); + assertEq(initializedSlotBefore, bytes32(uint256(2))); + + // AnchorStateRegistry address should be non-zero. + assertNotEq(address(optimismPortal2.anchorStateRegistry()), address(0)); + + // Try to trigger upgrade(). + vm.expectRevert("Initializable: contract is already initialized"); + optimismPortal2.upgrade(IAnchorStateRegistry(address(0xdeadbeef)), IETHLockbox(ethLockbox)); + } +} + /// @title OptimismPortal2_ResourceFuzz_Test /// @dev Test various values of the resource metering config to ensure that deposits cannot be /// broken by changing the config. diff --git a/packages/contracts-bedrock/test/L1/OptimismPortalInterop.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortalInterop.t.sol deleted file mode 100644 index df9100ce88..0000000000 --- a/packages/contracts-bedrock/test/L1/OptimismPortalInterop.t.sol +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.15; - -// Testing -import { CommonTest } from "test/setup/CommonTest.sol"; - -// Libraries -import { Constants } from "src/libraries/Constants.sol"; -import { Predeploys } from "src/libraries/Predeploys.sol"; -import "src/libraries/PortalErrors.sol"; - -// Interfaces -import { IL1BlockInterop, ConfigType } from "interfaces/L2/IL1BlockInterop.sol"; -import { IOptimismPortalInterop } from "interfaces/L1/IOptimismPortalInterop.sol"; - -contract OptimismPortalInterop_Test is CommonTest { - /// @notice Marked virtual to be overridden in - /// test/kontrol/deployment/DeploymentSummary.t.sol - function setUp() public virtual override { - super.enableInterop(); - super.setUp(); - } - - /// @notice Tests that the version function returns a valid string. We avoid testing the - /// specific value of the string as it changes frequently. - function test_version_succeeds() external view { - assert(bytes(_optimismPortalInterop().version()).length > 0); - } - - /// @dev Tests that the config for adding a dependency can be set. - function testFuzz_setConfig_addDependency_succeeds(bytes calldata _value) public { - vm.expectEmit(address(optimismPortal2)); - emitTransactionDeposited({ - _from: Constants.DEPOSITOR_ACCOUNT, - _to: Predeploys.L1_BLOCK_ATTRIBUTES, - _value: 0, - _mint: 0, - _gasLimit: 200_000, - _isCreation: false, - _data: abi.encodeCall(IL1BlockInterop.setConfig, (ConfigType.ADD_DEPENDENCY, _value)) - }); - - vm.prank(address(_optimismPortalInterop().systemConfig())); - _optimismPortalInterop().setConfig(ConfigType.ADD_DEPENDENCY, _value); - } - - /// @dev Tests that setting the add dependency config as not the system config reverts. - function testFuzz_setConfig_addDependencyButNotSystemConfig_reverts(bytes calldata _value) public { - vm.expectRevert(Unauthorized.selector); - _optimismPortalInterop().setConfig(ConfigType.ADD_DEPENDENCY, _value); - } - - /// @dev Tests that the config for removing a dependency can be set. - function testFuzz_setConfig_removeDependency_succeeds(bytes calldata _value) public { - vm.expectEmit(address(optimismPortal2)); - emitTransactionDeposited({ - _from: Constants.DEPOSITOR_ACCOUNT, - _to: Predeploys.L1_BLOCK_ATTRIBUTES, - _value: 0, - _mint: 0, - _gasLimit: 200_000, - _isCreation: false, - _data: abi.encodeCall(IL1BlockInterop.setConfig, (ConfigType.REMOVE_DEPENDENCY, _value)) - }); - - vm.prank(address(_optimismPortalInterop().systemConfig())); - _optimismPortalInterop().setConfig(ConfigType.REMOVE_DEPENDENCY, _value); - } - - /// @dev Tests that setting the remove dependency config as not the system config reverts. - function testFuzz_setConfig_removeDependencyButNotSystemConfig_reverts(bytes calldata _value) public { - vm.expectRevert(Unauthorized.selector); - _optimismPortalInterop().setConfig(ConfigType.REMOVE_DEPENDENCY, _value); - } - - /// @dev Returns the OptimismPortalInterop instance. - function _optimismPortalInterop() internal view returns (IOptimismPortalInterop) { - return IOptimismPortalInterop(payable(address(optimismPortal2))); - } -} diff --git a/packages/contracts-bedrock/test/L1/StandardValidator.t.sol b/packages/contracts-bedrock/test/L1/StandardValidator.t.sol index 39852cf273..d75fb0fc36 100644 --- a/packages/contracts-bedrock/test/L1/StandardValidator.t.sol +++ b/packages/contracts-bedrock/test/L1/StandardValidator.t.sol @@ -5,7 +5,12 @@ pragma solidity 0.8.15; import { Test } from "forge-std/Test.sol"; // Target contract -import { StandardValidatorBase, StandardValidatorV180, StandardValidatorV200 } from "src/L1/StandardValidator.sol"; +import { + StandardValidatorBase, + StandardValidatorV180, + StandardValidatorV200, + StandardValidatorV300 +} from "src/L1/StandardValidator.sol"; // Libraries import { GameType, GameTypes, Hash } from "src/dispute/lib/Types.sol"; @@ -1152,3 +1157,133 @@ contract StandardValidatorV200_Test is StandardValidatorTest { vm.mockCall(address(preimageOracle), abi.encodeCall(ISemver.version, ()), abi.encode("1.1.4")); } } + +contract StandardValidatorV300_Test is StandardValidatorTest { + StandardValidatorV300 validator; + + function getValidator() internal view override returns (StandardValidatorBase) { + return validator; + } + + function validate(bool _allowFailure) internal view override returns (string memory) { + StandardValidatorV300.InputV300 memory input = StandardValidatorV300.InputV300({ + proxyAdmin: proxyAdmin, + sysCfg: systemConfig, + absolutePrestate: absolutePrestate, + l2ChainID: l2ChainID + }); + return validator.validate(input, _allowFailure); + } + + function setUp() public override { + super.setUp(); + + // Deploy validator with all required constructor args + validator = new StandardValidatorV300( + StandardValidatorBase.ImplementationsBase({ + systemConfigImpl: makeAddr("systemConfigImpl"), + optimismPortalImpl: makeAddr("optimismPortalImpl"), + l1CrossDomainMessengerImpl: makeAddr("l1CrossDomainMessengerImpl"), + l1StandardBridgeImpl: makeAddr("l1StandardBridgeImpl"), + l1ERC721BridgeImpl: makeAddr("l1ERC721BridgeImpl"), + optimismMintableERC20FactoryImpl: makeAddr("optimismMintableERC20FactoryImpl"), + disputeGameFactoryImpl: makeAddr("disputeGameFactoryImpl"), + mipsImpl: makeAddr("mipsImpl"), + anchorStateRegistryImpl: makeAddr("anchorStateRegistryImpl"), + delayedWETHImpl: makeAddr("delayedWETHImpl") + }), + superchainConfig, + l1PAOMultisig, + mips, + challenger + ); + } + + /// @notice Tests that validation reverts with error message when allowFailure is false + function test_validate_allowFailureFalse_reverts() public { + _mockValidationCalls(); + + // Mock null implementation for permissioned dispute game + vm.mockCall( + address(disputeGameFactory), + abi.encodeCall(IDisputeGameFactory.gameImpls, (GameTypes.PERMISSIONED_CANNON)), + abi.encode(address(0)) + ); + + // Expect revert with PDDG-10 error message + vm.expectRevert("StandardValidatorV300: PDDG-10"); + validate(false); + } + + /// @notice Tests validation of operator fee settings in SystemConfig + function test_validate_systemConfigOperatorFees_succeeds() public { + // Test invalid operator fee scalar + _mockValidationCalls(); + vm.mockCall(address(systemConfig), abi.encodeCall(ISystemConfig.operatorFeeScalar, ()), abi.encode(1)); + assertEq("SYSCON-110", validate(true)); + + // Test invalid operator fee constant + _mockValidationCalls(); + vm.mockCall(address(systemConfig), abi.encodeCall(ISystemConfig.operatorFeeConstant, ()), abi.encode(1)); + assertEq("SYSCON-120", validate(true)); + + // Test both invalid + _mockValidationCalls(); + vm.mockCall(address(systemConfig), abi.encodeCall(ISystemConfig.operatorFeeScalar, ()), abi.encode(1)); + vm.mockCall(address(systemConfig), abi.encodeCall(ISystemConfig.operatorFeeConstant, ()), abi.encode(1)); + assertEq("SYSCON-110,SYSCON-120", validate(true)); + + // Test both valid (should be included in _mockValidationCalls, but let's be explicit) + _mockValidationCalls(); + vm.mockCall(address(systemConfig), abi.encodeCall(ISystemConfig.operatorFeeScalar, ()), abi.encode(0)); + vm.mockCall(address(systemConfig), abi.encodeCall(ISystemConfig.operatorFeeConstant, ()), abi.encode(0)); + assertEq("", validate(true)); + } + + function _testDisputeGame( + string memory errorPrefix, + address _disputeGame, + address _asr, + address _weth, + GameType _gameType + ) + public + override + { + super._testDisputeGame(errorPrefix, _disputeGame, _asr, _weth, _gameType); + + // Test invalid anchor state registry implementation + _mockValidationCalls(); + vm.mockCall( + address(proxyAdmin), + abi.encodeCall(IProxyAdmin.getProxyImplementation, (address(_asr))), + abi.encode(address(0xbad)) + ); + assertEq(string.concat(errorPrefix, "-ANCHORP-20"), validate(true)); + } + + function _mockValidationCalls() internal virtual override { + super._mockValidationCalls(); + + // Mock operator fee calls with valid values + vm.mockCall(address(systemConfig), abi.encodeCall(ISystemConfig.operatorFeeScalar, ()), abi.encode(0)); + vm.mockCall(address(systemConfig), abi.encodeCall(ISystemConfig.operatorFeeConstant, ()), abi.encode(0)); + + // Override version numbers for V300 + vm.mockCall(address(l1ERC721Bridge), abi.encodeCall(ISemver.version, ()), abi.encode("2.4.0")); + vm.mockCall(address(optimismPortal), abi.encodeCall(ISemver.version, ()), abi.encode("3.14.0")); + vm.mockCall(address(systemConfig), abi.encodeCall(ISemver.version, ()), abi.encode("2.5.0")); + vm.mockCall(address(optimismMintableERC20Factory), abi.encodeCall(ISemver.version, ()), abi.encode("1.10.1")); + vm.mockCall(address(l1CrossDomainMessenger), abi.encodeCall(ISemver.version, ()), abi.encode("2.6.0")); + vm.mockCall(address(l1StandardBridge), abi.encodeCall(ISemver.version, ()), abi.encode("2.3.0")); + vm.mockCall(address(disputeGameFactory), abi.encodeCall(ISemver.version, ()), abi.encode("1.0.1")); + vm.mockCall(address(permissionedASR), abi.encodeCall(ISemver.version, ()), abi.encode("2.2.2")); + vm.mockCall(address(permissionedDelayedWETH), abi.encodeCall(ISemver.version, ()), abi.encode("1.3.0")); + vm.mockCall(address(permissionlessASR), abi.encodeCall(ISemver.version, ()), abi.encode("2.2.2")); + vm.mockCall(address(permissionlessDelayedWETH), abi.encodeCall(ISemver.version, ()), abi.encode("1.3.0")); + vm.mockCall(address(mips), abi.encodeCall(ISemver.version, ()), abi.encode("1.0.0")); + vm.mockCall(address(permissionedDisputeGame), abi.encodeCall(ISemver.version, ()), abi.encode("1.4.1")); + vm.mockCall(address(permissionlessDisputeGame), abi.encodeCall(ISemver.version, ()), abi.encode("1.4.1")); + vm.mockCall(address(preimageOracle), abi.encodeCall(ISemver.version, ()), abi.encode("1.1.4")); + } +} diff --git a/packages/contracts-bedrock/test/L1/SystemConfig.t.sol b/packages/contracts-bedrock/test/L1/SystemConfig.t.sol index fbae6b4152..afd3da924e 100644 --- a/packages/contracts-bedrock/test/L1/SystemConfig.t.sol +++ b/packages/contracts-bedrock/test/L1/SystemConfig.t.sol @@ -4,6 +4,9 @@ pragma solidity 0.8.15; // Testing import { CommonTest } from "test/setup/CommonTest.sol"; +// Scripts +import { ForgeArtifacts, StorageSlot } from "scripts/libraries/ForgeArtifacts.sol"; + // Libraries import { Constants } from "src/libraries/Constants.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; @@ -71,7 +74,6 @@ contract SystemConfig_Initialize_Test is SystemConfig_Init { assertEq(address(impl.l1CrossDomainMessenger()), address(0)); assertEq(address(impl.l1ERC721Bridge()), address(0)); assertEq(address(impl.l1StandardBridge()), address(0)); - assertEq(address(impl.disputeGameFactory()), address(0)); assertEq(address(impl.optimismPortal()), address(0)); assertEq(address(impl.optimismMintableERC20Factory()), address(0)); } @@ -108,12 +110,11 @@ contract SystemConfig_Initialize_Test is SystemConfig_Init { assertEq(addrs.l1ERC721Bridge, address(l1ERC721Bridge)); assertEq(address(systemConfig.l1StandardBridge()), address(l1StandardBridge)); assertEq(addrs.l1StandardBridge, address(l1StandardBridge)); - assertEq(address(systemConfig.disputeGameFactory()), address(disputeGameFactory)); - assertEq(addrs.disputeGameFactory, address(disputeGameFactory)); assertEq(address(systemConfig.optimismPortal()), address(optimismPortal2)); assertEq(addrs.optimismPortal, address(optimismPortal2)); assertEq(address(systemConfig.optimismMintableERC20Factory()), address(optimismMintableERC20Factory)); assertEq(addrs.optimismMintableERC20Factory, address(optimismMintableERC20Factory)); + assertNotEq(systemConfig.l2ChainId(), 0); } } @@ -142,10 +143,10 @@ contract SystemConfig_Initialize_TestFail is SystemConfig_Initialize_Test { l1CrossDomainMessenger: address(0), l1ERC721Bridge: address(0), l1StandardBridge: address(0), - disputeGameFactory: address(0), optimismPortal: address(0), optimismMintableERC20Factory: address(0) - }) + }), + _l2ChainId: 1234 }); } @@ -171,10 +172,10 @@ contract SystemConfig_Initialize_TestFail is SystemConfig_Initialize_Test { l1CrossDomainMessenger: address(0), l1ERC721Bridge: address(0), l1StandardBridge: address(0), - disputeGameFactory: address(0), optimismPortal: address(0), optimismMintableERC20Factory: address(0) - }) + }), + _l2ChainId: 1234 }); assertEq(systemConfig.startBlock(), block.number); } @@ -201,10 +202,10 @@ contract SystemConfig_Initialize_TestFail is SystemConfig_Initialize_Test { l1CrossDomainMessenger: address(0), l1ERC721Bridge: address(0), l1StandardBridge: address(0), - disputeGameFactory: address(0), optimismPortal: address(0), optimismMintableERC20Factory: address(0) - }) + }), + _l2ChainId: 1234 }); assertEq(systemConfig.startBlock(), 1); } @@ -315,10 +316,10 @@ contract SystemConfig_Init_ResourceConfig is SystemConfig_Init { l1CrossDomainMessenger: address(0), l1ERC721Bridge: address(0), l1StandardBridge: address(0), - disputeGameFactory: address(0), optimismPortal: address(0), optimismMintableERC20Factory: address(0) - }) + }), + _l2ChainId: 1234 }); } } @@ -480,3 +481,73 @@ contract SystemConfig_Setters_Test is SystemConfig_Init { assertEq(systemConfig.eip1559Elasticity(), _elasticity); } } + +/// @title SystemConfig_upgrade_Test +/// @notice Reusable test for the current upgrade() function in the SystemConfig contract. If +/// the upgrade() function is changed, tests inside of this contract should be updated to +/// reflect the new function. If the upgrade() function is removed, remove the +/// corresponding tests but leave this contract in place so it's easy to add tests back +/// in the future. +contract SystemConfig_upgrade_Test is SystemConfig_Init { + /// @notice Tests that the upgrade() function succeeds. + function test_upgrade_succeeds() external { + // Get the slot for _initialized. + StorageSlot memory slot = ForgeArtifacts.getSlot("SystemConfig", "_initialized"); + + // Set the initialized slot to 0. + vm.store(address(systemConfig), bytes32(slot.slot), bytes32(0)); + + // Verify the initial dispute game factory slot is non-zero. + // We set a value here since it seems this defaults to zero. + bytes32 disputeGameFactorySlot = bytes32(uint256(keccak256("systemconfig.disputegamefactory")) - 1); + vm.store(address(systemConfig), disputeGameFactorySlot, bytes32(uint256(1))); + assertNotEq(systemConfig.disputeGameFactory(), address(0)); + assertNotEq(vm.load(address(systemConfig), disputeGameFactorySlot), bytes32(0)); + + // Trigger upgrade(). + systemConfig.upgrade(1234); + + // Verify that the initialized slot was updated. + bytes32 initializedSlotAfter = vm.load(address(systemConfig), bytes32(slot.slot)); + assertEq(initializedSlotAfter, bytes32(uint256(2))); + + // Verify that the l2ChainId was updated. + assertEq(systemConfig.l2ChainId(), 1234); + + // Verify that the dispute game factory address was cleared. + assertEq(vm.load(address(systemConfig), disputeGameFactorySlot), bytes32(0)); + } + + /// @notice Tests that the upgrade() function reverts if called a second time. + function test_upgrade_upgradeTwice_reverts() external { + // Get the slot for _initialized. + StorageSlot memory slot = ForgeArtifacts.getSlot("SystemConfig", "_initialized"); + + // Set the initialized slot to 0. + vm.store(address(systemConfig), bytes32(slot.slot), bytes32(0)); + + // Trigger first upgrade. + systemConfig.upgrade(1234); + + // Try to trigger second upgrade. + vm.expectRevert("Initializable: contract is already initialized"); + systemConfig.upgrade(1234); + } + + /// @notice Tests that the upgrade() function reverts if called after initialization. + function test_upgrade_afterInitialization_reverts() external { + // Get the slot for _initialized. + StorageSlot memory slot = ForgeArtifacts.getSlot("SystemConfig", "_initialized"); + + // Slot value should be set to 2 (already initialized). + bytes32 initializedSlotBefore = vm.load(address(systemConfig), bytes32(slot.slot)); + assertEq(initializedSlotBefore, bytes32(uint256(2))); + + // l2ChainId should be non-zero. + assertNotEq(systemConfig.l2ChainId(), 0); + + // Try to trigger upgrade(). + vm.expectRevert("Initializable: contract is already initialized"); + systemConfig.upgrade(1234); + } +} diff --git a/packages/contracts-bedrock/test/L1/SystemConfigInterop.t.sol b/packages/contracts-bedrock/test/L1/SystemConfigInterop.t.sol deleted file mode 100644 index 2f953ef741..0000000000 --- a/packages/contracts-bedrock/test/L1/SystemConfigInterop.t.sol +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.15; - -// Testing -import { CommonTest } from "test/setup/CommonTest.sol"; - -// Libraries -import { StaticConfig } from "src/libraries/StaticConfig.sol"; - -// Interfaces -import { ISystemConfigInterop } from "interfaces/L1/ISystemConfigInterop.sol"; -import { IOptimismPortalInterop } from "interfaces/L1/IOptimismPortalInterop.sol"; -import { ConfigType } from "interfaces/L2/IL1BlockInterop.sol"; - -contract SystemConfigInterop_Test is CommonTest { - /// @notice Marked virtual to be overridden in - /// test/kontrol/deployment/DeploymentSummary.t.sol - function setUp() public virtual override { - super.enableInterop(); - super.setUp(); - } - - /// @notice Tests that the version function returns a valid string. We avoid testing the - /// specific value of the string as it changes frequently. - function test_version_succeeds() external view { - assert(bytes(_systemConfigInterop().version()).length > 0); - } - - /// @dev Tests that a dependency can be added. - function testFuzz_addDependency_succeeds(uint256 _chainId) public { - vm.expectCall( - address(optimismPortal2), - abi.encodeCall( - IOptimismPortalInterop.setConfig, - (ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(_chainId)) - ) - ); - - vm.prank(_systemConfigInterop().dependencyManager()); - _systemConfigInterop().addDependency(_chainId); - } - - /// @dev Tests that adding a dependency as not the dependency manager reverts. - function testFuzz_addDependency_notDependencyManager_reverts(uint256 _chainId) public { - require(alice != _systemConfigInterop().dependencyManager(), "SystemConfigInterop_Test: 100"); - vm.expectRevert("SystemConfig: caller is not the dependency manager"); - vm.prank(alice); - _systemConfigInterop().addDependency(_chainId); - } - - /// @dev Tests that a dependency can be removed. - function testFuzz_removeDependency_succeeds(uint256 _chainId) public { - vm.expectCall( - address(optimismPortal2), - abi.encodeCall( - IOptimismPortalInterop.setConfig, - (ConfigType.REMOVE_DEPENDENCY, StaticConfig.encodeRemoveDependency(_chainId)) - ) - ); - - vm.prank(_systemConfigInterop().dependencyManager()); - _systemConfigInterop().removeDependency(_chainId); - } - - /// @dev Tests that removing a dependency as not the dependency manager reverts. - function testFuzz_removeDependency_notDependencyManager_reverts(uint256 _chainId) public { - require(alice != _systemConfigInterop().dependencyManager(), "SystemConfigInterop_Test: 100"); - vm.expectRevert("SystemConfig: caller is not the dependency manager"); - vm.prank(alice); - _systemConfigInterop().removeDependency(_chainId); - } - - /// @dev Returns the SystemConfigInterop instance. - function _systemConfigInterop() internal view returns (ISystemConfigInterop) { - return ISystemConfigInterop(address(systemConfig)); - } -} diff --git a/packages/contracts-bedrock/test/L2/CrossL2Inbox.t.sol b/packages/contracts-bedrock/test/L2/CrossL2Inbox.t.sol index 6950162546..8aea2454f6 100644 --- a/packages/contracts-bedrock/test/L2/CrossL2Inbox.t.sol +++ b/packages/contracts-bedrock/test/L2/CrossL2Inbox.t.sol @@ -4,267 +4,142 @@ pragma solidity 0.8.25; // Testing utilities import { Test } from "forge-std/Test.sol"; -// Libraries -import { Predeploys } from "src/libraries/Predeploys.sol"; -import { TransientContext } from "src/libraries/TransientContext.sol"; +// Interfaces +import { ICrossL2Inbox } from "interfaces/L2/ICrossL2Inbox.sol"; // Target contracts -import { - CrossL2Inbox, - Identifier, - NotEntered, - NoExecutingDeposits, - NotDepositor, - InteropStartAlreadySet -} from "src/L2/CrossL2Inbox.sol"; -import { IL1BlockInterop } from "interfaces/L2/IL1BlockInterop.sol"; - -/// @title CrossL2InboxWithModifiableTransientStorage -/// @dev CrossL2Inbox contract with methods to modify the transient storage. -/// This is used to test the transient storage of CrossL2Inbox. -contract CrossL2InboxWithModifiableTransientStorage is CrossL2Inbox { - /// @dev Increments call depth in transient storage. - function increment() external { - TransientContext.increment(); - } - - /// @dev Sets origin in transient storage. - /// @param _origin Origin to set. - function setOrigin(address _origin) external { - TransientContext.set(ORIGIN_SLOT, uint160(_origin)); - } - - /// @dev Sets block number in transient storage. - /// @param _blockNumber Block number to set. - function setBlockNumber(uint256 _blockNumber) external { - TransientContext.set(BLOCK_NUMBER_SLOT, _blockNumber); - } - - /// @dev Sets log index in transient storage. - /// @param _logIndex Log index to set. - function setLogIndex(uint256 _logIndex) external { - TransientContext.set(LOG_INDEX_SLOT, _logIndex); - } - - /// @dev Sets timestamp in transient storage. - /// @param _timestamp Timestamp to set. - function setTimestamp(uint256 _timestamp) external { - TransientContext.set(TIMESTAMP_SLOT, _timestamp); - } - - /// @dev Sets chain ID in transient storage. - /// @param _chainId Chain ID to set. - function setChainId(uint256 _chainId) external { - TransientContext.set(CHAINID_SLOT, _chainId); - } -} +import { CrossL2InboxWithSlotWarming, Identifier } from "test/mocks/CrossL2InboxWithSlotWarming.sol"; /// @title CrossL2InboxTest /// @dev Contract for testing the CrossL2Inbox contract. contract CrossL2InboxTest is Test { - /// @dev Selector for the `isInDependencySet` method of the L1Block contract. - bytes4 constant L1BlockIsInDependencySetSelector = bytes4(keccak256("isInDependencySet(uint256)")); - - /// @dev Storage slot that the interop start timestamp is stored at. - /// Equal to bytes32(uint256(keccak256("crossl2inbox.interopstart")) - 1) - bytes32 internal constant INTEROP_START_SLOT = bytes32(uint256(keccak256("crossl2inbox.interopstart")) - 1); + event ExecutingMessage(bytes32 indexed msgHash, Identifier id); /// @dev CrossL2Inbox contract instance. - CrossL2Inbox crossL2Inbox; - - // interop start timestamp - uint256 interopStartTime = 420; - - /// @dev The address that represents the system caller responsible for L1 attributes - /// transactions. - address internal constant DEPOSITOR_ACCOUNT = 0xDeaDDEaDDeAdDeAdDEAdDEaddeAddEAdDEAd0001; + CrossL2InboxWithSlotWarming crossL2Inbox; /// @dev Sets up the test suite. - function setUp() public { - // Deploy the L2ToL2CrossDomainMessenger contract - vm.etch(Predeploys.CROSS_L2_INBOX, address(new CrossL2InboxWithModifiableTransientStorage()).code); - crossL2Inbox = CrossL2Inbox(Predeploys.CROSS_L2_INBOX); - } - - modifier setInteropStart() { - // Set interop start - vm.store(address(crossL2Inbox), INTEROP_START_SLOT, bytes32(interopStartTime)); - - // Set timestamp to be after interop start - vm.warp(interopStartTime + 1 hours); - - _; - } - - /// @dev Tests that the setInteropStart function updates the INTEROP_START_SLOT storage slot correctly - function testFuzz_setInteropStart_succeeds(uint256 time) external { - // Jump to time. - vm.warp(time); - - // Impersonate the depositor account. - vm.prank(DEPOSITOR_ACCOUNT); - - // Set interop start. - crossL2Inbox.setInteropStart(); - - // Check that the storage slot was set correctly and the public getter function returns the right value. - assertEq(crossL2Inbox.interopStart(), time); - assertEq(uint256(vm.load(address(crossL2Inbox), INTEROP_START_SLOT)), time); - } - - /// @dev Tests that the setInteropStart function reverts when the caller is not the DEPOSITOR_ACCOUNT. - function test_setInteropStart_notDepositorAccount_reverts() external { - // Expect revert with OnlyDepositorAccount selector - vm.expectRevert(NotDepositor.selector); - - // Call setInteropStart function - crossL2Inbox.setInteropStart(); + function setUp() public virtual { + crossL2Inbox = new CrossL2InboxWithSlotWarming(); } - /// @dev Tests that the setInteropStart function reverts if called when already set - function test_setInteropStart_interopStartAlreadySet_reverts() external { - // Impersonate the depositor account. - vm.startPrank(DEPOSITOR_ACCOUNT); + /// Test that `validateMessage` reverts when the slot is not warm. + function testFuzz_validateMessage_accessList_reverts(Identifier memory _id, bytes32 _messageHash) external { + // Bound values types to ensure they are not too large + _id.blockNumber = bound(_id.blockNumber, 0, type(uint64).max); + _id.logIndex = bound(_id.logIndex, 0, type(uint32).max); + _id.timestamp = bound(_id.timestamp, 0, type(uint64).max); - // Call setInteropStart function - crossL2Inbox.setInteropStart(); - - // Expect revert with InteropStartAlreadySet selector if called a second time - vm.expectRevert(InteropStartAlreadySet.selector); - - // Call setInteropStart function again - crossL2Inbox.setInteropStart(); - } - - function testFuzz_validateMessage_succeeds(Identifier memory _id, bytes32 _messageHash) external setInteropStart { - // Ensure that the id's timestamp is valid (less than or equal to the current block timestamp and greater than - // interop start time) - _id.timestamp = bound(_id.timestamp, interopStartTime + 1, block.timestamp); - - // Ensure that the chain ID is in the dependency set - vm.mockCall({ - callee: Predeploys.L1_BLOCK_ATTRIBUTES, - data: abi.encodeCall(IL1BlockInterop.isInDependencySet, (_id.chainId)), - returnData: abi.encode(true) - }); - - // Ensure is not a deposit transaction - vm.mockCall({ - callee: Predeploys.L1_BLOCK_ATTRIBUTES, - data: abi.encodeCall(IL1BlockInterop.isDeposit, ()), - returnData: abi.encode(false) - }); - - // Look for the emit ExecutingMessage event - vm.expectEmit(Predeploys.CROSS_L2_INBOX); - emit CrossL2Inbox.ExecutingMessage(_messageHash, _id); - - // Call the validateMessage function + // Expect revert + vm.expectRevert(ICrossL2Inbox.NotInAccessList.selector); crossL2Inbox.validateMessage(_id, _messageHash); } - function testFuzz_validateMessage_isDeposit_reverts(Identifier calldata _id, bytes32 _messageHash) external { - // Ensure it is a deposit transaction - vm.mockCall({ - callee: Predeploys.L1_BLOCK_ATTRIBUTES, - data: abi.encodeCall(IL1BlockInterop.isDeposit, ()), - returnData: abi.encode(true) - }); - - // Expect a revert with the NoExecutingDeposits selector - vm.expectRevert(NoExecutingDeposits.selector); + /// Test that `validateMessage` succeeds when the slot for the message checksum is warm. + function testFuzz_validateMessage_succeeds(Identifier memory _id, bytes32 _messageHash) external { + // Bound values types to ensure they are not too large + _id.blockNumber = bound(_id.blockNumber, 0, type(uint64).max); + _id.logIndex = bound(_id.logIndex, 0, type(uint32).max); + _id.timestamp = bound(_id.timestamp, 0, type(uint64).max); - // Call the validateMessage function - crossL2Inbox.validateMessage(_id, _messageHash); - } + // Warm the slot + bytes32 slot = crossL2Inbox.calculateChecksum(_id, _messageHash); + crossL2Inbox.warmSlot(slot); - /// @dev Tests that the `origin` function returns the correct value. - function testFuzz_origin_succeeds(address _origin) external { - // Increment the call depth to prevent NotEntered revert - CrossL2InboxWithModifiableTransientStorage(Predeploys.CROSS_L2_INBOX).increment(); - // Set origin in the transient storage - CrossL2InboxWithModifiableTransientStorage(Predeploys.CROSS_L2_INBOX).setOrigin(_origin); - // Check that the `origin` function returns the correct value - assertEq(crossL2Inbox.origin(), _origin); - } - - /// @dev Tests that the `origin` function reverts when not entered. - function test_origin_notEntered_reverts() external { - // Expect a revert with the NotEntered selector - vm.expectRevert(NotEntered.selector); - // Call the `origin` function - crossL2Inbox.origin(); - } + // Expect `ExecutingMessage` event to be emitted + vm.expectEmit(address(crossL2Inbox)); + emit ExecutingMessage(_messageHash, _id); - /// @dev Tests that the `blockNumber` function returns the correct value. - function testFuzz_blockNumber_succeeds(uint256 _blockNumber) external { - // Increment the call depth to prevent NotEntered revert - CrossL2InboxWithModifiableTransientStorage(Predeploys.CROSS_L2_INBOX).increment(); - // Set blockNumber in the transient storage - CrossL2InboxWithModifiableTransientStorage(Predeploys.CROSS_L2_INBOX).setBlockNumber(_blockNumber); - // Check that the `blockNumber` function returns the correct value - assertEq(crossL2Inbox.blockNumber(), _blockNumber); - } - - /// @dev Tests that the `blockNumber` function reverts when not entered. - function test_blockNumber_notEntered_reverts() external { - // Expect a revert with the NotEntered selector - vm.expectRevert(NotEntered.selector); - // Call the `blockNumber` function - crossL2Inbox.blockNumber(); - } - - /// @dev Tests that the `logIndex` function returns the correct value. - function testFuzz_logIndex_succeeds(uint256 _logIndex) external { - // Increment the call depth to prevent NotEntered revert - CrossL2InboxWithModifiableTransientStorage(Predeploys.CROSS_L2_INBOX).increment(); - // Set logIndex in the transient storage - CrossL2InboxWithModifiableTransientStorage(Predeploys.CROSS_L2_INBOX).setLogIndex(_logIndex); - // Check that the `logIndex` function returns the correct value - assertEq(crossL2Inbox.logIndex(), _logIndex); - } - - /// @dev Tests that the `logIndex` function reverts when not entered. - function test_logIndex_notEntered_reverts() external { - // Expect a revert with the NotEntered selector - vm.expectRevert(NotEntered.selector); - // Call the `logIndex` function - crossL2Inbox.logIndex(); - } - - /// @dev Tests that the `timestamp` function returns the correct value. - function testFuzz_timestamp_succeeds(uint256 _timestamp) external { - // Increment the call depth to prevent NotEntered revert - CrossL2InboxWithModifiableTransientStorage(Predeploys.CROSS_L2_INBOX).increment(); - // Set timestamp in the transient storage - CrossL2InboxWithModifiableTransientStorage(Predeploys.CROSS_L2_INBOX).setTimestamp(_timestamp); - // Check that the `timestamp` function returns the correct value - assertEq(crossL2Inbox.timestamp(), _timestamp); - } - - /// @dev Tests that the `timestamp` function reverts when not entered. - function test_timestamp_notEntered_reverts() external { - // Expect a revert with the NotEntered selector - vm.expectRevert(NotEntered.selector); - // Call the `timestamp` function - crossL2Inbox.timestamp(); - } - - /// @dev Tests that the `chainId` function returns the correct value. - function testFuzz_chainId_succeeds(uint256 _chainId) external { - // Increment the call depth to prevent NotEntered revert - CrossL2InboxWithModifiableTransientStorage(Predeploys.CROSS_L2_INBOX).increment(); - // Set chainId in the transient storage - CrossL2InboxWithModifiableTransientStorage(Predeploys.CROSS_L2_INBOX).setChainId(_chainId); - // Check that the `chainId` function returns the correct value - assertEq(crossL2Inbox.chainId(), _chainId); + // Validate the message + crossL2Inbox.validateMessage(_id, _messageHash); } - /// @dev Tests that the `chainId` function reverts when not entered. - function test_chainId_notEntered_reverts() external { - // Expect a revert with the NotEntered selector - vm.expectRevert(NotEntered.selector); - // Call the `chainId` function - crossL2Inbox.chainId(); + /// Test that calculate checcksum reverts when the block number is greater than 2^64. + function testFuzz_calculateChecksum_withTooLargeBlockNumber_reverts( + Identifier memory _id, + bytes32 _messageHash + ) + external + { + // Set to the 2**64 + 1 + _id.blockNumber = 18446744073709551615 + 1; + vm.expectRevert(ICrossL2Inbox.BlockNumberTooHigh.selector); + crossL2Inbox.calculateChecksum(_id, _messageHash); + } + + /// Test that calculate checcksum reverts when the log index is greater than 2^32. + function testFuzz_calculateChecksum_withTooLargeLogIndex_reverts( + Identifier memory _id, + bytes32 _messageHash + ) + external + { + _id.blockNumber = bound(_id.blockNumber, 0, type(uint64).max); + + // Set to the 2**32 + 1 + _id.logIndex = 4294967295 + 1; + vm.expectRevert(ICrossL2Inbox.LogIndexTooHigh.selector); + crossL2Inbox.calculateChecksum(_id, _messageHash); + } + + /// Test that calculate checcksum reverts when the timestamp is greater than 2^64. + function testFuzz_calculateChecksum_withTooLargeTimestamp_reverts( + Identifier memory _id, + bytes32 _messageHash + ) + external + { + _id.blockNumber = bound(_id.blockNumber, 0, type(uint64).max); + _id.logIndex = bound(_id.logIndex, 0, type(uint32).max); + + // Set to the 2**64 + 1 + _id.timestamp = 18446744073709551615 + 1; + vm.expectRevert(ICrossL2Inbox.TimestampTooHigh.selector); + crossL2Inbox.calculateChecksum(_id, _messageHash); + } + + /// Test that `calculateChecksum` succeeds matching the expected calculated checksum. + /// Using a hardcoded checksum manually calculated and verified. + function test_calculateChecksum_succeeds() external view { + Identifier memory id = Identifier( + address(0), + uint64(0xa1a2a3a4a5a6a7a8), + uint32(0xb1b2b3b4), + uint64(0xc1c2c3c4c5c6c7c8), + uint256(0xd1d2d3d4d5d6d7d8) + ); + + // Calculate the expected checksum. + bytes32 messageHash = 0x8017559a85b12c04b14a1a425d53486d1015f833714a09bd62f04152a7e2ae9b; + bytes32 checksum = crossL2Inbox.calculateChecksum(id, messageHash); + bytes32 expectedChecksum = 0x03139ddd21106abad4bb82800fedfa3a103f53f242c2d5b7615b0baad8379531; + + // Expect it to match + assertEq(checksum, expectedChecksum); + } + + /// Test that `_isWarm` returns the correct value when the slot is not warm. + function testFuzz_isWarm_whenSlotIsNotInAccessList_succeeds(bytes32 _slot) external view { + // Assert that the slot is not warm + (bool isWarm, uint256 value) = crossL2Inbox.isWarm(_slot); + assertEq(isWarm, false); + assertEq(value, 0); + } + + /// Test that `_isWarm` returns the correct value when the slot is warm. + function testFuzz_isWarm_whenSlotIsWarm_succeeds(Identifier memory _id, bytes32 _messageHash) external view { + // Bound values types to ensure they are not too large + _id.blockNumber = bound(_id.blockNumber, 0, type(uint64).max); + _id.logIndex = bound(_id.logIndex, 0, type(uint32).max); + _id.timestamp = bound(_id.timestamp, 0, type(uint64).max); + + // Warm the slot + bytes32 slot = crossL2Inbox.calculateChecksum(_id, _messageHash); + crossL2Inbox.warmSlot(slot); + + // Assert that the slot is warm + (bool isWarm, uint256 value) = crossL2Inbox.isWarm(slot); + assertEq(isWarm, true); + assertEq(value, 0); } } diff --git a/packages/contracts-bedrock/test/L2/ETHLiquidity.t.sol b/packages/contracts-bedrock/test/L2/ETHLiquidity.t.sol index bae3601b51..83ec5461c3 100644 --- a/packages/contracts-bedrock/test/L2/ETHLiquidity.t.sol +++ b/packages/contracts-bedrock/test/L2/ETHLiquidity.t.sol @@ -100,8 +100,9 @@ contract ETHLiquidity_Test is CommonTest { // Assume _amount = bound(_amount, 0, type(uint248).max - 1); - // Arrange - // Nothing to arrange. + // Get balances before + uint256 superchainWethBalanceBefore = superchainWeth.balanceOf(address(ethLiquidity)); + uint256 ethLiquidityBalanceBefore = address(ethLiquidity).balance; // Act vm.expectEmit(address(ethLiquidity)); @@ -110,9 +111,9 @@ contract ETHLiquidity_Test is CommonTest { ethLiquidity.mint(_amount); // Assert - assertEq(address(superchainWeth).balance, _amount); - assertEq(address(ethLiquidity).balance, STARTING_LIQUIDITY_BALANCE - _amount); - assertEq(superchainWeth.balanceOf(address(ethLiquidity)), 0); + assertEq(address(superchainWeth).balance, superchainWethBalanceBefore + _amount); + assertEq(address(ethLiquidity).balance, ethLiquidityBalanceBefore - _amount); + assertEq(superchainWeth.balanceOf(address(ethLiquidity)), superchainWethBalanceBefore); } /// @notice Tests that the mint function always reverts when called by an unauthorized caller. diff --git a/packages/contracts-bedrock/test/L2/L1BlockInterop.t.sol b/packages/contracts-bedrock/test/L2/L1BlockInterop.t.sol deleted file mode 100644 index c569f42025..0000000000 --- a/packages/contracts-bedrock/test/L2/L1BlockInterop.t.sol +++ /dev/null @@ -1,271 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.15; - -// Testing -import { CommonTest } from "test/setup/CommonTest.sol"; - -// Libraries -import { StaticConfig } from "src/libraries/StaticConfig.sol"; -import { Predeploys } from "src/libraries/Predeploys.sol"; -import "src/libraries/L1BlockErrors.sol"; - -// Interfaces -import { IL1BlockInterop, ConfigType } from "interfaces/L2/IL1BlockInterop.sol"; - -contract L1BlockInteropTest is CommonTest { - event GasPayingTokenSet(address indexed token, uint8 indexed decimals, bytes32 name, bytes32 symbol); - event DependencyAdded(uint256 indexed chainId); - event DependencyRemoved(uint256 indexed chainId); - - modifier prankDepositor() { - vm.startPrank(_l1BlockInterop().DEPOSITOR_ACCOUNT()); - _; - vm.stopPrank(); - } - - /// @notice Marked virtual to be overridden in - /// test/kontrol/deployment/DeploymentSummary.t.sol - function setUp() public virtual override { - super.enableInterop(); - super.setUp(); - } - - /// @dev Tests that an arbitrary chain ID can be added to the dependency set. - function testFuzz_isInDependencySet_succeeds(uint256 _chainId) public prankDepositor { - vm.assume(_chainId != block.chainid); - - _l1BlockInterop().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(_chainId)); - - assertTrue(_l1BlockInterop().isInDependencySet(_chainId)); - } - - /// @dev Tests that `isInDependencySet` returns true when the chain's chain ID is passed as the input. - function test_isInDependencySet_chainChainId_succeeds() public view { - assertTrue(_l1BlockInterop().isInDependencySet(block.chainid)); - } - - /// @dev Tests that `isInDependencySet` reverts when the input chain ID is not in the dependency set - /// and is not the chain's chain ID. - function testFuzz_isInDependencySet_notDependency_reverts(uint256 _chainId) public view { - vm.assume(_chainId != block.chainid); - - // Check that the chain ID is not in the dependency set - assertFalse(_l1BlockInterop().isInDependencySet(_chainId)); - } - - /// @dev Tests that `isInDependencySet` returns false when the dependency set is empty. - function testFuzz_isInDependencySet_dependencySetEmpty_succeeds(uint256 _chainId) public view { - vm.assume(_chainId != block.chainid); - - assertEq(_l1BlockInterop().dependencySetSize(), 0); - - assertFalse(_l1BlockInterop().isInDependencySet(_chainId)); - } - - /// @dev Tests that the dependency set size is correct when adding an arbitrary number of chain IDs. - function testFuzz_dependencySetSize_succeeds(uint8 _dependencySetSize) public prankDepositor { - uint256 uniqueCount = 0; - - for (uint256 i = 0; i < _dependencySetSize; i++) { - if (i == block.chainid) continue; - _l1BlockInterop().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(i)); - uniqueCount++; - } - - assertEq(_l1BlockInterop().dependencySetSize(), uniqueCount); - } - - /// @dev Tests that the dependency set size is correct when the dependency set is empty. - function test_dependencySetSize_dependencySetEmpty_succeeds() public view { - assertEq(_l1BlockInterop().dependencySetSize(), 0); - } - - /// @dev Tests that the config for adding a dependency can be set. - function testFuzz_setConfig_addDependency_succeeds(uint256 _chainId) public prankDepositor { - vm.assume(_chainId != block.chainid); - - vm.expectEmit(address(l1Block)); - emit DependencyAdded(_chainId); - - _l1BlockInterop().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(_chainId)); - } - - /// @dev Tests that adding a dependency reverts if it's the chain's chain id - function test_setConfig_addDependencyButChainChainId_reverts() public prankDepositor { - vm.expectRevert(AlreadyDependency.selector); - _l1BlockInterop().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(block.chainid)); - } - - /// @dev Tests that adding a dependency already in the set reverts - function test_setConfig_addDependencyButAlreadyDependency_reverts(uint256 _chainId) public prankDepositor { - vm.assume(_chainId != block.chainid); - - _l1BlockInterop().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(_chainId)); - - vm.expectRevert(AlreadyDependency.selector); - _l1BlockInterop().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(_chainId)); - } - - /// @dev Tests that setting the add dependency config as not the depositor reverts. - function testFuzz_setConfig_addDependencyButNotDepositor_reverts(uint256 _chainId) public { - vm.expectRevert(NotDepositor.selector); - _l1BlockInterop().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(_chainId)); - } - - /// @dev Tests that setting the add dependency config when the dependency set size is too large reverts. - function test_setConfig_addDependencyButDependencySetSizeTooLarge_reverts() public prankDepositor { - for (uint256 i = 0; i < type(uint8).max; i++) { - _l1BlockInterop().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(i)); - } - - assertEq(_l1BlockInterop().dependencySetSize(), type(uint8).max); - - vm.expectRevert(DependencySetSizeTooLarge.selector); - _l1BlockInterop().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(1)); - } - - /// @dev Tests that the config for removing a dependency can be set. - function testFuzz_setConfig_removeDependency_succeeds(uint256 _chainId) public prankDepositor { - vm.assume(_chainId != block.chainid); - - // Add the chain ID to the dependency set before removing it - _l1BlockInterop().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(_chainId)); - - vm.expectEmit(address(l1Block)); - emit DependencyRemoved(_chainId); - - _l1BlockInterop().setConfig(ConfigType.REMOVE_DEPENDENCY, StaticConfig.encodeRemoveDependency(_chainId)); - } - - /// @dev Tests that setting the remove dependency config as not the depositor reverts. - function testFuzz_setConfig_removeDependencyButNotDepositor_reverts(uint256 _chainId) public { - vm.expectRevert(NotDepositor.selector); - _l1BlockInterop().setConfig(ConfigType.REMOVE_DEPENDENCY, StaticConfig.encodeRemoveDependency(_chainId)); - } - - /// @dev Tests that setting the remove dependency config for the chain's chain ID reverts. - function test_setConfig_removeDependencyButChainChainId_reverts() public prankDepositor { - vm.expectRevert(CantRemovedDependency.selector); - _l1BlockInterop().setConfig(ConfigType.REMOVE_DEPENDENCY, StaticConfig.encodeRemoveDependency(block.chainid)); - } - - /// @dev Tests that setting the remove dependency config for a chain ID that is not in the dependency set reverts. - function testFuzz_setConfig_removeDependencyButNotDependency_reverts(uint256 _chainId) public prankDepositor { - vm.assume(_chainId != block.chainid); - - vm.expectRevert(NotDependency.selector); - _l1BlockInterop().setConfig(ConfigType.REMOVE_DEPENDENCY, StaticConfig.encodeRemoveDependency(_chainId)); - } - - /// @dev Returns the L1BlockInterop instance. - function _l1BlockInterop() internal view returns (IL1BlockInterop) { - return IL1BlockInterop(address(l1Block)); - } -} - -contract L1BlockInteropIsDeposit_Test is L1BlockInteropTest { - /// @dev Tests that `isDeposit` reverts if the caller is not the cross L2 inbox. - function test_isDeposit_notCrossL2Inbox_reverts(address _caller) external { - vm.assume(_caller != Predeploys.CROSS_L2_INBOX); - vm.expectRevert(NotCrossL2Inbox.selector); - _l1BlockInterop().isDeposit(); - } - - /// @dev Tests that `isDeposit` always returns the correct value. - function test_isDeposit_succeeds() external { - // Assert is false if the value is not updated - vm.prank(Predeploys.CROSS_L2_INBOX); - assertEq(_l1BlockInterop().isDeposit(), false); - - /// @dev Assuming that `setL1BlockValuesInterop` will set the proper value. That function is tested as well - vm.prank(_l1BlockInterop().DEPOSITOR_ACCOUNT()); - _l1BlockInterop().setL1BlockValuesInterop(); - - // Assert is true if the value is updated - vm.prank(Predeploys.CROSS_L2_INBOX); - assertEq(_l1BlockInterop().isDeposit(), true); - } -} - -contract L1BlockInteropSetL1BlockValuesInterop_Test is L1BlockInteropTest { - /// @dev Tests that `setL1BlockValuesInterop` reverts if sender address is not the depositor - function test_setL1BlockValuesInterop_notDepositor_reverts(address _caller) external { - vm.assume(_caller != _l1BlockInterop().DEPOSITOR_ACCOUNT()); - vm.prank(_caller); - vm.expectRevert(NotDepositor.selector); - _l1BlockInterop().setL1BlockValuesInterop(); - } - - /// @dev Tests that `setL1BlockValuesInterop` succeeds if sender address is the depositor - function test_setL1BlockValuesInterop_succeeds( - uint32 baseFeeScalar, - uint32 blobBaseFeeScalar, - uint64 sequenceNumber, - uint64 timestamp, - uint64 number, - uint256 baseFee, - uint256 blobBaseFee, - bytes32 hash, - bytes32 batcherHash - ) - external - { - // Ensure the `isDepositTransaction` flag is false before calling `setL1BlockValuesInterop` - vm.prank(Predeploys.CROSS_L2_INBOX); - assertEq(_l1BlockInterop().isDeposit(), false); - - bytes memory setValuesEcotoneCalldata = abi.encodePacked( - baseFeeScalar, blobBaseFeeScalar, sequenceNumber, timestamp, number, baseFee, blobBaseFee, hash, batcherHash - ); - - vm.prank(_l1BlockInterop().DEPOSITOR_ACCOUNT()); - (bool success,) = address(l1Block).call( - abi.encodePacked(IL1BlockInterop.setL1BlockValuesInterop.selector, setValuesEcotoneCalldata) - ); - assertTrue(success, "function call failed"); - - // Assert that the `isDepositTransaction` flag was properly set to true - vm.prank(Predeploys.CROSS_L2_INBOX); - assertEq(_l1BlockInterop().isDeposit(), true); - - // Assert `setL1BlockValuesEcotone` was properly called, forwarding the calldata to it - assertEq(_l1BlockInterop().baseFeeScalar(), baseFeeScalar, "base fee scalar not properly set"); - assertEq(_l1BlockInterop().blobBaseFeeScalar(), blobBaseFeeScalar, "blob base fee scalar not properly set"); - assertEq(_l1BlockInterop().sequenceNumber(), sequenceNumber, "sequence number not properly set"); - assertEq(_l1BlockInterop().timestamp(), timestamp, "timestamp not properly set"); - assertEq(_l1BlockInterop().number(), number, "number not properly set"); - assertEq(_l1BlockInterop().basefee(), baseFee, "base fee not properly set"); - assertEq(_l1BlockInterop().blobBaseFee(), blobBaseFee, "blob base fee not properly set"); - assertEq(_l1BlockInterop().hash(), hash, "hash not properly set"); - assertEq(_l1BlockInterop().batcherHash(), batcherHash, "batcher hash not properly set"); - } -} - -contract L1BlockDepositsComplete_Test is L1BlockInteropTest { - // @dev Tests that `depositsComplete` reverts if the caller is not the depositor. - function test_depositsComplete_notDepositor_reverts(address _caller) external { - vm.assume(_caller != _l1BlockInterop().DEPOSITOR_ACCOUNT()); - vm.expectRevert(NotDepositor.selector); - _l1BlockInterop().depositsComplete(); - } - - // @dev Tests that `depositsComplete` succeeds if the caller is the depositor. - function test_depositsComplete_succeeds() external { - // Set the `isDeposit` flag to true - vm.prank(_l1BlockInterop().DEPOSITOR_ACCOUNT()); - _l1BlockInterop().setL1BlockValuesInterop(); - - // Assert that the `isDeposit` flag was properly set to true - vm.prank(Predeploys.CROSS_L2_INBOX); - assertTrue(_l1BlockInterop().isDeposit()); - - // Call `depositsComplete` - vm.prank(_l1BlockInterop().DEPOSITOR_ACCOUNT()); - _l1BlockInterop().depositsComplete(); - - // Assert that the `isDeposit` flag was properly set to false - /// @dev Assuming that `isDeposit()` wil return the proper value. That function is tested as well - vm.prank(Predeploys.CROSS_L2_INBOX); - assertEq(_l1BlockInterop().isDeposit(), false); - } -} diff --git a/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol b/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol index 5cfcc3a8da..10fdfcfdd9 100644 --- a/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol +++ b/packages/contracts-bedrock/test/L2/L2ToL2CrossDomainMessenger.t.sol @@ -17,13 +17,9 @@ import { IdOriginNotL2ToL2CrossDomainMessenger, EventPayloadNotSentMessage, MessageDestinationNotRelayChain, - MessageTargetCrossL2Inbox, MessageTargetL2ToL2CrossDomainMessenger, MessageAlreadyRelayed, - ReentrantCall, - TargetCallFailed, - IDependencySet, - InvalidChainId + ReentrantCall } from "src/L2/L2ToL2CrossDomainMessenger.sol"; // Interfaces @@ -91,13 +87,6 @@ contract L2ToL2CrossDomainMessengerTest is Test { // Ensure that the target contract is not CrossL2Inbox or L2ToL2CrossDomainMessenger vm.assume(_target != Predeploys.CROSS_L2_INBOX && _target != Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); - // Mock the call over the `isInDependencySet` function to return true - vm.mockCall( - Predeploys.L1_BLOCK_ATTRIBUTES, - abi.encodeCall(IDependencySet.isInDependencySet, (_destination)), - abi.encode(true) - ); - // Get the current message nonce uint256 messageNonce = l2ToL2CrossDomainMessenger.messageNonce(); @@ -169,22 +158,6 @@ contract L2ToL2CrossDomainMessengerTest is Test { l2ToL2CrossDomainMessenger.sendMessage({ _destination: block.chainid, _target: _target, _message: _message }); } - /// @dev Tests that the `sendMessage` function reverts when the target is CrossL2Inbox. - function testFuzz_sendMessage_targetCrossL2Inbox_reverts(uint256 _destination, bytes calldata _message) external { - // Ensure the destination is not the same as the source, otherwise the function will revert regardless of target - vm.assume(_destination != block.chainid); - - // Expect a revert with the MessageTargetCrossL2Inbox selector - vm.expectRevert(MessageTargetCrossL2Inbox.selector); - - // Call `senderMessage` with the CrossL2Inbox as the target to provoke revert - l2ToL2CrossDomainMessenger.sendMessage({ - _destination: _destination, - _target: Predeploys.CROSS_L2_INBOX, - _message: _message - }); - } - /// @dev Tests that the `sendMessage` function reverts when the target is L2ToL2CrossDomainMessenger. function testFuzz_sendMessage_targetL2ToL2CrossDomainMessenger_reverts( uint256 _destination, @@ -206,34 +179,6 @@ contract L2ToL2CrossDomainMessengerTest is Test { }); } - /// @notice Tests the `sendMessage` function reverts when the `destination` is not in the dependency set. - function testFuzz_sendMessage_notInDependencySet_reverts( - uint256 _destination, - address _target, - bytes calldata _message - ) - external - { - // Ensure the destination is not the same as the source, otherwise the function will revert - vm.assume(_destination != block.chainid); - - // Ensure that the target contract is not CrossL2Inbox or L2ToL2CrossDomainMessenger - vm.assume(_target != Predeploys.CROSS_L2_INBOX && _target != Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); - - // Mock the call over the `isInDependencySet` function to return false - vm.mockCall( - Predeploys.L1_BLOCK_ATTRIBUTES, - abi.encodeCall(IDependencySet.isInDependencySet, (_destination)), - abi.encode(false) - ); - - // Expect a revert with the InvalidChainId selector - vm.expectRevert(InvalidChainId.selector); - - // Call `sendMessage` with a destination that is not in the dependency set to provoke revert - l2ToL2CrossDomainMessenger.sendMessage(_destination, _target, _message); - } - /// @dev Tests that the `relayMessage` function succeeds and emits the correct RelayedMessage event. function testFuzz_relayMessage_succeeds( uint256 _source, @@ -242,9 +187,9 @@ contract L2ToL2CrossDomainMessengerTest is Test { address _target, bytes calldata _message, uint256 _value, - uint256 _blockNum, - uint256 _logIndex, - uint256 _time + uint64 _blockNum, + uint32 _logIndex, + uint64 _time ) external { @@ -300,9 +245,9 @@ contract L2ToL2CrossDomainMessengerTest is Test { uint256 _nonce, bytes32 _msgHash, uint256 _value, - uint256 _blockNum, - uint256 _logIndex, - uint256 _time + uint64 _blockNum, + uint32 _logIndex, + uint64 _time ) external { @@ -347,9 +292,9 @@ contract L2ToL2CrossDomainMessengerTest is Test { uint256 _nonce, address _sender, uint256 _value, - uint256 _blockNum, - uint256 _logIndex, - uint256 _time + uint64 _blockNum, + uint32 _logIndex, + uint64 _time ) external { @@ -407,9 +352,9 @@ contract L2ToL2CrossDomainMessengerTest is Test { uint256 _nonce, address _sender, uint256 _value, - uint256 _blockNum, - uint256 _logIndex, - uint256 _time, + uint64 _blockNum, + uint32 _logIndex, + uint64 _time, address _target, bytes memory _mockedReturnData ) @@ -485,9 +430,9 @@ contract L2ToL2CrossDomainMessengerTest is Test { address _sender2, // sender passed to `relayMessage` by the reentrant call. uint256 _nonce, uint256 _value, - uint256 _blockNum, - uint256 _logIndex, - uint256 _time + uint64 _blockNum, + uint32 _logIndex, + uint64 _time ) external { @@ -517,8 +462,8 @@ contract L2ToL2CrossDomainMessengerTest is Test { returnData: "" }); - // Expect a revert with the TargetCallFailed selector - vm.expectRevert(TargetCallFailed.selector); + // Expect the target call to revert + vm.expectRevert(1); hoax(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, _value); l2ToL2CrossDomainMessenger.relayMessage{ value: _value }(id, sentMessage); @@ -541,9 +486,9 @@ contract L2ToL2CrossDomainMessengerTest is Test { bytes calldata _message, uint256 _value, address _origin, - uint256 _blockNum, - uint256 _logIndex, - uint256 _time + uint64 _blockNum, + uint32 _logIndex, + uint64 _time ) external { @@ -573,9 +518,9 @@ contract L2ToL2CrossDomainMessengerTest is Test { address _target, bytes calldata _message, uint256 _value, - uint256 _blockNum, - uint256 _logIndex, - uint256 _time + uint64 _blockNum, + uint32 _logIndex, + uint64 _time ) external { @@ -604,86 +549,6 @@ contract L2ToL2CrossDomainMessengerTest is Test { l2ToL2CrossDomainMessenger.relayMessage{ value: _value }(id, sentMessage); } - /// @dev Tests that the `relayMessage` function reverts when the message target is CrossL2Inbox. - function testFuzz_relayMessage_targetCrossL2Inbox_reverts( - uint256 _source, - uint256 _nonce, - address _sender, - bytes calldata _message, - uint256 _value, - uint256 _blockNum, - uint256 _logIndex, - uint256 _time - ) - external - { - // Expect a revert with the MessageTargetCrossL2Inbox selector - vm.expectRevert(MessageTargetCrossL2Inbox.selector); - - // Call `relayMessage` with CrossL2Inbox as the target to provoke revert. The current chain is the destination - // to prevent revert due to invalid destination - Identifier memory id = - Identifier(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, _blockNum, _logIndex, _time, _source); - bytes memory sentMessage = abi.encodePacked( - abi.encode( - L2ToL2CrossDomainMessenger.SentMessage.selector, block.chainid, Predeploys.CROSS_L2_INBOX, _nonce - ), // topics - abi.encode(_sender, _message) // data - ); - - // Ensure the CrossL2Inbox validates this message - vm.mockCall({ - callee: Predeploys.CROSS_L2_INBOX, - data: abi.encodeCall(ICrossL2Inbox.validateMessage, (id, keccak256(sentMessage))), - returnData: "" - }); - - // Call - hoax(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, _value); - l2ToL2CrossDomainMessenger.relayMessage{ value: _value }(id, sentMessage); - } - - /// @dev Tests that the `relayMessage` function reverts when the message target is L2ToL2CrossDomainMessenger. - function testFuzz_relayMessage_targetL2ToL2CrossDomainMessenger_reverts( - uint256 _source, - uint256 _nonce, - address _sender, - bytes calldata _message, - uint256 _value, - uint256 _blockNum, - uint256 _logIndex, - uint256 _time - ) - external - { - // Expect a revert with the MessageTargetL2ToL2CrossDomainMessenger selector - vm.expectRevert(MessageTargetL2ToL2CrossDomainMessenger.selector); - - // Call `relayMessage` with L2ToL2CrossDomainMessenger as the target to provoke revert. The current chain is the - // destination to prevent revert due to invalid destination - Identifier memory id = - Identifier(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, _blockNum, _logIndex, _time, _source); - bytes memory sentMessage = abi.encodePacked( - abi.encode( - L2ToL2CrossDomainMessenger.SentMessage.selector, - block.chainid, - Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, - _nonce - ), // topics - abi.encode(_sender, _message) // data - ); - - // Ensure the CrossL2Inbox validates this message - vm.mockCall({ - callee: Predeploys.CROSS_L2_INBOX, - data: abi.encodeCall(ICrossL2Inbox.validateMessage, (id, keccak256(sentMessage))), - returnData: "" - }); - - hoax(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, _value); - l2ToL2CrossDomainMessenger.relayMessage{ value: _value }(id, sentMessage); - } - /// @dev Tests that the `relayMessage` function reverts when the message has already been relayed. function testFuzz_relayMessage_alreadyRelayed_reverts( uint256 _source, @@ -692,9 +557,9 @@ contract L2ToL2CrossDomainMessengerTest is Test { address _target, bytes calldata _message, uint256 _value, - uint256 _blockNum, - uint256 _logIndex, - uint256 _time + uint64 _blockNum, + uint32 _logIndex, + uint64 _time ) external { @@ -754,9 +619,7 @@ contract L2ToL2CrossDomainMessengerTest is Test { address _target, bytes calldata _message, uint256 _value, - uint256 _blockNum, - uint256 _logIndex, - uint256 _time + bytes calldata _revertData ) external { @@ -767,10 +630,12 @@ contract L2ToL2CrossDomainMessengerTest is Test { if (_value > 0) assumePayable(_target); // Ensure that the target contract reverts - vm.mockCallRevert({ callee: _target, msgValue: _value, data: _message, revertData: abi.encode(false) }); + vm.mockCallRevert({ callee: _target, msgValue: _value, data: _message, revertData: _revertData }); + + // Construct the identifier -- using some hardcoded values for the block number, log index, and time to avoid + // stack too deep errors. + Identifier memory id = Identifier(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, 1, 1, 1, _source); - Identifier memory id = - Identifier(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, _blockNum, _logIndex, _time, _source); bytes memory sentMessage = abi.encodePacked( abi.encode(L2ToL2CrossDomainMessenger.SentMessage.selector, block.chainid, _target, _nonce), // topics abi.encode(_sender, _message) // data @@ -783,8 +648,8 @@ contract L2ToL2CrossDomainMessengerTest is Test { returnData: "" }); - // Expect a revert with the TargetCallFailed selector - vm.expectRevert(TargetCallFailed.selector); + // Expect the target call to revert with the proper return data. + vm.expectRevert(_revertData); hoax(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, _value); l2ToL2CrossDomainMessenger.relayMessage{ value: _value }(id, sentMessage); } diff --git a/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol b/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol index e122e79794..610b052430 100644 --- a/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol +++ b/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol @@ -19,25 +19,22 @@ import { IL2ToL2CrossDomainMessenger } from "interfaces/L2/IL2ToL2CrossDomainMes /// @title SuperchainWETH_Test /// @notice Contract for testing the SuperchainWETH contract. contract SuperchainWETH_Test is CommonTest { - /// @notice Emitted when a transfer is made. event Transfer(address indexed src, address indexed dst, uint256 wad); - /// @notice Emitted when a deposit is made. event Deposit(address indexed dst, uint256 wad); - /// @notice Emitted when a withdrawal is made. event Withdrawal(address indexed src, uint256 wad); - /// @notice Emitted when a crosschain transfer mints tokens. event CrosschainMint(address indexed to, uint256 amount, address indexed sender); - /// @notice Emitted when a crosschain transfer burns tokens. event CrosschainBurn(address indexed from, uint256 amount, address indexed sender); event SendETH(address indexed from, address indexed to, uint256 amount, uint256 destination); event RelayETH(address indexed from, address indexed to, uint256 amount, uint256 source); + event Approval(address indexed src, address indexed guy, uint256 wad); + address internal constant ZERO_ADDRESS = address(0); /// @notice Test setup. @@ -296,6 +293,36 @@ contract SuperchainWETH_Test is CommonTest { assertEq(_allowance, _wad); } + /// @notice Tests that the `approve` function reverts when the spender is Permit2 and the allowance is not infinite. + function testFuzz_approve_permit2NonInfiniteAllowance_reverts(uint256 _wad) public { + vm.assume(_wad != type(uint256).max); + vm.expectRevert(ISuperchainWETH.Permit2AllowanceIsFixedAtInfinity.selector); + superchainWeth.approve(Preinstalls.Permit2, _wad); + } + + /// @notice Tests that the `approve` function succeeds when the spender is Permit2 and the allowance is infinite. + function testFuzz_approve_permit2InfiniteAllowance_succeeds(address _src) public { + vm.expectEmit(address(superchainWeth)); + emit Approval(_src, Preinstalls.Permit2, type(uint256).max); + + vm.prank(_src); + superchainWeth.approve(Preinstalls.Permit2, type(uint256).max); + assertEq(superchainWeth.allowance(_src, Preinstalls.Permit2), type(uint256).max); + } + + /// @notice Tests that the `approve` function succeeds correctly updating the allowance. + function testFuzz_approve_succeeds(address _src, address _guy, uint256 _wad) public { + if (_guy == Preinstalls.Permit2) _wad = type(uint256).max; + + vm.expectEmit(address(superchainWeth)); + emit Approval(_src, _guy, _wad); + + vm.prank(_src); + superchainWeth.approve(_guy, _wad); + + assertEq(superchainWeth.allowance(_src, _guy), _wad); + } + /// @notice Tests that `transferFrom` works when the caller (spender) is Permit2, without any explicit approval. /// @param _src The funds owner. /// @param _dst The address of the recipient. diff --git a/packages/contracts-bedrock/test/dispute/AnchorStateRegistry.t.sol b/packages/contracts-bedrock/test/dispute/AnchorStateRegistry.t.sol index b3c7bf0640..58cb5b7afe 100644 --- a/packages/contracts-bedrock/test/dispute/AnchorStateRegistry.t.sol +++ b/packages/contracts-bedrock/test/dispute/AnchorStateRegistry.t.sol @@ -5,9 +5,10 @@ pragma solidity ^0.8.15; import { FaultDisputeGame_Init, _changeClaimStatus } from "test/dispute/FaultDisputeGame.t.sol"; // Libraries -import { GameType, GameStatus, Hash, Claim, VMStatuses, OutputRoot } from "src/dispute/lib/Types.sol"; +import { GameType, GameStatus, Hash, Claim, VMStatuses, Proposal } from "src/dispute/lib/Types.sol"; // Interfaces +import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; @@ -15,8 +16,9 @@ contract AnchorStateRegistry_Init is FaultDisputeGame_Init { /// @dev A valid l2BlockNumber that comes after the current anchor root block. uint256 validL2BlockNumber; - event AnchorNotUpdated(IFaultDisputeGame indexed game); event AnchorUpdated(IFaultDisputeGame indexed game); + event RespectedGameTypeSet(GameType gameType); + event RetirementTimestampSet(uint256 timestamp); function setUp() public virtual override { // Duplicating the initialization/setup logic of FaultDisputeGame_Test. @@ -46,7 +48,6 @@ contract AnchorStateRegistry_Initialize_Test is AnchorStateRegistry_Init { // Verify contract addresses. assert(anchorStateRegistry.superchainConfig() == superchainConfig); assert(anchorStateRegistry.disputeGameFactory() == disputeGameFactory); - assert(anchorStateRegistry.portal() == optimismPortal2); } } @@ -57,11 +58,11 @@ contract AnchorStateRegistry_Initialize_TestFail is AnchorStateRegistry_Init { anchorStateRegistry.initialize( superchainConfig, disputeGameFactory, - optimismPortal2, - OutputRoot({ + Proposal({ root: Hash.wrap(0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF), - l2BlockNumber: 0 - }) + l2SequenceNumber: 0 + }), + GameType.wrap(0) ); } } @@ -73,6 +74,25 @@ contract AnchorStateRegistry_Version_Test is AnchorStateRegistry_Init { } } +contract AnchorStateRegistry_Paused_Test is AnchorStateRegistry_Init { + /// @notice Tests that paused() will return the correct value. + function test_paused_succeeds() public { + // Pause the superchain. + vm.prank(superchainConfig.guardian()); + superchainConfig.pause("testing"); + + // Paused should return true. + assertTrue(anchorStateRegistry.paused()); + + // Unpause the superchain. + vm.prank(superchainConfig.guardian()); + superchainConfig.unpause(); + + // Paused should return false. + assertFalse(anchorStateRegistry.paused()); + } +} + contract AnchorStateRegistry_GetAnchorRoot_Test is AnchorStateRegistry_Init { /// @notice Tests that getAnchorRoot will return the value of the starting anchor root when no /// anchor game exists yet. @@ -103,7 +123,30 @@ contract AnchorStateRegistry_GetAnchorRoot_Test is AnchorStateRegistry_Init { // We should get the anchor root back. (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); assertEq(root.raw(), gameProxy.rootClaim().raw()); - assertEq(l2BlockNumber, gameProxy.l2BlockNumber()); + assertEq(l2BlockNumber, gameProxy.l2SequenceNumber()); + } + + /// @notice Tests that getAnchorRoot will return the latest anchor root even if the superchain + /// is paused. + function test_getAnchorRoot_superchainPaused_succeeds() public { + // Mock the game to be resolved. + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(block.timestamp)); + vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds() + 1); + + // Mock the game to be the defender wins. + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS)); + + // Set the anchor game to the game proxy. + anchorStateRegistry.setAnchorState(gameProxy); + + // Pause the superchain. + vm.prank(superchainConfig.guardian()); + superchainConfig.pause("testing"); + + // We should get the anchor root back. + (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); + assertEq(root.raw(), gameProxy.rootClaim().raw()); + assertEq(l2BlockNumber, gameProxy.l2SequenceNumber()); } /// @notice Tests that getAnchorRoot returns even if the anchor game is blacklisted. @@ -118,17 +161,14 @@ contract AnchorStateRegistry_GetAnchorRoot_Test is AnchorStateRegistry_Init { // Set the anchor game to the game proxy. anchorStateRegistry.setAnchorState(gameProxy); - // Mock the disputeGameBlacklist call to return true. - vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.disputeGameBlacklist, (gameProxy)), - abi.encode(true) - ); + // Blacklist the game. + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.blacklistDisputeGame(gameProxy); // Get the anchor root. (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); assertEq(root.raw(), gameProxy.rootClaim().raw()); - assertEq(l2BlockNumber, gameProxy.l2BlockNumber()); + assertEq(l2BlockNumber, gameProxy.l2SequenceNumber()); } } @@ -172,12 +212,11 @@ contract AnchorStateRegistry_IsGameRegistered_Test is AnchorStateRegistry_Init { contract AnchorStateRegistry_IsGameBlacklisted_Test is AnchorStateRegistry_Init { /// @notice Tests that isGameBlacklisted will return true if the game is blacklisted. function test_isGameBlacklisted_isActuallyBlacklisted_succeeds() public { - // Mock the disputeGameBlacklist call to return true. - vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.disputeGameBlacklist, (gameProxy)), - abi.encode(true) - ); + // Blacklist the game. + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.blacklistDisputeGame(gameProxy); + + // Should return true. assertTrue(anchorStateRegistry.isGameBlacklisted(gameProxy)); } @@ -185,8 +224,8 @@ contract AnchorStateRegistry_IsGameBlacklisted_Test is AnchorStateRegistry_Init function test_isGameBlacklisted_isNotBlacklisted_succeeds() public { // Mock the disputeGameBlacklist call to return false. vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.disputeGameBlacklist, (gameProxy)), + address(anchorStateRegistry), + abi.encodeCall(anchorStateRegistry.disputeGameBlacklist, (gameProxy)), abi.encode(false) ); assertFalse(anchorStateRegistry.isGameBlacklisted(gameProxy)); @@ -214,34 +253,35 @@ contract AnchorStateRegistry_IsGameRespected_Test is AnchorStateRegistry_Init { contract AnchorStateRegistry_IsGameRetired_Test is AnchorStateRegistry_Init { /// @notice Tests that isGameRetired will return true if the game is retired. - /// @param _retirementTimestamp The retirement timestamp to use for the test. - function testFuzz_isGameRetired_isRetired_succeeds(uint64 _retirementTimestamp) public { - // Make sure retirement timestamp is greater than or equal to the game's creation time. - _retirementTimestamp = uint64(bound(_retirementTimestamp, gameProxy.createdAt().raw(), type(uint64).max)); + /// @param _createdAtTimestamp The createdAt timestamp to use for the test. + function testFuzz_isGameRetired_isRetired_succeeds(uint64 _createdAtTimestamp) public { + // Set the retirement timestamp to now. + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.updateRetirementTimestamp(); + + // Make sure createdAt timestamp is less than or equal to the retirementTimestamp. + _createdAtTimestamp = uint64(bound(_createdAtTimestamp, 0, anchorStateRegistry.retirementTimestamp())); // Mock the respectedGameTypeUpdatedAt call. - vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.respectedGameTypeUpdatedAt, ()), - abi.encode(_retirementTimestamp) - ); + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.createdAt, ()), abi.encode(_createdAtTimestamp)); // Game should be retired. assertTrue(anchorStateRegistry.isGameRetired(gameProxy)); } /// @notice Tests that isGameRetired will return false if the game is not retired. - /// @param _retirementTimestamp The retirement timestamp to use for the test. - function testFuzz_isGameRetired_isNotRetired_succeeds(uint64 _retirementTimestamp) public { - // Make sure retirement timestamp is earlier than the game's creation time. - _retirementTimestamp = uint64(bound(_retirementTimestamp, 0, gameProxy.createdAt().raw() - 1)); + /// @param _createdAtTimestamp The createdAt timestamp to use for the test. + function testFuzz_isGameRetired_isNotRetired_succeeds(uint64 _createdAtTimestamp) public { + // Set the retirement timestamp to now. + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.updateRetirementTimestamp(); - // Mock the respectedGameTypeUpdatedAt call to be earlier than the game's creation time. - vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.respectedGameTypeUpdatedAt, ()), - abi.encode(_retirementTimestamp) - ); + // Make sure createdAt timestamp is greater than the retirementTimestamp. + _createdAtTimestamp = + uint64(bound(_createdAtTimestamp, anchorStateRegistry.retirementTimestamp() + 1, type(uint64).max)); + + // Mock the call to createdAt. + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.createdAt, ()), abi.encode(_createdAtTimestamp)); // Game should not be retired. assertFalse(anchorStateRegistry.isGameRetired(gameProxy)); @@ -287,29 +327,38 @@ contract AnchorStateRegistry_IsGameProper_Test is AnchorStateRegistry_Init { /// @notice Tests that isGameProper will return false if the game is blacklisted. function test_isGameProper_isBlacklisted_succeeds() public { - // Mock the disputeGameBlacklist call to return true. - vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.disputeGameBlacklist, (gameProxy)), - abi.encode(true) - ); + // Blacklist the game. + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.blacklistDisputeGame(gameProxy); + + // Should return false. + assertFalse(anchorStateRegistry.isGameProper(gameProxy)); + } + /// @notice Tests that isGameProper will return false if the superchain is paused. + function test_isGameProper_superchainPaused_succeeds() public { + // Pause the superchain. + vm.prank(superchainConfig.guardian()); + superchainConfig.pause("testing"); + + // Game should not be proper. assertFalse(anchorStateRegistry.isGameProper(gameProxy)); } /// @notice Tests that isGameProper will return false if the game is retired. - /// @param _retirementTimestamp The retirement timestamp to use for the test. - function testFuzz_isGameProper_isRetired_succeeds(uint64 _retirementTimestamp) public { - // Make sure retirement timestamp is later than the game's creation time. - _retirementTimestamp = uint64(bound(_retirementTimestamp, gameProxy.createdAt().raw() + 1, type(uint64).max)); + /// @param _createdAtTimestamp The createdAt timestamp to use for the test. + function testFuzz_isGameProper_isRetired_succeeds(uint64 _createdAtTimestamp) public { + // Set the retirement timestamp to now. + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.updateRetirementTimestamp(); - // Mock the respectedGameTypeUpdatedAt call to be later than the game's creation time. - vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.respectedGameTypeUpdatedAt, ()), - abi.encode(_retirementTimestamp) - ); + // Make sure createdAt timestamp is less than or equal to the retirementTimestamp. + _createdAtTimestamp = uint64(bound(_createdAtTimestamp, 0, anchorStateRegistry.retirementTimestamp())); + + // Mock the call to createdAt. + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.createdAt, ()), abi.encode(_createdAtTimestamp)); + // Game should not be proper. assertFalse(anchorStateRegistry.isGameProper(gameProxy)); } } @@ -476,8 +525,8 @@ contract AnchorStateRegistry_IsGameClaimValid_Test is AnchorStateRegistry_Init { function testFuzz_isGameClaimValid_isBlacklisted_succeeds() public { // Mock the disputeGameBlacklist call to return true. vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.disputeGameBlacklist, (gameProxy)), + address(anchorStateRegistry), + abi.encodeCall(anchorStateRegistry.disputeGameBlacklist, (gameProxy)), abi.encode(true) ); @@ -486,17 +535,17 @@ contract AnchorStateRegistry_IsGameClaimValid_Test is AnchorStateRegistry_Init { } /// @notice Tests that isGameClaimValid will return false if the game is retired. - /// @param _resolvedAtTimestamp The resolvedAt timestamp to use for the test. - function testFuzz_isGameClaimValid_isRetired_succeeds(uint256 _resolvedAtTimestamp) public { - // Make sure retirement timestamp is later than the game's creation time. - _resolvedAtTimestamp = uint64(bound(_resolvedAtTimestamp, gameProxy.createdAt().raw() + 1, type(uint64).max)); + /// @param _createdAtTimestamp The createdAt timestamp to use for the test. + function testFuzz_isGameClaimValid_isRetired_succeeds(uint256 _createdAtTimestamp) public { + // Set the retirement timestamp to now. + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.updateRetirementTimestamp(); - // Mock the respectedGameTypeUpdatedAt call to be later than the game's creation time. - vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.respectedGameTypeUpdatedAt, ()), - abi.encode(_resolvedAtTimestamp) - ); + // Make sure createdAt timestamp is less than or equal to the retirementTimestamp. + _createdAtTimestamp = uint64(bound(_createdAtTimestamp, 0, anchorStateRegistry.retirementTimestamp())); + + // Mock the call to createdAt. + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.createdAt, ()), abi.encode(_createdAtTimestamp)); // Claim should not be valid. assertFalse(anchorStateRegistry.isGameClaimValid(gameProxy)); @@ -528,6 +577,16 @@ contract AnchorStateRegistry_IsGameClaimValid_Test is AnchorStateRegistry_Init { // Claim should not be valid. assertFalse(anchorStateRegistry.isGameClaimValid(gameProxy)); } + + /// @notice Tests that isGameClaimValid will return false if the superchain is paused. + function test_isGameClaimValid_superchainPaused_succeeds() public { + // Pause the superchain. + vm.prank(superchainConfig.guardian()); + superchainConfig.pause("testing"); + + // Game should not be valid. + assertFalse(anchorStateRegistry.isGameClaimValid(gameProxy)); + } } contract AnchorStateRegistry_SetAnchorState_Test is AnchorStateRegistry_Init { @@ -543,7 +602,7 @@ contract AnchorStateRegistry_SetAnchorState_Test is AnchorStateRegistry_Init { _l2BlockNumber = bound(_l2BlockNumber, validL2BlockNumber, type(uint256).max); // Mock the l2BlockNumber call. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2BlockNumber, ()), abi.encode(_l2BlockNumber)); + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2SequenceNumber, ()), abi.encode(_l2BlockNumber)); // Mock the DEFENDER_WINS state. vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS)); @@ -563,7 +622,7 @@ contract AnchorStateRegistry_SetAnchorState_Test is AnchorStateRegistry_Init { // Confirm that the anchor state is now the same as the game state. (root, l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); - assertEq(l2BlockNumber, gameProxy.l2BlockNumber()); + assertEq(l2BlockNumber, gameProxy.l2SequenceNumber()); assertEq(root.raw(), gameProxy.rootClaim().raw()); // Confirm that the anchor game is now set. @@ -584,7 +643,7 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init _l2BlockNumber = bound(_l2BlockNumber, 0, l2BlockNumber); // Mock the l2BlockNumber call. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2BlockNumber, ()), abi.encode(_l2BlockNumber)); + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2SequenceNumber, ()), abi.encode(_l2BlockNumber)); // Mock the DEFENDER_WINS state. vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS)); @@ -617,7 +676,7 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init _l2BlockNumber = bound(_l2BlockNumber, l2BlockNumber, type(uint256).max); // Mock the l2BlockNumber call. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2BlockNumber, ()), abi.encode(_l2BlockNumber)); + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2SequenceNumber, ()), abi.encode(_l2BlockNumber)); // Mock the DEFENDER_WINS state. vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS)); @@ -656,7 +715,7 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init _l2BlockNumber = bound(_l2BlockNumber, l2BlockNumber, type(uint256).max); // Mock the l2BlockNumber call. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2BlockNumber, ()), abi.encode(_l2BlockNumber)); + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2SequenceNumber, ()), abi.encode(_l2BlockNumber)); // Mock the CHALLENGER_WINS state. vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)); @@ -690,7 +749,7 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init _l2BlockNumber = bound(_l2BlockNumber, l2BlockNumber, type(uint256).max); // Mock the l2BlockNumber call. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2BlockNumber, ()), abi.encode(_l2BlockNumber)); + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2SequenceNumber, ()), abi.encode(_l2BlockNumber)); // Mock the CHALLENGER_WINS state. vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.IN_PROGRESS)); @@ -723,7 +782,7 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init _l2BlockNumber = bound(_l2BlockNumber, l2BlockNumber, type(uint256).max); // Mock the l2BlockNumber call. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2BlockNumber, ()), abi.encode(_l2BlockNumber)); + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2SequenceNumber, ()), abi.encode(_l2BlockNumber)); // Mock the DEFENDER_WINS state. vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS)); @@ -768,12 +827,9 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(block.timestamp)); vm.warp(block.timestamp + optimismPortal2.disputeGameFinalityDelaySeconds() + 1); - // Mock the disputeGameBlacklist call to return true. - vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.disputeGameBlacklist, (gameProxy)), - abi.encode(true) - ); + // Blacklist the game. + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.blacklistDisputeGame(gameProxy); // Update the anchor state. vm.prank(address(gameProxy)); @@ -786,8 +842,7 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init assertEq(updatedRoot.raw(), root.raw()); } - /// @notice Tests that setAnchorState will revert if the game is valid and the game is - /// retired. + /// @notice Tests that setAnchorState will revert if the game is retired. /// @param _l2BlockNumber The L2 block number to use for the game. function testFuzz_setAnchorState_retiredGame_fails(uint256 _l2BlockNumber) public { // Grab block number of the existing anchor root. @@ -802,11 +857,15 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init // Mock that the game was respected. vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(true)); - // Mock the respectedGameTypeUpdatedAt call to be later than the game's creation time. + // Set the retirement timestamp. + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.updateRetirementTimestamp(); + + // Mock the call to createdAt. vm.mockCall( - address(optimismPortal2), - abi.encodeCall(optimismPortal2.respectedGameTypeUpdatedAt, ()), - abi.encode(gameProxy.createdAt().raw() + 1) + address(gameProxy), + abi.encodeCall(gameProxy.createdAt, ()), + abi.encode(anchorStateRegistry.retirementTimestamp() - 1) ); // Update the anchor state. @@ -819,4 +878,153 @@ contract AnchorStateRegistry_SetAnchorState_TestFail is AnchorStateRegistry_Init assertEq(updatedL2BlockNumber, l2BlockNumber); assertEq(updatedRoot.raw(), root.raw()); } + + /// @notice Tests that setAnchorState will revert if the superchain is paused. + function test_setAnchorState_superchainPaused_fails() public { + // Pause the superchain. + vm.prank(superchainConfig.guardian()); + superchainConfig.pause("testing"); + + // Update the anchor state. + vm.prank(address(gameProxy)); + vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_InvalidAnchorGame.selector); + anchorStateRegistry.setAnchorState(gameProxy); + } +} + +contract AnchorStateRegistry_setRespectedGameType_Test is AnchorStateRegistry_Init { + /// @notice Tests that setRespectedGameType succeeds when called by the guardian + /// @param _gameType The game type to set as respected + function testFuzz_setRespectedGameType_succeeds(GameType _gameType) public { + // Call as guardian + vm.prank(superchainConfig.guardian()); + vm.expectEmit(address(anchorStateRegistry)); + emit RespectedGameTypeSet(_gameType); + anchorStateRegistry.setRespectedGameType(_gameType); + + // Verify the game type was set + assertEq(anchorStateRegistry.respectedGameType().raw(), _gameType.raw()); + } +} + +contract AnchorStateRegistry_setRespectedGameType_TestFail is AnchorStateRegistry_Init { + /// @notice Tests that setRespectedGameType reverts when not called by the guardian + /// @param _gameType The game type to attempt to set + /// @param _caller The address attempting to call the function + function testFuzz_setRespectedGameType_notGuardian_reverts(GameType _gameType, address _caller) public { + // Ensure caller is not the guardian + vm.assume(_caller != superchainConfig.guardian()); + + // Attempt to call as non-guardian + vm.prank(_caller); + vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_Unauthorized.selector); + anchorStateRegistry.setRespectedGameType(_gameType); + } +} + +contract AnchorStateRegistry_updateRetirementTimestamp_Test is AnchorStateRegistry_Init { + /// @notice Tests that updateRetirementTimestamp succeeds when called by the guardian + function test_updateRetirementTimestamp_succeeds() public { + // Call as guardian + vm.prank(superchainConfig.guardian()); + vm.expectEmit(address(anchorStateRegistry)); + emit RetirementTimestampSet(block.timestamp); + anchorStateRegistry.updateRetirementTimestamp(); + + // Verify the timestamp was set + assertEq(anchorStateRegistry.retirementTimestamp(), block.timestamp); + } + + /// @notice Tests that updateRetirementTimestamp can be called multiple times by the guardian + function test_updateRetirementTimestamp_multipleUpdates_succeeds() public { + // First update + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.updateRetirementTimestamp(); + uint64 firstTimestamp = anchorStateRegistry.retirementTimestamp(); + + // Warp forward and update again + vm.warp(block.timestamp + 1000); + vm.prank(superchainConfig.guardian()); + vm.expectEmit(address(anchorStateRegistry)); + emit RetirementTimestampSet(block.timestamp); + anchorStateRegistry.updateRetirementTimestamp(); + + // Verify the timestamp was updated + assertEq(anchorStateRegistry.retirementTimestamp(), block.timestamp); + assertGt(anchorStateRegistry.retirementTimestamp(), firstTimestamp); + } +} + +contract AnchorStateRegistry_updateRetirementTimestamp_TestFail is AnchorStateRegistry_Init { + /// @notice Tests that updateRetirementTimestamp reverts when not called by the guardian + /// @param _caller The address attempting to call the function + function testFuzz_updateRetirementTimestamp_notGuardian_reverts(address _caller) public { + // Ensure caller is not the guardian + vm.assume(_caller != superchainConfig.guardian()); + + // Attempt to call as non-guardian + vm.prank(_caller); + vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_Unauthorized.selector); + anchorStateRegistry.updateRetirementTimestamp(); + } +} + +contract AnchorStateRegistry_blacklistDisputeGame_Test is AnchorStateRegistry_Init { + /// @notice Tests that blacklistDisputeGame succeeds when called by the guardian + function test_blacklistDisputeGame_succeeds() public { + // Call as guardian + vm.prank(superchainConfig.guardian()); + vm.expectEmit(address(anchorStateRegistry)); + emit DisputeGameBlacklisted(gameProxy); + anchorStateRegistry.blacklistDisputeGame(gameProxy); + + // Verify the game was blacklisted + assertTrue(anchorStateRegistry.disputeGameBlacklist(gameProxy)); + } + + /// @notice Tests that multiple games can be blacklisted + function test_blacklistDisputeGame_multipleGames_succeeds() public { + // Create a second game proxy + IDisputeGame secondGame = IDisputeGame(address(0x123)); + + // Blacklist both games + vm.startPrank(superchainConfig.guardian()); + anchorStateRegistry.blacklistDisputeGame(gameProxy); + anchorStateRegistry.blacklistDisputeGame(secondGame); + vm.stopPrank(); + + // Verify both games are blacklisted + assertTrue(anchorStateRegistry.disputeGameBlacklist(gameProxy)); + assertTrue(anchorStateRegistry.disputeGameBlacklist(secondGame)); + } +} + +contract AnchorStateRegistry_blacklistDisputeGame_TestFail is AnchorStateRegistry_Init { + /// @notice Tests that blacklistDisputeGame reverts when not called by the guardian + /// @param _caller The address attempting to call the function + function testFuzz_blacklistDisputeGame_notGuardian_reverts(address _caller) public { + // Ensure caller is not the guardian + vm.assume(_caller != superchainConfig.guardian()); + + // Attempt to call as non-guardian + vm.prank(_caller); + vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_Unauthorized.selector); + anchorStateRegistry.blacklistDisputeGame(gameProxy); + } + + /// @notice Tests that blacklisting a game twice succeeds but doesn't change state + function test_blacklistDisputeGame_twice_succeeds() public { + // Blacklist the game + vm.startPrank(superchainConfig.guardian()); + anchorStateRegistry.blacklistDisputeGame(gameProxy); + + // Blacklist again - should emit event but not change state + vm.expectEmit(address(anchorStateRegistry)); + emit DisputeGameBlacklisted(gameProxy); + anchorStateRegistry.blacklistDisputeGame(gameProxy); + vm.stopPrank(); + + // Verify the game is still blacklisted + assertTrue(anchorStateRegistry.disputeGameBlacklist(gameProxy)); + } } diff --git a/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol b/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol index a90f62ea68..5c681b0532 100644 --- a/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol +++ b/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol @@ -96,6 +96,12 @@ contract FaultDisputeGame_Init is DisputeGameFactory_Init { // Register the game implementation with the factory. disputeGameFactory.setImplementation(GAME_TYPE, gameImpl); uint256 bondAmount = disputeGameFactory.initBonds(GAME_TYPE); + + // Warp ahead of the game retirement timestamp if needed. + if (block.timestamp <= anchorStateRegistry.retirementTimestamp()) { + vm.warp(anchorStateRegistry.retirementTimestamp() + 1); + } + // Create a new game. gameProxy = IFaultDisputeGame( payable(address(disputeGameFactory.create{ value: bondAmount }(GAME_TYPE, rootClaim, extraData))) @@ -1849,14 +1855,14 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init { /// resolves in favor of the defender but the game state is not newer than the anchor state. function test_resolve_validOlderStateSameAnchor_succeeds() public { // Mock the game block to be older than the game state. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2BlockNumber, ()), abi.encode(0)); + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2SequenceNumber, ()), abi.encode(0)); // Confirm that the anchor state is newer than the game state. (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); - assert(l2BlockNumber >= gameProxy.l2BlockNumber()); + assert(l2BlockNumber >= gameProxy.l2SequenceNumber()); // Resolve the game. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2BlockNumber, ()), abi.encode(0)); + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2SequenceNumber, ()), abi.encode(0)); vm.warp(block.timestamp + 3 days + 12 hours); gameProxy.resolveClaim(0, 0); assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS)); diff --git a/packages/contracts-bedrock/test/dispute/SuperFaultDisputeGame.t.sol b/packages/contracts-bedrock/test/dispute/SuperFaultDisputeGame.t.sol index 099a85e56c..5cf181ea12 100644 --- a/packages/contracts-bedrock/test/dispute/SuperFaultDisputeGame.t.sol +++ b/packages/contracts-bedrock/test/dispute/SuperFaultDisputeGame.t.sol @@ -27,6 +27,7 @@ import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IPreimageOracle } from "interfaces/dispute/IBigStepper.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; +import { ISuperFaultDisputeGame } from "interfaces/dispute/ISuperFaultDisputeGame.sol"; import { IDelayedWETH } from "interfaces/dispute/IDelayedWETH.sol"; contract SuperFaultDisputeGame_Init is DisputeGameFactory_Init { @@ -34,9 +35,9 @@ contract SuperFaultDisputeGame_Init is DisputeGameFactory_Init { GameType internal constant GAME_TYPE = GameType.wrap(4); /// @dev The implementation of the game. - IFaultDisputeGame internal gameImpl; + ISuperFaultDisputeGame internal gameImpl; /// @dev The `Clone` proxy of the game. - IFaultDisputeGame internal gameProxy; + ISuperFaultDisputeGame internal gameProxy; /// @dev The extra data passed to the game for initialization. bytes internal extraData; @@ -46,14 +47,14 @@ contract SuperFaultDisputeGame_Init is DisputeGameFactory_Init { event ReceiveETH(uint256 amount); - function init(Claim rootClaim, Claim absolutePrestate, uint256 l2BlockNumber) public { + function init(Claim rootClaim, Claim absolutePrestate, uint256 l2SequenceNumber) public { // Set the time to a realistic date. if (!isForkTest()) { vm.warp(1690906994); } // Set the extra data for the game creation - extraData = abi.encode(l2BlockNumber); + extraData = abi.encode(l2SequenceNumber); // Set preimage oracle challenge period to something arbitrary (4 seconds) just so we can // actually test the clock extensions later on. This is not a realistic value. @@ -68,14 +69,14 @@ contract SuperFaultDisputeGame_Init is DisputeGameFactory_Init { ); // Deploy an implementation of the fault game - gameImpl = IFaultDisputeGame( + gameImpl = ISuperFaultDisputeGame( DeployUtils.create1({ _name: "SuperFaultDisputeGame", _args: DeployUtils.encodeConstructor( abi.encodeCall( - IFaultDisputeGame.__constructor__, + ISuperFaultDisputeGame.__constructor__, ( - IFaultDisputeGame.GameConstructorParams({ + ISuperFaultDisputeGame.GameConstructorParams({ gameType: GAME_TYPE, absolutePrestate: absolutePrestate, maxGameDepth: 2 ** 3, @@ -98,10 +99,15 @@ contract SuperFaultDisputeGame_Init is DisputeGameFactory_Init { uint256 bondAmount = disputeGameFactory.initBonds(GAME_TYPE); vm.prank(superchainConfig.guardian()); - optimismPortal2.setRespectedGameType(GAME_TYPE); + anchorStateRegistry.setRespectedGameType(GAME_TYPE); + + // Warp ahead of the game retirement timestamp if needed. + if (block.timestamp <= anchorStateRegistry.retirementTimestamp()) { + vm.warp(anchorStateRegistry.retirementTimestamp() + 1); + } // Create a new game. - gameProxy = IFaultDisputeGame( + gameProxy = ISuperFaultDisputeGame( payable(address(disputeGameFactory.create{ value: bondAmount }(GAME_TYPE, rootClaim, extraData))) ); @@ -135,8 +141,8 @@ contract SuperFaultDisputeGame_Test is SuperFaultDisputeGame_Init { bytes internal absolutePrestateData; /// @dev The absolute prestate of the trace. Claim internal absolutePrestate; - /// @dev A valid l2BlockNumber that comes after the current anchor root block. - uint256 validL2BlockNumber; + /// @dev A valid l2SequenceNumber that comes after the current anchor root block. + uint256 validl2SequenceNumber; function setUp() public override { absolutePrestateData = abi.encode(0); @@ -146,17 +152,17 @@ contract SuperFaultDisputeGame_Test is SuperFaultDisputeGame_Init { // Get the actual anchor roots (Hash root, uint256 l2Bn) = anchorStateRegistry.getAnchorRoot(); - validL2BlockNumber = l2Bn + 1; + validl2SequenceNumber = l2Bn + 1; ROOT_CLAIM = Claim.wrap(Hash.unwrap(root)); if (isForkTest()) { - // Set the init bond of anchor game type 0 to be 0. + // Set the init bond of anchor game type 4 to be 0. vm.store( - address(disputeGameFactory), keccak256(abi.encode(GameType.wrap(0), uint256(102))), bytes32(uint256(0)) + address(disputeGameFactory), keccak256(abi.encode(GameType.wrap(4), uint256(102))), bytes32(uint256(0)) ); } - super.init({ rootClaim: ROOT_CLAIM, absolutePrestate: absolutePrestate, l2BlockNumber: validL2BlockNumber }); + super.init({ rootClaim: ROOT_CLAIM, absolutePrestate: absolutePrestate, l2SequenceNumber: validl2SequenceNumber }); } //////////////////////////////////////////////////////////////// @@ -180,9 +186,9 @@ contract SuperFaultDisputeGame_Test is SuperFaultDisputeGame_Init { _name: "SuperFaultDisputeGame", _args: DeployUtils.encodeConstructor( abi.encodeCall( - IFaultDisputeGame.__constructor__, + ISuperFaultDisputeGame.__constructor__, ( - IFaultDisputeGame.GameConstructorParams({ + ISuperFaultDisputeGame.GameConstructorParams({ gameType: GAME_TYPE, absolutePrestate: absolutePrestate, maxGameDepth: _maxGameDepth, @@ -224,9 +230,9 @@ contract SuperFaultDisputeGame_Test is SuperFaultDisputeGame_Init { _name: "SuperFaultDisputeGame", _args: DeployUtils.encodeConstructor( abi.encodeCall( - IFaultDisputeGame.__constructor__, + ISuperFaultDisputeGame.__constructor__, ( - IFaultDisputeGame.GameConstructorParams({ + ISuperFaultDisputeGame.GameConstructorParams({ gameType: GAME_TYPE, absolutePrestate: absolutePrestate, maxGameDepth: 2 ** 3, @@ -264,9 +270,9 @@ contract SuperFaultDisputeGame_Test is SuperFaultDisputeGame_Init { _name: "SuperFaultDisputeGame", _args: DeployUtils.encodeConstructor( abi.encodeCall( - IFaultDisputeGame.__constructor__, + ISuperFaultDisputeGame.__constructor__, ( - IFaultDisputeGame.GameConstructorParams({ + ISuperFaultDisputeGame.GameConstructorParams({ gameType: GAME_TYPE, absolutePrestate: absolutePrestate, maxGameDepth: maxGameDepth, @@ -304,9 +310,9 @@ contract SuperFaultDisputeGame_Test is SuperFaultDisputeGame_Init { _name: "SuperFaultDisputeGame", _args: DeployUtils.encodeConstructor( abi.encodeCall( - IFaultDisputeGame.__constructor__, + ISuperFaultDisputeGame.__constructor__, ( - IFaultDisputeGame.GameConstructorParams({ + ISuperFaultDisputeGame.GameConstructorParams({ gameType: GAME_TYPE, absolutePrestate: absolutePrestate, maxGameDepth: 2 ** 3, @@ -352,9 +358,9 @@ contract SuperFaultDisputeGame_Test is SuperFaultDisputeGame_Init { _name: "SuperFaultDisputeGame", _args: DeployUtils.encodeConstructor( abi.encodeCall( - IFaultDisputeGame.__constructor__, + ISuperFaultDisputeGame.__constructor__, ( - IFaultDisputeGame.GameConstructorParams({ + ISuperFaultDisputeGame.GameConstructorParams({ gameType: GAME_TYPE, absolutePrestate: absolutePrestate, maxGameDepth: 16, @@ -390,9 +396,9 @@ contract SuperFaultDisputeGame_Test is SuperFaultDisputeGame_Init { _name: "SuperFaultDisputeGame", _args: DeployUtils.encodeConstructor( abi.encodeCall( - IFaultDisputeGame.__constructor__, + ISuperFaultDisputeGame.__constructor__, ( - IFaultDisputeGame.GameConstructorParams({ + ISuperFaultDisputeGame.GameConstructorParams({ gameType: GameType.wrap(type(uint32).max), absolutePrestate: absolutePrestate, maxGameDepth: 16, @@ -445,19 +451,20 @@ contract SuperFaultDisputeGame_Test is SuperFaultDisputeGame_Init { } //////////////////////////////////////////////////////////////// - // `IFaultDisputeGame` Implementation Tests // + // `ISuperFaultDisputeGame` Implementation Tests // //////////////////////////////////////////////////////////////// /// @dev Tests that the game cannot be initialized with an output root that commits to <= the configured starting /// block number function testFuzz_initialize_cannotProposeGenesis_reverts(uint256 _blockNumber) public { - (, uint256 startingL2Block) = gameProxy.startingOutputRoot(); + (, uint256 startingL2Block) = gameProxy.startingProposal(); _blockNumber = bound(_blockNumber, 0, startingL2Block); Claim claim = _dummyClaim(); vm.expectRevert(abi.encodeWithSelector(UnexpectedRootClaim.selector, claim)); - gameProxy = - IFaultDisputeGame(payable(address(disputeGameFactory.create(GAME_TYPE, claim, abi.encode(_blockNumber))))); + gameProxy = ISuperFaultDisputeGame( + payable(address(disputeGameFactory.create(GAME_TYPE, claim, abi.encode(_blockNumber)))) + ); } /// @dev Tests that the proxy receives ETH from the dispute game factory. @@ -466,11 +473,11 @@ contract SuperFaultDisputeGame_Test is SuperFaultDisputeGame_Init { vm.deal(address(this), _value); assertEq(address(gameProxy).balance, 0); - gameProxy = IFaultDisputeGame( + gameProxy = ISuperFaultDisputeGame( payable( address( disputeGameFactory.create{ value: _value }( - GAME_TYPE, arbitaryRootClaim, abi.encode(validL2BlockNumber) + GAME_TYPE, arbitaryRootClaim, abi.encode(validl2SequenceNumber) ) ) ) @@ -483,7 +490,7 @@ contract SuperFaultDisputeGame_Test is SuperFaultDisputeGame_Init { function test_initialize_invalidRoot_reverts() public { Claim claim = Claim.wrap(keccak256("invalid")); vm.expectRevert(bytes4(keccak256("SuperFaultDisputeGameInvalidRootClaim()"))); - gameProxy = IFaultDisputeGame(payable(address(disputeGameFactory.create(GAME_TYPE, claim, extraData)))); + gameProxy = ISuperFaultDisputeGame(payable(address(disputeGameFactory.create(GAME_TYPE, claim, extraData)))); } /// @dev Tests that the game cannot be initialized with extra data of the incorrect length (must be 32 bytes) @@ -500,14 +507,14 @@ contract SuperFaultDisputeGame_Test is SuperFaultDisputeGame_Init { bytes memory _extraData = new bytes(_extraDataLen); // Assign the first 32 bytes in `extraData` to a valid L2 block number passed the starting block. - (, uint256 startingL2Block) = gameProxy.startingOutputRoot(); + (, uint256 startingL2Block) = gameProxy.startingProposal(); assembly { mstore(add(_extraData, 0x20), add(startingL2Block, 1)) } Claim claim = _dummyClaim(); vm.expectRevert(abi.encodeWithSelector(BadExtraData.selector)); - gameProxy = IFaultDisputeGame(payable(address(disputeGameFactory.create(GAME_TYPE, claim, _extraData)))); + gameProxy = ISuperFaultDisputeGame(payable(address(disputeGameFactory.create(GAME_TYPE, claim, _extraData)))); } /// @dev Tests that the game is initialized with the correct data. @@ -548,7 +555,7 @@ contract SuperFaultDisputeGame_Test is SuperFaultDisputeGame_Init { // Creation should fail. vm.expectRevert(AnchorRootNotFound.selector); - gameProxy = IFaultDisputeGame(payable(address(disputeGameFactory.create(GAME_TYPE, _dummyClaim(), hex"")))); + gameProxy = ISuperFaultDisputeGame(payable(address(disputeGameFactory.create(GAME_TYPE, _dummyClaim(), hex"")))); } /// @dev Tests that the game cannot be initialized twice. @@ -557,13 +564,13 @@ contract SuperFaultDisputeGame_Test is SuperFaultDisputeGame_Init { gameProxy.initialize(); } - /// @dev Tests that startingOutputRoot and it's getters are set correctly. - function test_startingOutputRootGetters_succeeds() public view { - (Hash root, uint256 l2BlockNumber) = gameProxy.startingOutputRoot(); + /// @dev Tests that startingProposal and it's getters are set correctly. + function test_startingProposalGetters_succeeds() public view { + (Hash root, uint256 l2SequenceNumber) = gameProxy.startingProposal(); (Hash anchorRoot, uint256 anchorRootBlockNumber) = anchorStateRegistry.anchors(GAME_TYPE); - assertEq(gameProxy.startingBlockNumber(), l2BlockNumber); - assertEq(gameProxy.startingBlockNumber(), anchorRootBlockNumber); + assertEq(gameProxy.startingSequenceNumber(), l2SequenceNumber); + assertEq(gameProxy.startingSequenceNumber(), anchorRootBlockNumber); assertEq(Hash.unwrap(gameProxy.startingRootHash()), Hash.unwrap(root)); assertEq(Hash.unwrap(gameProxy.startingRootHash()), Hash.unwrap(anchorRoot)); } @@ -1661,8 +1668,8 @@ contract SuperFaultDisputeGame_Test is SuperFaultDisputeGame_Init { /// favor of the defender and the anchor state is older than the game state. function test_resolve_validNewerStateUpdatesAnchor_succeeds() public { // Confirm that the anchor state is older than the game state. - (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); - assert(l2BlockNumber < gameProxy.l2BlockNumber()); + (Hash root, uint256 l2SequenceNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); + assert(l2SequenceNumber < gameProxy.l2SequenceNumber()); // Resolve the game. vm.warp(block.timestamp + 3 days + 12 hours); @@ -1676,8 +1683,8 @@ contract SuperFaultDisputeGame_Test is SuperFaultDisputeGame_Init { gameProxy.closeGame(); // Confirm that the anchor state is now the same as the game state. - (root, l2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); - assertEq(l2BlockNumber, gameProxy.l2BlockNumber()); + (root, l2SequenceNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); + assertEq(l2SequenceNumber, gameProxy.l2SequenceNumber()); assertEq(root.raw(), gameProxy.rootClaim().raw()); } @@ -1685,14 +1692,14 @@ contract SuperFaultDisputeGame_Test is SuperFaultDisputeGame_Init { /// resolves in favor of the defender but the game state is not newer than the anchor state. function test_resolve_validOlderStateSameAnchor_succeeds() public { // Mock the game block to be older than the game state. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2BlockNumber, ()), abi.encode(0)); + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2SequenceNumber, ()), abi.encode(0)); // Confirm that the anchor state is newer than the game state. - (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); - assert(l2BlockNumber >= gameProxy.l2BlockNumber()); + (Hash root, uint256 l2SequenceNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); + assert(l2SequenceNumber >= gameProxy.l2SequenceNumber()); // Resolve the game. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2BlockNumber, ()), abi.encode(0)); + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2SequenceNumber, ()), abi.encode(0)); vm.warp(block.timestamp + 3 days + 12 hours); gameProxy.resolveClaim(0, 0); assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS)); @@ -1704,8 +1711,8 @@ contract SuperFaultDisputeGame_Test is SuperFaultDisputeGame_Init { gameProxy.closeGame(); // Confirm that the anchor state is the same as the initial anchor state. - (Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); - assertEq(updatedL2BlockNumber, l2BlockNumber); + (Hash updatedRoot, uint256 updatedl2SequenceNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); + assertEq(updatedl2SequenceNumber, l2SequenceNumber); assertEq(updatedRoot.raw(), root.raw()); } @@ -1713,8 +1720,8 @@ contract SuperFaultDisputeGame_Test is SuperFaultDisputeGame_Init { /// resolves in favor of the challenger, even if the game state is newer than the anchor. function test_resolve_invalidStateSameAnchor_succeeds() public { // Confirm that the anchor state is older than the game state. - (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); - assert(l2BlockNumber < gameProxy.l2BlockNumber()); + (Hash root, uint256 l2SequenceNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); + assert(l2SequenceNumber < gameProxy.l2SequenceNumber()); // Challenge the claim and resolve it. (,,,, Claim disputed,,) = gameProxy.claimData(0); @@ -1731,8 +1738,8 @@ contract SuperFaultDisputeGame_Test is SuperFaultDisputeGame_Init { gameProxy.closeGame(); // Confirm that the anchor state is the same as the initial anchor state. - (Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); - assertEq(updatedL2BlockNumber, l2BlockNumber); + (Hash updatedRoot, uint256 updatedl2SequenceNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); + assertEq(updatedl2SequenceNumber, l2SequenceNumber); assertEq(updatedRoot.raw(), root.raw()); } @@ -1972,7 +1979,7 @@ contract SuperFaultDisputeGame_Test is SuperFaultDisputeGame_Init { gameProxy.attack{ value: lastBond }(disputed, 4, _changeClaimStatus(_dummyClaim(), VMStatuses.PANIC)); // Expected start/disputed claims - (Hash root,) = gameProxy.startingOutputRoot(); + (Hash root,) = gameProxy.startingProposal(); bytes32 startingClaim = root.raw(); bytes32 disputedClaim = bytes32(uint256(3)); Position disputedPos = LibPosition.wrap(4, 0); @@ -1982,7 +1989,7 @@ contract SuperFaultDisputeGame_Test is SuperFaultDisputeGame_Init { gameProxy.l1Head().raw(), startingClaim, disputedClaim, - bytes32(uint256(gameProxy.l2BlockNumber()) << 0xC0) + bytes32(uint256(gameProxy.l2SequenceNumber()) << 0xC0) ]; for (uint256 i = 1; i <= 4; i++) { @@ -2032,7 +2039,7 @@ contract SuperFaultDisputeGame_Test is SuperFaultDisputeGame_Init { gameProxy.l1Head().raw(), startingClaim, disputedClaim, - bytes32(uint256(gameProxy.l2BlockNumber()) << 0xC0) + bytes32(uint256(gameProxy.l2SequenceNumber()) << 0xC0) ]; for (uint256 i = 1; i <= 4; i++) { @@ -2058,13 +2065,13 @@ contract SuperFaultDisputeGame_Test is SuperFaultDisputeGame_Init { /// @dev Tests that the L2 block number claim is favored over the bisected-to block when adding data /// - function test_addLocalData_l2BlockNumberExtension_succeeds() public { + function test_addLocalData_l2SequenceNumberExtension_succeeds() public { // Deploy a new dispute game with a L2 block number claim of 8. This is directly in the middle of // the leaves in our output bisection test tree, at SPLIT_DEPTH = 2 ** 2 - IFaultDisputeGame game = IFaultDisputeGame( + ISuperFaultDisputeGame game = ISuperFaultDisputeGame( address( disputeGameFactory.create( - GAME_TYPE, Claim.wrap(bytes32(uint256(0xFF))), abi.encode(uint256(validL2BlockNumber)) + GAME_TYPE, Claim.wrap(bytes32(uint256(0xFF))), abi.encode(uint256(validl2SequenceNumber)) ) ) ); @@ -2100,9 +2107,9 @@ contract SuperFaultDisputeGame_Test is SuperFaultDisputeGame_Init { bytes32 disputedClaim = bytes32(uint256(0xFF)); Position disputedPos = LibPosition.wrap(0, 0); - // Expected local data. This should be `l2BlockNumber`, and not the actual bisected-to block, + // Expected local data. This should be `l2SequenceNumber`, and not the actual bisected-to block, // as we choose the minimum between the two. - bytes32 expectedNumber = bytes32(uint256(validL2BlockNumber << 0xC0)); + bytes32 expectedNumber = bytes32(uint256(validl2SequenceNumber << 0xC0)); uint256 expectedLen = 8; uint256 l2NumberIdent = LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER; @@ -2314,7 +2321,7 @@ contract SuperFaultDisputeGame_Test is SuperFaultDisputeGame_Init { function _generateOutputRootProof( bytes32 _storageRoot, bytes32 _withdrawalRoot, - bytes memory _l2BlockNumber + bytes memory _l2SequenceNumber ) internal pure @@ -2330,7 +2337,7 @@ contract SuperFaultDisputeGame_Test is SuperFaultDisputeGame_Init { rawHeaderRLP[5] = hex"83FACADE"; rawHeaderRLP[6] = hex"83FACADE"; rawHeaderRLP[7] = hex"83FACADE"; - rawHeaderRLP[8] = RLPWriter.writeBytes(_l2BlockNumber); + rawHeaderRLP[8] = RLPWriter.writeBytes(_l2SequenceNumber); rlp_ = RLPWriter.writeList(rawHeaderRLP); // Output root @@ -2728,7 +2735,7 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_Init { Claim absolutePrestateExec = _changeClaimStatus(Claim.wrap(keccak256(absolutePrestateData_)), VMStatuses.UNFINISHED); Claim rootClaim = Claim.wrap(bytes32(uint256(_rootClaim))); - super.init({ rootClaim: rootClaim, absolutePrestate: absolutePrestateExec, l2BlockNumber: _rootClaim }); + super.init({ rootClaim: rootClaim, absolutePrestate: absolutePrestateExec, l2SequenceNumber: _rootClaim }); } /// @dev Helper to create actors for the 1v1 dispute. @@ -2743,13 +2750,13 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_Init { internal { honest = new HonestDisputeActor({ - _gameProxy: gameProxy, + _gameProxy: IFaultDisputeGame(address(gameProxy)), _l2Outputs: _honestL2Outputs, _trace: _honestTrace, _preStateData: _honestPreStateData }); dishonest = new HonestDisputeActor({ - _gameProxy: gameProxy, + _gameProxy: IFaultDisputeGame(address(gameProxy)), _l2Outputs: _dishonestL2Outputs, _trace: _dishonestTrace, _preStateData: _dishonestPreStateData @@ -2794,10 +2801,10 @@ contract SuperFaultDispute_1v1_Actors_Test is SuperFaultDisputeGame_Init { contract ClaimCreditReenter { Vm internal immutable vm; - IFaultDisputeGame internal immutable GAME; + ISuperFaultDisputeGame internal immutable GAME; uint256 public numCalls; - constructor(IFaultDisputeGame _gameProxy, Vm _vm) { + constructor(ISuperFaultDisputeGame _gameProxy, Vm _vm) { GAME = _gameProxy; vm = _vm; } diff --git a/packages/contracts-bedrock/test/integration/EventLogger.t.sol b/packages/contracts-bedrock/test/integration/EventLogger.t.sol index 17a49315d3..277d6cebf4 100644 --- a/packages/contracts-bedrock/test/integration/EventLogger.t.sol +++ b/packages/contracts-bedrock/test/integration/EventLogger.t.sol @@ -9,19 +9,12 @@ import { EventLogger } from "../../src/integration/EventLogger.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; -import { CrossL2Inbox, Identifier as ImplIdentifier } from "src/L2/CrossL2Inbox.sol"; - -// @title MockL1BlockInfo -// @notice mock L1 block info to fake a deposit-context. -contract MockL1BlockInfo { - // @notice mock deposit-context that is never active - // @return always false - function isDeposit() external pure returns (bool isDeposit_) { - return false; - } -} +import { Identifier as ImplIdentifier } from "src/L2/CrossL2Inbox.sol"; +import { CrossL2InboxWithSlotWarming as CrossL2Inbox } from "test/L2/CrossL2Inbox.t.sol"; contract EventLogger_Initializer is Test { + event ExecutingMessage(bytes32 indexed msgHash, ImplIdentifier id); + EventLogger eventLogger; function setUp() public { @@ -31,10 +24,6 @@ contract EventLogger_Initializer is Test { vm.etch(Predeploys.CROSS_L2_INBOX, address(new CrossL2Inbox()).code); vm.label(Predeploys.CROSS_L2_INBOX, "CrossL2Inbox"); - - // CrossL2Inbox needs this to do the deposit-context check - vm.etch(Predeploys.L1_BLOCK_ATTRIBUTES, address(new MockL1BlockInfo()).code); - vm.label(Predeploys.L1_BLOCK_ATTRIBUTES, "L1Block"); } } @@ -101,9 +90,9 @@ contract EventLoggerTest is EventLogger_Initializer { /// @notice It should succeed with any Identifier function test_validateMessage_succeeds( address _origin, - uint256 _blockNumber, - uint256 _logIndex, - uint256 _timestamp, + uint64 _blockNumber, + uint32 _logIndex, + uint64 _timestamp, uint256 _chainId, bytes32 _msgHash ) @@ -123,9 +112,16 @@ contract EventLoggerTest is EventLogger_Initializer { timestamp: _timestamp, chainId: _chainId }); + address emitter = Predeploys.CROSS_L2_INBOX; + + // Warm the slot for the function to succeed + bytes32 checksum = CrossL2Inbox(emitter).calculateChecksum(idImpl, _msgHash); + CrossL2Inbox(emitter).warmSlot(checksum); + vm.expectEmit(false, false, false, true, emitter); - emit CrossL2Inbox.ExecutingMessage(_msgHash, idImpl); + emit ExecutingMessage(_msgHash, idImpl); + eventLogger.validateMessage(idIface, _msgHash); } } diff --git a/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol index 15ce33c252..8ad3665389 100644 --- a/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/invariants/OptimismPortal2.t.sol @@ -14,7 +14,6 @@ import { ResourceMetering } from "src/L1/ResourceMetering.sol"; import { Constants } from "src/libraries/Constants.sol"; import { Types } from "src/libraries/Types.sol"; import "src/dispute/lib/Types.sol"; -import "src/libraries/PortalErrors.sol"; // Interfaces import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; @@ -119,7 +118,7 @@ contract OptimismPortal2_Invariant_Harness is CommonTest { }); // Warp forward in time to ensure that the game is created after the retirement timestamp. - vm.warp(optimismPortal2.respectedGameTypeUpdatedAt() + 1 seconds); + vm.warp(anchorStateRegistry.retirementTimestamp() + 1); // Create a dispute game with the output root we've proposed. _proposedBlockNumber = 0xFF; @@ -140,7 +139,7 @@ contract OptimismPortal2_Invariant_Harness is CommonTest { game.resolve(); // Fund the portal so that we can withdraw ETH. - vm.deal(address(optimismPortal2), 0xFFFFFFFF); + vm.deal(address(ethLockbox), 0xFFFFFFFF); } } @@ -188,7 +187,7 @@ contract OptimismPortal2_CannotTimeTravel is OptimismPortal2_Invariant_Harness { /// A withdrawal that has been proven should not be able to be finalized /// until after the proof maturity period has elapsed. function invariant_cannotFinalizeBeforePeriodHasPassed() external { - vm.expectRevert("OptimismPortal: proven withdrawal has not matured yet"); + vm.expectRevert(IOptimismPortal2.OptimismPortal_ProofNotOldEnough.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } } @@ -217,7 +216,7 @@ contract OptimismPortal2_CannotFinalizeTwice is OptimismPortal2_Invariant_Harnes /// Ensures that there is no chain of calls that can be made that allows a withdrawal to be /// finalized twice. function invariant_cannotFinalizeTwice() external { - vm.expectRevert(AlreadyFinalized.selector); + vm.expectRevert(IOptimismPortal2.OptimismPortal_AlreadyFinalized.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } } diff --git a/packages/contracts-bedrock/test/invariants/SuperFaultDisputeGame.t.sol b/packages/contracts-bedrock/test/invariants/SuperFaultDisputeGame.t.sol index 0b85caa084..cfd40b35bf 100644 --- a/packages/contracts-bedrock/test/invariants/SuperFaultDisputeGame.t.sol +++ b/packages/contracts-bedrock/test/invariants/SuperFaultDisputeGame.t.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.15; // Testing import { SuperFaultDisputeGame_Init } from "test/dispute/SuperFaultDisputeGame.t.sol"; +import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { RandomClaimActor } from "test/invariants/FaultDisputeGame.t.sol"; // Libraries @@ -18,9 +19,9 @@ contract SuperFaultDisputeGame_Solvency_Invariant is SuperFaultDisputeGame_Init function setUp() public override { super.setUp(); - super.init({ rootClaim: ROOT_CLAIM, absolutePrestate: ABSOLUTE_PRESTATE, l2BlockNumber: 0x10 }); + super.init({ rootClaim: ROOT_CLAIM, absolutePrestate: ABSOLUTE_PRESTATE, l2SequenceNumber: 0x10 }); - actor = new RandomClaimActor(gameProxy, vm); + actor = new RandomClaimActor(IFaultDisputeGame(address(gameProxy)), vm); targetContract(address(actor)); vm.startPrank(address(actor)); diff --git a/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol b/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol index 4ca84d96b0..cae03ff5be 100644 --- a/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol +++ b/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol @@ -42,10 +42,10 @@ contract SystemConfig_GasLimitBoundaries_Invariant is Test { l1CrossDomainMessenger: address(0), l1ERC721Bridge: address(0), l1StandardBridge: address(0), - disputeGameFactory: address(0), optimismPortal: address(0), optimismMintableERC20Factory: address(0) - }) + }), + 1234 // _l2ChainId ) ) ); diff --git a/packages/contracts-bedrock/test/kontrol/proofs/OptimismPortal2.k.sol b/packages/contracts-bedrock/test/kontrol/proofs/OptimismPortal2.k.sol index 7d7b49e3a0..756b225751 100644 --- a/packages/contracts-bedrock/test/kontrol/proofs/OptimismPortal2.k.sol +++ b/packages/contracts-bedrock/test/kontrol/proofs/OptimismPortal2.k.sol @@ -6,7 +6,6 @@ import { KontrolUtils } from "./utils/KontrolUtils.sol"; import { Types } from "src/libraries/Types.sol"; import { IOptimismPortal2 as OptimismPortal } from "interfaces/L1/IOptimismPortal2.sol"; import { ISuperchainConfig as SuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; -import "src/libraries/PortalErrors.sol"; contract OptimismPortal2Kontrol is DeploymentSummaryFaultProofs, KontrolUtils { OptimismPortal optimismPortal; @@ -26,7 +25,7 @@ contract OptimismPortal2Kontrol is DeploymentSummaryFaultProofs, KontrolUtils { vm.prank(optimismPortal.guardian()); superchainConfig.pause("identifier"); - vm.expectRevert(CallPaused.selector); + vm.expectRevert(OptimismPortal.OptimismPortal_CallPaused.selector); optimismPortal.finalizeWithdrawalTransaction(_tx); } @@ -47,7 +46,7 @@ contract OptimismPortal2Kontrol is DeploymentSummaryFaultProofs, KontrolUtils { vm.prank(optimismPortal.guardian()); superchainConfig.pause("identifier"); - vm.expectRevert(CallPaused.selector); + vm.expectRevert(OptimismPortal.OptimismPortal_CallPaused.selector); optimismPortal.proveWithdrawalTransaction(_tx, _l2OutputIndex, _outputRootProof, _withdrawalProof); } diff --git a/packages/contracts-bedrock/test/libraries/Encoding.t.sol b/packages/contracts-bedrock/test/libraries/Encoding.t.sol index 277cce328d..76ab3343d7 100644 --- a/packages/contracts-bedrock/test/libraries/Encoding.t.sol +++ b/packages/contracts-bedrock/test/libraries/Encoding.t.sol @@ -1,17 +1,24 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities +// Testing import { CommonTest } from "test/setup/CommonTest.sol"; // Libraries +import { Encoding } from "src/libraries/Encoding.sol"; import { Types } from "src/libraries/Types.sol"; import { LegacyCrossDomainUtils } from "src/libraries/LegacyCrossDomainUtils.sol"; -// Target contract -import { Encoding } from "src/libraries/Encoding.sol"; +contract Encoding_TestInit is CommonTest { + Encoding_Harness encoding; -contract Encoding_Test is CommonTest { + function setUp() public override { + super.setUp(); + encoding = new Encoding_Harness(); + } +} + +contract Encoding_Test is Encoding_TestInit { /// @dev Tests encoding and decoding a nonce and version. function testFuzz_nonceVersioning_succeeds(uint240 _nonce, uint16 _version) external pure { (uint240 nonce, uint16 version) = Encoding.decodeVersionedNonce(Encoding.encodeVersionedNonce(_nonce, _version)); @@ -77,8 +84,6 @@ contract Encoding_Test is CommonTest { uint256 minInvalidNonce = (uint256(type(uint240).max) + 1) * 2; nonce = bound(nonce, minInvalidNonce, type(uint256).max); - EncodingContract encoding = new EncodingContract(); - vm.expectRevert(bytes("Encoding: unknown cross domain message version")); encoding.encodeCrossDomainMessage(nonce, address(this), address(this), 1, 100, hex""); } @@ -107,7 +112,182 @@ contract Encoding_Test is CommonTest { } } -contract EncodingContract { +contract Encoding_encodeSuperRootProof_Test is Encoding_TestInit { + /// @notice Tests successful encoding of a valid super root proof + /// @param _timestamp The timestamp of the super root proof + /// @param _length The number of output roots in the super root proof + /// @param _seed The seed used to generate the output roots + function testFuzz_encodeSuperRootProof_succeeds(uint64 _timestamp, uint256 _length, uint256 _seed) external pure { + // Ensure at least 1 element and cap at a reasonable maximum to avoid gas issues + _length = uint256(bound(_length, 1, 50)); + + // Create output roots array + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](_length); + + // Generate deterministic chain IDs and roots based on the seed + for (uint256 i = 0; i < _length; i++) { + // Use different derivations of the seed for each value + uint256 chainId = uint256(keccak256(abi.encode(_seed, "chainId", i))); + bytes32 root = keccak256(abi.encode(_seed, "root", i)); + + outputRoots[i] = Types.OutputRootWithChainId({ chainId: chainId, root: root }); + } + + // Create the super root proof + Types.SuperRootProof memory proof = + Types.SuperRootProof({ version: 0x01, timestamp: _timestamp, outputRoots: outputRoots }); + + // Encode the proof + bytes memory encoded = Encoding.encodeSuperRootProof(proof); + + // Verify encoding structure + assertEq(encoded[0], bytes1(0x01), "Version byte should be 0x01"); + + // Verify timestamp (bytes 1-8) + bytes8 encodedTimestamp; + for (uint256 i = 0; i < 8; i++) { + encodedTimestamp |= bytes8(encoded[i + 1]) >> (i * 8); + } + assertEq(uint64(encodedTimestamp), _timestamp, "Timestamp should match"); + + // Verify each chain ID and root is encoded correctly + uint256 offset = 9; // 1 byte version + 8 bytes timestamp + for (uint256 i = 0; i < _length; i++) { + // Extract chain ID (32 bytes) + uint256 encodedChainId; + assembly { + // Load 32 bytes from encoded at position offset + encodedChainId := mload(add(add(encoded, 32), offset)) + } + assertEq(encodedChainId, outputRoots[i].chainId, "Chain ID should match"); + offset += 32; + + // Extract root (32 bytes) + bytes32 encodedRoot; + assembly { + // Load 32 bytes from encoded at position offset + encodedRoot := mload(add(add(encoded, 32), offset)) + } + assertEq(encodedRoot, outputRoots[i].root, "Root should match"); + offset += 32; + } + + // Verify total length + assertEq(encoded.length, 9 + (_length * 64), "Encoded length should match expected"); + } + + /// @notice Tests encoding with a single output root + function test_encodeSuperRootProof_singleOutputRoot_succeeds() external pure { + // Create a single output root + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](1); + outputRoots[0] = Types.OutputRootWithChainId({ chainId: 10, root: bytes32(uint256(0xdeadbeef)) }); + + // Create the super root proof + Types.SuperRootProof memory proof = + Types.SuperRootProof({ version: 0x01, timestamp: 1234567890, outputRoots: outputRoots }); + + // Encode the proof + bytes memory encoded = Encoding.encodeSuperRootProof(proof); + + // Expected: 1 byte version + 8 bytes timestamp + (32 bytes chainId + 32 bytes root) + assertEq(encoded.length, 1 + 8 + 64, "Encoded length should be 73 bytes"); + assertEq(encoded[0], bytes1(0x01), "First byte should be version 0x01"); + } + + /// @notice Tests encoding with multiple output roots + function test_encodeSuperRootProof_multipleOutputRoots_succeeds() external pure { + // Create multiple output roots + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](3); + outputRoots[0] = Types.OutputRootWithChainId({ chainId: 10, root: bytes32(uint256(0xdeadbeef)) }); + outputRoots[1] = Types.OutputRootWithChainId({ chainId: 20, root: bytes32(uint256(0xbeefcafe)) }); + outputRoots[2] = Types.OutputRootWithChainId({ chainId: 30, root: bytes32(uint256(0xcafebabe)) }); + + // Create the super root proof + Types.SuperRootProof memory proof = + Types.SuperRootProof({ version: 0x01, timestamp: 1234567890, outputRoots: outputRoots }); + + // Encode the proof + bytes memory encoded = Encoding.encodeSuperRootProof(proof); + + // Expected: 1 byte version + 8 bytes timestamp + 3 * (32 bytes chainId + 32 bytes root) + assertEq(encoded.length, 1 + 8 + (3 * 64), "Encoded length should be 201 bytes"); + } + + /// @notice Tests that the Solidity impl of encodeSuperRootProof matches the FFI impl + /// @param _timestamp The timestamp of the super root proof + /// @param _length The number of output roots in the super root proof + /// @param _seed The seed used to generate the output roots + function testDiff_encodeSuperRootProof_succeeds(uint64 _timestamp, uint256 _length, uint256 _seed) external { + // Ensure at least 1 element and cap at a reasonable maximum to avoid gas issues + _length = uint256(bound(_length, 1, 50)); + + // Create output roots array + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](_length); + + // Generate deterministic chain IDs and roots based on the seed + for (uint256 i = 0; i < _length; i++) { + // Use different derivations of the seed for each value + uint256 chainId = uint256(keccak256(abi.encode(_seed, "chainId", i))); + bytes32 root = keccak256(abi.encode(_seed, "root", i)); + + outputRoots[i] = Types.OutputRootWithChainId({ chainId: chainId, root: root }); + } + + // Create the super root proof + Types.SuperRootProof memory proof = + Types.SuperRootProof({ version: 0x01, timestamp: _timestamp, outputRoots: outputRoots }); + + // Encode using the Solidity implementation + bytes memory encoding1 = Encoding.encodeSuperRootProof(proof); + + // Encode using the FFI implementation + bytes memory encoding2 = ffi.encodeSuperRootProof(proof); + + // Compare the results + assertEq(encoding1, encoding2, "Solidity and FFI implementations should match"); + } +} + +contract Encoding_encodeSuperRootProof_TestFail is Encoding_TestInit { + /// @notice Tests that encoding fails when version is not 0x01 + /// @param _version The version to use for the super root proof + /// @param _timestamp The timestamp of the super root proof + function testFuzz_encodeSuperRootProof_invalidVersion_reverts(bytes1 _version, uint64 _timestamp) external { + // Ensure version is not 0x01 + if (_version == 0x01) { + _version = 0x02; + } + + // Create a minimal valid output roots array + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](1); + outputRoots[0] = Types.OutputRootWithChainId({ chainId: 1, root: bytes32(uint256(1)) }); + + // Create the super root proof with invalid version + Types.SuperRootProof memory proof = + Types.SuperRootProof({ version: _version, timestamp: _timestamp, outputRoots: outputRoots }); + + // Expect revert when encoding + vm.expectRevert(Encoding.Encoding_InvalidSuperRootVersion.selector); + encoding.encodeSuperRootProof(proof); + } + + /// @notice Tests that encoding fails when output roots array is empty + /// @param _timestamp The timestamp of the super root proof + function testFuzz_encodeSuperRootProof_emptyOutputRoots_reverts(uint64 _timestamp) external { + // Create an empty output roots array + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](0); + + // Create the super root proof with empty output roots + Types.SuperRootProof memory proof = + Types.SuperRootProof({ version: 0x01, timestamp: _timestamp, outputRoots: outputRoots }); + + // Expect revert when encoding + vm.expectRevert(Encoding.Encoding_EmptySuperRoot.selector); + encoding.encodeSuperRootProof(proof); + } +} + +contract Encoding_Harness { function encodeCrossDomainMessage( uint256 nonce, address sender, @@ -122,4 +302,8 @@ contract EncodingContract { { return Encoding.encodeCrossDomainMessage(nonce, sender, target, value, gasLimit, data); } + + function encodeSuperRootProof(Types.SuperRootProof memory proof) external pure returns (bytes memory) { + return Encoding.encodeSuperRootProof(proof); + } } diff --git a/packages/contracts-bedrock/test/libraries/Hashing.t.sol b/packages/contracts-bedrock/test/libraries/Hashing.t.sol index 45242acb08..73aef7cb22 100644 --- a/packages/contracts-bedrock/test/libraries/Hashing.t.sol +++ b/packages/contracts-bedrock/test/libraries/Hashing.t.sol @@ -12,6 +12,12 @@ import { LegacyCrossDomainUtils } from "src/libraries/LegacyCrossDomainUtils.sol // Target contract import { Hashing } from "src/libraries/Hashing.sol"; +contract Hashing_Harness { + function hashSuperRootProof(Types.SuperRootProof memory _proof) external pure returns (bytes32) { + return Hashing.hashSuperRootProof(_proof); + } +} + contract Hashing_hashDepositSource_Test is CommonTest { /// @notice Tests that hashDepositSource returns the correct hash in a simple case. function test_hashDepositSource_succeeds() external pure { @@ -136,3 +142,51 @@ contract Hashing_hashDepositTransaction_Test is CommonTest { ); } } + +contract Hashing_hashSuperRootProof_Test is CommonTest { + Hashing_Harness internal harness; + + /// @notice Sets up the test. + function setUp() public override { + super.setUp(); + harness = new Hashing_Harness(); + } + + /// @notice Tests that the Solidity impl of hashSuperRootProof matches the FFI impl + /// @param _proof The super root proof to test. + function testDiff_hashSuperRootProof_succeeds(Types.SuperRootProof memory _proof) external { + // Make sure the proof has the right version. + _proof.version = 0x01; + + // Make sure the proof has at least one output root. + if (_proof.outputRoots.length == 0) { + _proof.outputRoots = new Types.OutputRootWithChainId[](1); + _proof.outputRoots[0] = Types.OutputRootWithChainId({ + chainId: vm.randomUint(0, type(uint64).max), + root: bytes32(vm.randomUint()) + }); + } + + // Encode using the Solidity implementation + bytes32 hash1 = harness.hashSuperRootProof(_proof); + + // Encode using the FFI implementation + bytes32 hash2 = ffi.hashSuperRootProof(_proof); + + // Compare the results + assertEq(hash1, hash2, "Solidity and FFI implementations should match"); + } + + /// @notice Tests that hashSuperRootProof reverts when the version is incorrect. + /// @param _proof The super root proof to test. + function testFuzz_hashSuperRootProof_wrongVersion_reverts(Types.SuperRootProof memory _proof) external { + // 0x01 is the correct version, so we need any other version. + if (_proof.version == 0x01) { + _proof.version = 0x00; + } + + // Should always revert when the version is incorrect. + vm.expectRevert(Encoding.Encoding_InvalidSuperRootVersion.selector); + harness.hashSuperRootProof(_proof); + } +} diff --git a/packages/contracts-bedrock/test/mocks/CrossL2InboxWithSlotWarming.sol b/packages/contracts-bedrock/test/mocks/CrossL2InboxWithSlotWarming.sol new file mode 100644 index 0000000000..722b957c44 --- /dev/null +++ b/packages/contracts-bedrock/test/mocks/CrossL2InboxWithSlotWarming.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +// Target contracts +import { CrossL2Inbox, Identifier } from "src/L2/CrossL2Inbox.sol"; + +/// @title CrossL2InboxWithSlotWarming +/// @dev CrossL2Inbox contract with a method to warm a slot. +contract CrossL2InboxWithSlotWarming is CrossL2Inbox { + // Getter for warming a slot on tests. + function warmSlot(bytes32 _slot) external view returns (uint256 res_) { + assembly { + res_ := sload(_slot) + } + } + + // Getter to expose `_isWarm` function for the tests. + function isWarm(bytes32 _slot) external view returns (bool isWarm_, uint256 value_) { + (isWarm_, value_) = _isWarm(_slot); + } + + // Getter to expose `_calculateChecksum` function for the tests. + function calculateChecksum(Identifier memory _id, bytes32 _msgHash) external pure returns (bytes32 checksum_) { + checksum_ = _calculateChecksum(_id, _msgHash); + } +} diff --git a/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol b/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol index 85680343cc..895aa9e082 100644 --- a/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol @@ -17,7 +17,7 @@ import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.so import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; +import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPortal2.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.sol"; import { IL1ERC721Bridge } from "interfaces/L1/IL1ERC721Bridge.sol"; @@ -25,11 +25,11 @@ import { IL1StandardBridge } from "interfaces/L1/IL1StandardBridge.sol"; import { IOptimismMintableERC20Factory } from "interfaces/universal/IOptimismMintableERC20Factory.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; import { DeployImplementationsInput, DeployImplementations, - DeployImplementationsInterop, DeployImplementationsOutput } from "scripts/deploy/DeployImplementations.s.sol"; @@ -88,7 +88,8 @@ contract DeployImplementationsOutput_Test is Test { function test_set_succeeds() public { IOPContractsManager opcm = IOPContractsManager(address(makeAddr("opcm"))); - IOptimismPortal2 optimismPortalImpl = IOptimismPortal2(payable(makeAddr("optimismPortalImpl"))); + IOptimismPortal optimismPortalImpl = IOptimismPortal(payable(makeAddr("optimismPortalImpl"))); + IETHLockbox ethLockboxImpl = IETHLockbox(payable(makeAddr("ethLockboxImpl"))); IDelayedWETH delayedWETHImpl = IDelayedWETH(payable(makeAddr("delayedWETHImpl"))); IPreimageOracle preimageOracleSingleton = IPreimageOracle(makeAddr("preimageOracleSingleton")); IMIPS mipsSingleton = IMIPS(makeAddr("mipsSingleton")); @@ -104,6 +105,7 @@ contract DeployImplementationsOutput_Test is Test { vm.etch(address(opcm), hex"01"); vm.etch(address(optimismPortalImpl), hex"01"); + vm.etch(address(ethLockboxImpl), hex"01"); vm.etch(address(delayedWETHImpl), hex"01"); vm.etch(address(preimageOracleSingleton), hex"01"); vm.etch(address(mipsSingleton), hex"01"); @@ -116,6 +118,7 @@ contract DeployImplementationsOutput_Test is Test { vm.etch(address(anchorStateRegistryImpl), hex"01"); dio.set(dio.opcm.selector, address(opcm)); dio.set(dio.optimismPortalImpl.selector, address(optimismPortalImpl)); + dio.set(dio.ethLockboxImpl.selector, address(ethLockboxImpl)); dio.set(dio.delayedWETHImpl.selector, address(delayedWETHImpl)); dio.set(dio.preimageOracleSingleton.selector, address(preimageOracleSingleton)); dio.set(dio.mipsSingleton.selector, address(mipsSingleton)); @@ -139,6 +142,7 @@ contract DeployImplementationsOutput_Test is Test { assertEq(address(optimismMintableERC20FactoryImpl), address(dio.optimismMintableERC20FactoryImpl()), "900"); assertEq(address(disputeGameFactoryImpl), address(dio.disputeGameFactoryImpl()), "950"); assertEq(address(anchorStateRegistryImpl), address(dio.anchorStateRegistryImpl()), "960"); + assertEq(address(ethLockboxImpl), address(dio.ethLockboxImpl()), "1000"); } function test_getters_whenNotSet_reverts() public { @@ -147,6 +151,9 @@ contract DeployImplementationsOutput_Test is Test { vm.expectRevert(expectedErr); dio.optimismPortalImpl(); + vm.expectRevert(expectedErr); + dio.ethLockboxImpl(); + vm.expectRevert(expectedErr); dio.delayedWETHImpl(); @@ -186,6 +193,10 @@ contract DeployImplementationsOutput_Test is Test { vm.expectRevert(expectedErr); dio.optimismPortalImpl(); + dio.set(dio.ethLockboxImpl.selector, emptyAddr); + vm.expectRevert(expectedErr); + dio.ethLockboxImpl(); + dio.set(dio.delayedWETHImpl.selector, emptyAddr); vm.expectRevert(expectedErr); dio.delayedWETHImpl(); @@ -247,7 +258,6 @@ contract DeployImplementations_Test is Test { // By deploying the `DeployImplementations` contract with this virtual function, we provide a // hook that child contracts can override to return a different implementation of the contract. - // This lets us test e.g. the `DeployImplementationsInterop` contract without duplicating test code. function createDeployImplementationsContract() internal virtual returns (DeployImplementations) { return new DeployImplementations(); } @@ -284,11 +294,12 @@ contract DeployImplementations_Test is Test { deployImplementations.deployL1StandardBridgeImpl(dio); deployImplementations.deployOptimismMintableERC20FactoryImpl(dio); deployImplementations.deployOptimismPortalImpl(dii, dio); + deployImplementations.deployETHLockboxImpl(dio); deployImplementations.deployDelayedWETHImpl(dii, dio); deployImplementations.deployPreimageOracleSingleton(dii, dio); deployImplementations.deployMipsSingleton(dii, dio); deployImplementations.deployDisputeGameFactoryImpl(dio); - deployImplementations.deployAnchorStateRegistryImpl(dio); + deployImplementations.deployAnchorStateRegistryImpl(dii, dio); deployImplementations.deployOPContractsManager(dii, dio); // Store the original addresses. @@ -298,6 +309,7 @@ contract DeployImplementations_Test is Test { address l1StandardBridgeImpl = address(dio.l1StandardBridgeImpl()); address optimismMintableERC20FactoryImpl = address(dio.optimismMintableERC20FactoryImpl()); address optimismPortalImpl = address(dio.optimismPortalImpl()); + address ethLockboxImpl = address(dio.ethLockboxImpl()); address delayedWETHImpl = address(dio.delayedWETHImpl()); address preimageOracleSingleton = address(dio.preimageOracleSingleton()); address mipsSingleton = address(dio.mipsSingleton()); @@ -312,11 +324,12 @@ contract DeployImplementations_Test is Test { deployImplementations.deployL1StandardBridgeImpl(dio); deployImplementations.deployOptimismMintableERC20FactoryImpl(dio); deployImplementations.deployOptimismPortalImpl(dii, dio); + deployImplementations.deployETHLockboxImpl(dio); deployImplementations.deployDelayedWETHImpl(dii, dio); deployImplementations.deployPreimageOracleSingleton(dii, dio); deployImplementations.deployMipsSingleton(dii, dio); deployImplementations.deployDisputeGameFactoryImpl(dio); - deployImplementations.deployAnchorStateRegistryImpl(dio); + deployImplementations.deployAnchorStateRegistryImpl(dii, dio); deployImplementations.deployOPContractsManager(dii, dio); // Assert that the addresses did not change. @@ -332,6 +345,7 @@ contract DeployImplementations_Test is Test { assertEq(disputeGameFactoryImpl, address(dio.disputeGameFactoryImpl()), "1000"); assertEq(anchorStateRegistryImpl, address(dio.anchorStateRegistryImpl()), "1100"); assertEq(opcm, address(dio.opcm()), "1200"); + assertEq(ethLockboxImpl, address(dio.ethLockboxImpl()), "1300"); } function testFuzz_run_memory_succeeds(bytes32 _seed) public { @@ -443,9 +457,3 @@ contract DeployImplementations_Test is Test { deployImplementations.run(dii, dio); } } - -contract DeployImplementationsInterop_Test is DeployImplementations_Test { - function createDeployImplementationsContract() internal override returns (DeployImplementations) { - return new DeployImplementationsInterop(); - } -} diff --git a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol index 48d8046873..26c3d3df69 100644 --- a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol @@ -7,7 +7,6 @@ import { DeploySuperchainInput, DeploySuperchain, DeploySuperchainOutput } from import { DeployImplementationsInput, DeployImplementations, - DeployImplementationsInterop, DeployImplementationsOutput } from "scripts/deploy/DeployImplementations.s.sol"; import { DeployOPChainInput, DeployOPChain, DeployOPChainOutput } from "scripts/deploy/DeployOPChain.s.sol"; @@ -27,7 +26,7 @@ import { IProtocolVersions, ProtocolVersion } from "interfaces/L1/IProtocolVersi import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; -import { Claim, Duration, GameType, GameTypes, Hash, OutputRoot } from "src/dispute/lib/Types.sol"; +import { Claim, Duration, GameType, GameTypes, Hash, Proposal } from "src/dispute/lib/Types.sol"; contract DeployOPChainInput_Test is Test { DeployOPChainInput doi; @@ -328,7 +327,7 @@ contract DeployOPChain_TestBase is Test { uint32 basefeeScalar = 100; uint32 blobBaseFeeScalar = 200; uint256 l2ChainId = 300; - OutputRoot startingAnchorRoot = OutputRoot({ root: Hash.wrap(keccak256("defaultOutputRoot")), l2BlockNumber: 400 }); + Proposal startingAnchorRoot = Proposal({ root: Hash.wrap(keccak256("defaultOutputRoot")), l2SequenceNumber: 400 }); IOPContractsManager opcm = IOPContractsManager(address(0)); string saltMixer = "defaultSaltMixer"; uint64 gasLimit = 60_000_000; @@ -522,9 +521,3 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { doi.set(doi.disputeMaxClockDuration.selector, disputeMaxClockDuration); } } - -contract DeployOPChain_Test_Interop is DeployOPChain_Test { - function createDeployImplementationsContract() internal override returns (DeployImplementations) { - return new DeployImplementationsInterop(); - } -} diff --git a/packages/contracts-bedrock/test/opcm/ManageDependencies.t.sol b/packages/contracts-bedrock/test/opcm/ManageDependencies.t.sol deleted file mode 100644 index aeac3e23a0..0000000000 --- a/packages/contracts-bedrock/test/opcm/ManageDependencies.t.sol +++ /dev/null @@ -1,95 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import { Test } from "forge-std/Test.sol"; -import { ISystemConfigInterop } from "interfaces/L1/ISystemConfigInterop.sol"; -import { ManageDependencies, ManageDependenciesInput } from "scripts/deploy/ManageDependencies.s.sol"; - -contract ManageDependencies_Test is Test { - ManageDependencies script; - ManageDependenciesInput input; - address mockSystemConfig; - uint256 testChainId; - - event DependencyAdded(uint256 indexed chainId); - event DependencyRemoved(uint256 indexed chainId); - - function setUp() public { - script = new ManageDependencies(); - input = new ManageDependenciesInput(); - mockSystemConfig = makeAddr("systemConfig"); - testChainId = 123; - - vm.etch(mockSystemConfig, hex"01"); - } - - function test_run_add_succeeds() public { - input.set(input.systemConfig.selector, mockSystemConfig); - input.set(input.chainId.selector, testChainId); - input.set(input.remove.selector, false); - - // Expect the addDependency call - vm.mockCall(mockSystemConfig, abi.encodeCall(ISystemConfigInterop.addDependency, testChainId), bytes("")); - script.run(input); - } - - function test_run_remove_succeeds() public { - input.set(input.systemConfig.selector, mockSystemConfig); - input.set(input.chainId.selector, testChainId); - input.set(input.remove.selector, true); - - vm.mockCall(mockSystemConfig, abi.encodeCall(ISystemConfigInterop.removeDependency, testChainId), bytes("")); - script.run(input); - } -} - -contract ManageDependenciesInput_Test is Test { - ManageDependenciesInput input; - - function setUp() public { - input = new ManageDependenciesInput(); - } - - function test_getters_whenNotSet_reverts() public { - vm.expectRevert("ManageDependenciesInput: not set"); - input.chainId(); - - vm.expectRevert("ManageDependenciesInput: not set"); - input.systemConfig(); - - // remove() doesn't revert when not set, returns false - assertFalse(input.remove()); - } - - function test_set_succeeds() public { - address systemConfig = makeAddr("systemConfig"); - uint256 chainId = 123; - bool remove = true; - - vm.etch(systemConfig, hex"01"); - - input.set(input.systemConfig.selector, systemConfig); - input.set(input.chainId.selector, chainId); - input.set(input.remove.selector, remove); - - assertEq(address(input.systemConfig()), systemConfig); - assertEq(input.chainId(), chainId); - assertTrue(input.remove()); - } - - function test_set_withZeroAddress_reverts() public { - vm.expectRevert("ManageDependenciesInput: cannot set zero address"); - input.set(input.systemConfig.selector, address(0)); - } - - function test_set_withInvalidSelector_reverts() public { - vm.expectRevert("ManageDependenciesInput: unknown selector"); - input.set(bytes4(0xdeadbeef), makeAddr("test")); - - vm.expectRevert("ManageDependenciesInput: unknown selector"); - input.set(bytes4(0xdeadbeef), uint256(1)); - - vm.expectRevert("ManageDependenciesInput: unknown selector"); - input.set(bytes4(0xdeadbeef), true); - } -} diff --git a/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol b/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol index 2dca24b486..c5ef3011a9 100644 --- a/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol +++ b/packages/contracts-bedrock/test/opcm/SetDisputeGameImpl.t.sol @@ -4,15 +4,14 @@ pragma solidity ^0.8.0; import { Test } from "forge-std/Test.sol"; import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; -import { GameType } from "src/dispute/lib/Types.sol"; +import { GameType, Proposal, Hash } from "src/dispute/lib/Types.sol"; import { SetDisputeGameImpl, SetDisputeGameImplInput } from "scripts/deploy/SetDisputeGameImpl.s.sol"; import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.sol"; import { Proxy } from "src/universal/Proxy.sol"; -import { OptimismPortal2 } from "src/L1/OptimismPortal2.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; -import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { SuperchainConfig } from "src/L1/SuperchainConfig.sol"; +import { AnchorStateRegistry } from "src/dispute/AnchorStateRegistry.sol"; +import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; contract SetDisputeGameImplInput_Test is Test { SetDisputeGameImplInput input; @@ -70,7 +69,7 @@ contract SetDisputeGameImpl_Test is Test { SetDisputeGameImpl script; SetDisputeGameImplInput input; IDisputeGameFactory factory; - IOptimismPortal2 portal; + IAnchorStateRegistry anchorStateRegistry; address mockImpl; uint32 gameType; @@ -78,8 +77,8 @@ contract SetDisputeGameImpl_Test is Test { script = new SetDisputeGameImpl(); input = new SetDisputeGameImplInput(); DisputeGameFactory dgfImpl = new DisputeGameFactory(); - OptimismPortal2 portalImpl = new OptimismPortal2(0, 0); SuperchainConfig supConfigImpl = new SuperchainConfig(); + AnchorStateRegistry anchorStateRegistryImpl = new AnchorStateRegistry(0); Proxy supConfigProxy = new Proxy(address(1)); vm.prank(address(1)); @@ -92,21 +91,21 @@ contract SetDisputeGameImpl_Test is Test { factoryProxy.upgradeToAndCall(address(dgfImpl), abi.encodeCall(dgfImpl.initialize, (address(this)))); factory = IDisputeGameFactory(address(factoryProxy)); - Proxy portalProxy = new Proxy(address(1)); + Proxy anchorStateRegistryProxy = new Proxy(address(1)); vm.prank(address(1)); - portalProxy.upgradeToAndCall( - address(portalImpl), + anchorStateRegistryProxy.upgradeToAndCall( + address(anchorStateRegistryImpl), abi.encodeCall( - portalImpl.initialize, + anchorStateRegistryImpl.initialize, ( - factory, - ISystemConfig(makeAddr("sysConfig")), ISuperchainConfig(address(supConfigProxy)), + factory, + Proposal({ root: Hash.wrap(0), l2SequenceNumber: 0 }), GameType.wrap(100) ) ) ); - portal = IOptimismPortal2(payable(address(portalProxy))); + anchorStateRegistry = IAnchorStateRegistry(address(anchorStateRegistryProxy)); mockImpl = makeAddr("impl"); gameType = 999; @@ -115,7 +114,7 @@ contract SetDisputeGameImpl_Test is Test { function test_run_succeeds() public { input.set(input.factory.selector, address(factory)); input.set(input.impl.selector, mockImpl); - input.set(input.portal.selector, address(portal)); + input.set(input.anchorStateRegistry.selector, address(anchorStateRegistry)); input.set(input.gameType.selector, gameType); script.run(input); @@ -124,7 +123,7 @@ contract SetDisputeGameImpl_Test is Test { function test_run_whenImplAlreadySet_reverts() public { input.set(input.factory.selector, address(factory)); input.set(input.impl.selector, mockImpl); - input.set(input.portal.selector, address(portal)); + input.set(input.anchorStateRegistry.selector, address(anchorStateRegistry)); input.set(input.gameType.selector, gameType); // First run should succeed diff --git a/packages/contracts-bedrock/test/periphery/monitoring/DisputeMonitorHelper.t.sol b/packages/contracts-bedrock/test/periphery/monitoring/DisputeMonitorHelper.t.sol new file mode 100644 index 0000000000..332d20f81d --- /dev/null +++ b/packages/contracts-bedrock/test/periphery/monitoring/DisputeMonitorHelper.t.sol @@ -0,0 +1,403 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing +import { CommonTest } from "test/setup/CommonTest.sol"; + +// Contracts +import { DisputeMonitorHelper } from "src/periphery/monitoring/DisputeMonitorHelper.sol"; +import { GameTypes, Claim } from "src/dispute/lib/Types.sol"; +import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; + +contract DisputeMonitorHelper_TestInit is CommonTest { + DisputeMonitorHelper helper; + + function setUp() public override { + super.setUp(); + helper = new DisputeMonitorHelper(); + + // Skip everything for forked networks. Tests here involve carefully controlling the list + // of games in the factory, which is not possible on forked networks. + skipIfForkTest("DisputeMonitorHelper tests are not applicable to forked networks"); + } + + /// @notice Helper to create a game with a specific timestamp. + /// @param _timestamp The timestamp to set for the game creation. + /// @param _claim The claim for the game. + /// @return gameIndex_ The index of the created game. + function createGameWithTimestamp(uint256 _timestamp, bytes32 _claim) internal returns (uint256 gameIndex_) { + // Store current timestamp to restore later. + uint256 currentTimestamp = block.timestamp; + + // Warp to the desired timestamp. + vm.warp(_timestamp); + + // Create the game. + disputeGameFactory.create(GameTypes.CANNON, Claim.wrap(_claim), abi.encode(999999)); + + // Get the game index. + gameIndex_ = disputeGameFactory.gameCount() - 1; + + // Restore the original timestamp. + vm.warp(currentTimestamp); + } +} + +contract DisputeMonitorHelper_isGameRegistered_Test is DisputeMonitorHelper_TestInit { + /// @notice Test that a game created through the factory is registered. + function test_isGameRegistered_validGame_succeeds() external { + // Create a game through the factory + uint256 gameIndex = createGameWithTimestamp(block.timestamp, bytes32(uint256(1))); + + // Get the game address + (,, IDisputeGame game) = disputeGameFactory.gameAtIndex(gameIndex); + + // Check that the game is registered + bool isRegistered = helper.isGameRegistered(disputeGameFactory, game); + assertTrue(isRegistered, "Game should be registered"); + } + + /// @notice Test that a random address is not registered as a game. + function test_isGameRegistered_invalidGame_fails() external { + // Create a random address that is not a registered game + address randomAddress = address(uint160(uint256(keccak256(abi.encodePacked(block.timestamp))))); + IDisputeGame fakeGame = IDisputeGame(randomAddress); + + // Mock the gameData call on the fake game to return something + vm.mockCall( + randomAddress, + abi.encodeCall(IDisputeGame.gameData, ()), + abi.encode(GameTypes.CANNON, Claim.wrap(bytes32(uint256(1))), abi.encode(999999)) + ); + + // Check that the random address is not registered + bool isRegistered = helper.isGameRegistered(disputeGameFactory, fakeGame); + assertFalse(isRegistered, "Random address should not be registered as a game"); + } +} + +contract DisputeMonitorHelper_search_Test is DisputeMonitorHelper_TestInit { + /// @notice Fuzz test for searching with random timestamps and directions. + /// @param _numGames Number of games to generate for the test. + /// @param _searchOlderThan Search direction. + function testFuzz_search_succeeds(uint8 _numGames, bool _searchOlderThan) external { + // Convert into search directon. + DisputeMonitorHelper.SearchDirection direction = _searchOlderThan + ? DisputeMonitorHelper.SearchDirection.OLDER_THAN_OR_EQ + : DisputeMonitorHelper.SearchDirection.NEWER_THAN_OR_EQ; + + // Create an array to store game timestamps and indices. + uint256[] memory gameTimestamps = new uint256[](_numGames); + uint256[] memory gameIndices = new uint256[](_numGames); + + // Start with a base timestamp. + uint256 currentTimestamp = 1000; + + // Create games with increasing timestamps. + for (uint256 i = 0; i < _numGames; i++) { + // Generate a random timestamp increase (between 0 and 1000). Games can have the same + // exact timestamp. If this happens, we expect the earliest index in the NEWER_THAN + // case or the latest index in the OLDER_THAN case. + uint256 timestampIncrease = vm.randomUint(0, 1000); + currentTimestamp += timestampIncrease; + + // Store the timestamp. + gameTimestamps[i] = currentTimestamp; + + // Create the game and store its index. + gameIndices[i] = createGameWithTimestamp(currentTimestamp, bytes32(i + 1)); + } + + // Verify the game count. + assertEq(disputeGameFactory.gameCount(), _numGames, "wrong number of created games"); + + // If we have no games, expect the NoGames error no matter the timestamp. + if (_numGames == 0) { + uint256 foundIndex = + helper.search(disputeGameFactory, vm.randomUint(0, block.timestamp + 1000000), direction); + assertEq(foundIndex, type(uint256).max, "found index should be max"); + } else { + // Do 10 random tests inside the range we just created. + for (uint256 i = 0; i < 10; i++) { + // Pick a random timestamp that might be outside of the bounds of the available + // timestamps. We'll use cases that fall outside of the available bounds to make sure + // that errors are working as expected. + uint256 rangeStart = gameTimestamps[0]; + uint256 rangeEnd = gameTimestamps[gameTimestamps.length - 1]; + uint256 randomTimestamp = vm.randomUint(rangeStart - 500, rangeEnd + 500); + + // Different assertions for different cases. + if ( + (direction == DisputeMonitorHelper.SearchDirection.OLDER_THAN_OR_EQ && randomTimestamp < rangeStart) + || ( + direction == DisputeMonitorHelper.SearchDirection.NEWER_THAN_OR_EQ && randomTimestamp > rangeEnd + ) + ) { + // If we fall outside of the range, expect the max index representing that no + // valid game was found. + uint256 foundIndex = helper.search(disputeGameFactory, randomTimestamp, direction); + assertEq(foundIndex, type(uint256).max, "found index should be max"); + } else { + // Otherwise, we expect a valid index. Manual linear search to figure out what + // the right answer should be. + uint256 targetIndex; + for (uint256 j = 0; j < _numGames; j++) { + if (direction == DisputeMonitorHelper.SearchDirection.OLDER_THAN_OR_EQ) { + if (gameTimestamps[j] <= randomTimestamp) { + // Need to find newer indices for the OLDER_THAN_OR_EQ case. + targetIndex = j; + } + } else { + if (gameTimestamps[j] >= randomTimestamp) { + // Only find the first index for the NEWER_THAN_OR_EQ case. + targetIndex = j; + break; + } + } + } + + // Perform the search with our function. + uint256 foundIndex = helper.search(disputeGameFactory, randomTimestamp, direction); + + // Indices should match. + assertEq(foundIndex, targetIndex, "found incorrect index"); + } + } + } + } + + /// @notice Test that searching for a game with no games returns the max index. + /// @param _timestamp The timestamp to search for. + /// @param _searchOlderThan Whether to search for an older or newer game. + function testFuzz_search_noGames_succeeds(uint256 _timestamp, bool _searchOlderThan) external view { + // Convert into search directon. + DisputeMonitorHelper.SearchDirection direction = _searchOlderThan + ? DisputeMonitorHelper.SearchDirection.OLDER_THAN_OR_EQ + : DisputeMonitorHelper.SearchDirection.NEWER_THAN_OR_EQ; + + // Search for a game with no games. + uint256 foundIndex = helper.search(disputeGameFactory, _timestamp, direction); + + // Should return the max index. + assertEq(foundIndex, type(uint256).max, "found index should be max"); + } + + /// @notice Test that searching for a game older than all games returns the max index. + function test_search_olderThanEverything_succeeds() external { + // Select a target timestamp. + uint256 targetTimestamp = vm.randomUint(1, 100); + + // Create one game. + createGameWithTimestamp(targetTimestamp, bytes32(uint256(1))); + + // Search by providing a timestamp that is before all games. + uint256 foundIndex = helper.search( + disputeGameFactory, targetTimestamp - 1, DisputeMonitorHelper.SearchDirection.OLDER_THAN_OR_EQ + ); + assertEq(foundIndex, type(uint256).max, "found index should be max"); + } + + /// @notice Test that searching for a game newer than all games returns the max index. + function test_search_newerThanEverything_succeeds() external { + // Select a target timestamp. + uint256 targetTimestamp = vm.randomUint(1, 100); + + // Create one game. + createGameWithTimestamp(targetTimestamp, bytes32(uint256(1))); + + // Search by providing a timestamp that is after all games. + uint256 foundIndex = helper.search( + disputeGameFactory, targetTimestamp + 1, DisputeMonitorHelper.SearchDirection.NEWER_THAN_OR_EQ + ); + assertEq(foundIndex, type(uint256).max, "found index should be max"); + } +} + +contract DisputeMonitorHelper_getUnresolvedGames_Test is DisputeMonitorHelper_TestInit { + /// @notice Fuzz test for searching for unresolved games. + /// @param _numGames Number of games to create. + /// @param _resolvedPercent Percentage of games to mark as resolved. + function testFuzz_getUnresolvedGames_succeeds(uint8 _numGames, uint8 _resolvedPercent) external { + // Want _resolvedPercent to have 5% steps. + _resolvedPercent = _resolvedPercent % 20; + + // Create an array to store game timestamps and indices. + bool[] memory gameStatuses = new bool[](_numGames); + uint256[] memory gameTimestamps = new uint256[](_numGames); + uint256[] memory gameIndices = new uint256[](_numGames); + + // Start with a base timestamp. + uint256 currentTimestamp = 1000; + + // Create games with increasing timestamps. + for (uint256 i = 0; i < _numGames; i++) { + // Generate a random timestamp increase (between 0 and 1000). + uint256 timestampIncrease = vm.randomUint(0, 1000); + currentTimestamp += timestampIncrease; + + // Store the timestamp. + gameTimestamps[i] = currentTimestamp; + + // Create the game and store its index. + gameIndices[i] = createGameWithTimestamp(currentTimestamp, bytes32(i + 1)); + + // Decide if the game should be resolved or not. + if (_resolvedPercent != 0 && vm.randomUint(0, 20) <= _resolvedPercent) { + // Winner winner! + // Mock the resolvedAt timestamp to anything but 0. + gameStatuses[i] = true; + (,, IDisputeGame game) = disputeGameFactory.gameAtIndex(i); + vm.mockCall(address(game), abi.encodeCall(game.resolvedAt, ()), abi.encode(block.timestamp)); + } else { + gameStatuses[i] = false; + } + } + + // If we have no games, expect an empty array always + if (_numGames == 0) { + uint256 creationRangeStart = vm.randomUint(0, block.timestamp + 1000000); + uint256 creationRangeEnd = vm.randomUint(creationRangeStart, creationRangeStart + 1000000); + IDisputeGame[] memory results = + helper.getUnresolvedGames(disputeGameFactory, creationRangeStart, creationRangeEnd); + assertEq(results.length, 0, "empty case returned games"); + } else { + // Do 10 random tests inside the range we just created. + for (uint256 i = 0; i < 10; i++) { + // Pick a random timestamp that might be outside of the bounds of the available + // timestamps. We'll use cases that fall outside of the available bounds to make sure + // that errors are working as expected. + uint256 rangeStart = gameTimestamps[0]; + uint256 rangeEnd = gameTimestamps[gameTimestamps.length - 1]; + uint256 randomRangeStart = vm.randomUint(rangeStart - 500, rangeEnd + 500); + uint256 randomRangeEnd = vm.randomUint(rangeStart - 500, rangeEnd + 500); + + // Different assertions for different cases. + if (randomRangeStart > randomRangeEnd) { + // If the boundaries are invalid, expect an error. + vm.expectRevert(DisputeMonitorHelper.DisputeMonitorHelper_InvalidSearchRange.selector); + helper.getUnresolvedGames(disputeGameFactory, randomRangeStart, randomRangeEnd); + } else if (randomRangeEnd < rangeStart || randomRangeStart > rangeEnd) { + // If the boundaries are valid but the range is outside of the range of + // timestamps created by the array of games, expect an empty array. + IDisputeGame[] memory results = + helper.getUnresolvedGames(disputeGameFactory, randomRangeStart, randomRangeEnd); + assertEq(results.length, 0, "results should be empty"); + } else { + // Otherwise, we expect a number of results equal to the number of games that + // are unresolved within the range. Start by allocating an array with the total + // number of games, though actual size will be less. + IDisputeGame[] memory expected = new IDisputeGame[](_numGames); + + // Create the array of expected results. + uint256 insertedCount = 0; + for (uint256 j = 0; j < _numGames; j++) { + if ( + gameStatuses[j] == false && gameTimestamps[j] >= randomRangeStart + && gameTimestamps[j] <= randomRangeEnd + ) { + (,, IDisputeGame game) = disputeGameFactory.gameAtIndex(j); + expected[insertedCount] = game; + insertedCount++; + } + } + + // Perform the search with our function. + IDisputeGame[] memory results = + helper.getUnresolvedGames(disputeGameFactory, randomRangeStart, randomRangeEnd); + + // Should have a number of results equal to the elements inserted. + assertEq(results.length, insertedCount, "unexpected results length"); + + // Each element should match. + for (uint256 j = 0; j < results.length; j++) { + assertEq(address(results[j]), address(expected[j])); + } + } + } + } + } + + /// @notice Test that getting unresolved games with no games returns an empty array. + /// @param _creationRangeStart The start of the creation range. + /// @param _creationRangeEnd The end of the creation range. + function testFuzz_getUnresolvedGames_noGames_succeeds( + uint256 _creationRangeStart, + uint256 _creationRangeEnd + ) + external + view + { + // Make sure the boundaries are valid. + _creationRangeEnd = bound(_creationRangeEnd, _creationRangeStart, type(uint256).max); + + // Get the unresolved games. + IDisputeGame[] memory results = + helper.getUnresolvedGames(disputeGameFactory, _creationRangeStart, _creationRangeEnd); + assertEq(results.length, 0, "empty case returned games"); + } + + /// @notice Test that getting unresolved games between two timestamps returns an empty array. + function test_getUnresolvedGames_betweenTimestamps_succeeds() external { + // Select two timestamps. + uint256 timestamp1 = 100; + uint256 timestamp2 = 200; + + // Create two games. + createGameWithTimestamp(timestamp1, bytes32(uint256(1))); + createGameWithTimestamp(timestamp2, bytes32(uint256(2))); + + // Select a range that falls between the two timestamps. + uint256 rangeStart = timestamp1 + 1; + uint256 rangeEnd = timestamp2 - 1; + + // Get the unresolved games. + IDisputeGame[] memory results = helper.getUnresolvedGames(disputeGameFactory, rangeStart, rangeEnd); + assertEq(results.length, 0, "expected 0 games"); + } + + /// @notice Fuzz test for getting unresolved games with bad boundaries. + /// @param _creationRangeStart The start of the creation range. + /// @param _creationRangeEnd The end of the creation range. + function testFuzz_getUnresolvedGames_badBoundaries_reverts( + uint256 _creationRangeStart, + uint256 _creationRangeEnd + ) + external + { + // Make sure the boundaries are deliberately invalid. + _creationRangeStart = bound(_creationRangeStart, 1, type(uint256).max); + _creationRangeEnd = bound(_creationRangeEnd, 0, _creationRangeStart - 1); + + // Get the unresolved games. + vm.expectRevert(DisputeMonitorHelper.DisputeMonitorHelper_InvalidSearchRange.selector); + helper.getUnresolvedGames(disputeGameFactory, _creationRangeStart, _creationRangeEnd); + } + + /// @notice Fuzz test for getting unresolved games when all games are resolved. + /// @param _numGames Number of games to create. + function testFuzz_getUnresolvedGames_allResolved_succeeds(uint8 _numGames) external { + // Create 5 games. + for (uint256 i = 0; i < _numGames; i++) { + createGameWithTimestamp(1000, bytes32(uint256(i + 1))); + (,, IDisputeGame game) = disputeGameFactory.gameAtIndex(i); + vm.mockCall(address(game), abi.encodeCall(game.resolvedAt, ()), abi.encode(block.timestamp)); + } + + // Get the unresolved games. + IDisputeGame[] memory results = helper.getUnresolvedGames(disputeGameFactory, 0, type(uint256).max); + assertEq(results.length, 0, "expected 0 games"); + } + + /// @notice Fuzz test for getting unresolved games when all games are unresolved. + /// @param _numGames Number of games to create. + function testFuzz_getUnresolvedGames_allUnresolved_succeeds(uint8 _numGames) external { + // Create 5 games. + for (uint256 i = 0; i < _numGames; i++) { + createGameWithTimestamp(1000, bytes32(uint256(i + 1))); + } + + // Get the unresolved games. + IDisputeGame[] memory results = helper.getUnresolvedGames(disputeGameFactory, 0, type(uint256).max); + assertEq(results.length, _numGames, "expected 5 games"); + } +} diff --git a/packages/contracts-bedrock/test/safe/DeputyGuardianModule.t.sol b/packages/contracts-bedrock/test/safe/DeputyGuardianModule.t.sol index f17ec2204f..c1b4ad6fca 100644 --- a/packages/contracts-bedrock/test/safe/DeputyGuardianModule.t.sol +++ b/packages/contracts-bedrock/test/safe/DeputyGuardianModule.t.sol @@ -14,17 +14,13 @@ import "src/dispute/lib/Types.sol"; // Interfaces import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; -import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; -import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; contract DeputyGuardianModule_TestInit is CommonTest, SafeTestTools { using SafeTestLib for SafeInstance; - error Unauthorized(); - error ExecutionFailed(string); - event ExecutionFromModuleSuccess(address indexed); + event RetirementTimestampUpdated(Timestamp indexed); IDeputyGuardianModule deputyGuardianModule; SafeInstance safeInstance; @@ -91,7 +87,7 @@ contract DeputyGuardianModule_Pause_Test is DeputyGuardianModule_TestInit { contract DeputyGuardianModule_Pause_TestFail is DeputyGuardianModule_TestInit { /// @dev Tests that `pause` reverts when called by a non deputy guardian. function test_pause_notDeputyGuardian_reverts() external { - vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector)); + vm.expectRevert(abi.encodeWithSelector(IDeputyGuardianModule.DeputyGuardianModule_Unauthorized.selector)); deputyGuardianModule.pause(); } @@ -104,7 +100,12 @@ contract DeputyGuardianModule_Pause_TestFail is DeputyGuardianModule_TestInit { ); vm.prank(address(deputyGuardian)); - vm.expectRevert(abi.encodeWithSelector(ExecutionFailed.selector, "SuperchainConfig: pause() reverted")); + vm.expectRevert( + abi.encodeWithSelector( + IDeputyGuardianModule.DeputyGuardianModule_ExecutionFailed.selector, + "SuperchainConfig: pause() reverted" + ) + ); deputyGuardianModule.pause(); } } @@ -140,7 +141,7 @@ contract DeputyGuardianModule_Unpause_Test is DeputyGuardianModule_TestInit { contract DeputyGuardianModule_Unpause_TestFail is DeputyGuardianModule_Unpause_Test { /// @dev Tests that `unpause` reverts when called by a non deputy guardian. function test_unpause_notDeputyGuardian_reverts() external { - vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector)); + vm.expectRevert(abi.encodeWithSelector(IDeputyGuardianModule.DeputyGuardianModule_Unauthorized.selector)); deputyGuardianModule.unpause(); assertTrue(superchainConfig.paused()); } @@ -153,42 +154,14 @@ contract DeputyGuardianModule_Unpause_TestFail is DeputyGuardianModule_Unpause_T "SuperchainConfig: unpause reverted" ); - vm.prank(address(deputyGuardian)); - vm.expectRevert(abi.encodeWithSelector(ExecutionFailed.selector, "SuperchainConfig: unpause reverted")); - deputyGuardianModule.unpause(); - } -} - -contract DeputyGuardianModule_SetAnchorState_TestFail is DeputyGuardianModule_TestInit { - function test_setAnchorState_notDeputyGuardian_reverts() external { - IAnchorStateRegistry asr = IAnchorStateRegistry(makeAddr("asr")); - vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector)); - deputyGuardianModule.setAnchorState(asr, IFaultDisputeGame(address(0))); - } - - function test_setAnchorState_targetReverts_reverts() external { - IAnchorStateRegistry asr = IAnchorStateRegistry(makeAddr("asr")); - vm.mockCallRevert( - address(asr), abi.encodePacked(asr.setAnchorState.selector), "AnchorStateRegistry: setAnchorState reverted" - ); vm.prank(address(deputyGuardian)); vm.expectRevert( - abi.encodeWithSelector(ExecutionFailed.selector, "AnchorStateRegistry: setAnchorState reverted") - ); - deputyGuardianModule.setAnchorState(asr, IFaultDisputeGame(address(0))); - } -} - -contract DeputyGuardianModule_SetAnchorState_Test is DeputyGuardianModule_TestInit { - function test_setAnchorState_succeeds() external { - IAnchorStateRegistry asr = IAnchorStateRegistry(makeAddr("asr")); - vm.mockCall( - address(asr), abi.encodeCall(IAnchorStateRegistry.setAnchorState, (IFaultDisputeGame(address(0)))), "" + abi.encodeWithSelector( + IDeputyGuardianModule.DeputyGuardianModule_ExecutionFailed.selector, + "SuperchainConfig: unpause reverted" + ) ); - vm.expectEmit(address(safeInstance.safe)); - emit ExecutionFromModuleSuccess(address(deputyGuardianModule)); - vm.prank(address(deputyGuardian)); - deputyGuardianModule.setAnchorState(asr, IFaultDisputeGame(address(0))); + deputyGuardianModule.unpause(); } } @@ -205,8 +178,8 @@ contract DeputyGuardianModule_BlacklistDisputeGame_Test is DeputyGuardianModule_ emit DisputeGameBlacklisted(game); vm.prank(address(deputyGuardian)); - deputyGuardianModule.blacklistDisputeGame(optimismPortal2, game); - assertTrue(optimismPortal2.disputeGameBlacklist(game)); + deputyGuardianModule.blacklistDisputeGame(anchorStateRegistry, game); + assertTrue(anchorStateRegistry.disputeGameBlacklist(game)); } } @@ -214,25 +187,28 @@ contract DeputyGuardianModule_BlacklistDisputeGame_TestFail is DeputyGuardianMod /// @dev Tests that `blacklistDisputeGame` reverts when called by a non deputy guardian. function test_blacklistDisputeGame_notDeputyGuardian_reverts() external { IDisputeGame game = IDisputeGame(makeAddr("game")); - vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector)); - deputyGuardianModule.blacklistDisputeGame(optimismPortal2, game); - assertFalse(optimismPortal2.disputeGameBlacklist(game)); + vm.expectRevert(abi.encodeWithSelector(IDeputyGuardianModule.DeputyGuardianModule_Unauthorized.selector)); + deputyGuardianModule.blacklistDisputeGame(anchorStateRegistry, game); + assertFalse(anchorStateRegistry.disputeGameBlacklist(game)); } /// @dev Tests that when the call from the Safe reverts, the error message is returned. function test_blacklistDisputeGame_targetReverts_reverts() external { vm.mockCallRevert( - address(optimismPortal2), - abi.encodePacked(optimismPortal2.blacklistDisputeGame.selector), - "OptimismPortal2: blacklistDisputeGame reverted" + address(anchorStateRegistry), + abi.encodePacked(anchorStateRegistry.blacklistDisputeGame.selector), + "AnchorStateRegistry: blacklistDisputeGame reverted" ); IDisputeGame game = IDisputeGame(makeAddr("game")); vm.prank(address(deputyGuardian)); vm.expectRevert( - abi.encodeWithSelector(ExecutionFailed.selector, "OptimismPortal2: blacklistDisputeGame reverted") + abi.encodeWithSelector( + IDeputyGuardianModule.DeputyGuardianModule_ExecutionFailed.selector, + "AnchorStateRegistry: blacklistDisputeGame reverted" + ) ); - deputyGuardianModule.blacklistDisputeGame(optimismPortal2, game); + deputyGuardianModule.blacklistDisputeGame(anchorStateRegistry, game); } } @@ -240,11 +216,6 @@ contract DeputyGuardianModule_setRespectedGameType_Test is DeputyGuardianModule_ /// @dev Tests that `setRespectedGameType` successfully updates the respected game type when called by the deputy /// guardian. function testFuzz_setRespectedGameType_succeeds(GameType _gameType) external { - // Game type(uint32).max is reserved for setting the respectedGameTypeUpdatedAt timestamp. - // TODO(#14638): Remove this once we've removed the hack. - uint32 boundedGameType = uint32(bound(_gameType.raw(), 0, type(uint32).max - 1)); - _gameType = GameType.wrap(boundedGameType); - vm.expectEmit(address(safeInstance.safe)); emit ExecutionFromModuleSuccess(address(deputyGuardianModule)); @@ -252,9 +223,8 @@ contract DeputyGuardianModule_setRespectedGameType_Test is DeputyGuardianModule_ emit RespectedGameTypeSet(_gameType, Timestamp.wrap(uint64(block.timestamp))); vm.prank(address(deputyGuardian)); - deputyGuardianModule.setRespectedGameType(optimismPortal2, _gameType); - assertEq(GameType.unwrap(optimismPortal2.respectedGameType()), GameType.unwrap(_gameType)); - assertEq(optimismPortal2.respectedGameTypeUpdatedAt(), uint64(block.timestamp)); + deputyGuardianModule.setRespectedGameType(anchorStateRegistry, _gameType); + assertEq(GameType.unwrap(anchorStateRegistry.respectedGameType()), GameType.unwrap(_gameType)); } } @@ -262,31 +232,81 @@ contract DeputyGuardianModule_setRespectedGameType_TestFail is DeputyGuardianMod /// @dev Tests that `setRespectedGameType` when called by a non deputy guardian. function testFuzz_setRespectedGameType_notDeputyGuardian_reverts(GameType _gameType) external { // Change the game type if it's the same to avoid test rejections. - if (GameType.unwrap(optimismPortal2.respectedGameType()) == GameType.unwrap(_gameType)) { + if (GameType.unwrap(anchorStateRegistry.respectedGameType()) == GameType.unwrap(_gameType)) { unchecked { _gameType = GameType.wrap(GameType.unwrap(_gameType) + 1); } } - vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector)); - deputyGuardianModule.setRespectedGameType(optimismPortal2, _gameType); - assertNotEq(GameType.unwrap(optimismPortal2.respectedGameType()), GameType.unwrap(_gameType)); + vm.expectRevert(abi.encodeWithSelector(IDeputyGuardianModule.DeputyGuardianModule_Unauthorized.selector)); + deputyGuardianModule.setRespectedGameType(anchorStateRegistry, _gameType); + assertNotEq(GameType.unwrap(anchorStateRegistry.respectedGameType()), GameType.unwrap(_gameType)); } /// @dev Tests that when the call from the Safe reverts, the error message is returned. function test_setRespectedGameType_targetReverts_reverts() external { vm.mockCallRevert( - address(optimismPortal2), - abi.encodePacked(optimismPortal2.setRespectedGameType.selector), - "OptimismPortal2: setRespectedGameType reverted" + address(anchorStateRegistry), + abi.encodePacked(anchorStateRegistry.setRespectedGameType.selector), + "AnchorStateRegistry: setRespectedGameType reverted" ); GameType gameType = GameType.wrap(1); vm.prank(address(deputyGuardian)); vm.expectRevert( - abi.encodeWithSelector(ExecutionFailed.selector, "OptimismPortal2: setRespectedGameType reverted") + abi.encodeWithSelector( + IDeputyGuardianModule.DeputyGuardianModule_ExecutionFailed.selector, + "AnchorStateRegistry: setRespectedGameType reverted" + ) ); - deputyGuardianModule.setRespectedGameType(optimismPortal2, gameType); + deputyGuardianModule.setRespectedGameType(anchorStateRegistry, gameType); + } +} + +contract DeputyGuardianModule_updateRetirementTimestamp_Test is DeputyGuardianModule_TestInit { + /// @notice Tests that updateRetirementTimestamp() successfully updates the retirement timestamp + /// when called by the deputy guardian. + function test_updateRetirementTimestamp_succeeds() external { + vm.expectEmit(address(safeInstance.safe)); + emit ExecutionFromModuleSuccess(address(deputyGuardianModule)); + + vm.expectEmit(address(deputyGuardianModule)); + emit RetirementTimestampUpdated(Timestamp.wrap(uint64(block.timestamp))); + + vm.prank(address(deputyGuardian)); + deputyGuardianModule.updateRetirementTimestamp(anchorStateRegistry); + assertEq(anchorStateRegistry.retirementTimestamp(), block.timestamp); + } +} + +contract DeputyGuardianModule_updateRetirementTimestamp_TestFail is DeputyGuardianModule_TestInit { + /// @notice Tests that updateRetirementTimestamp() reverts when called by an address other than + /// the deputy guardian. + function testFuzz_updateRetirementTimestamp_notDeputyGuardian_reverts(address _caller) external { + vm.assume(_caller != address(deputyGuardian)); + vm.prank(_caller); + vm.expectRevert(abi.encodeWithSelector(IDeputyGuardianModule.DeputyGuardianModule_Unauthorized.selector)); + deputyGuardianModule.updateRetirementTimestamp(anchorStateRegistry); + } + + /// @notice Tests that when the call from the Safe reverts, the error message is returned. + function test_updateRetirementTimestamp_targetReverts_reverts() external { + // Mock a revert from the ASR. + vm.mockCallRevert( + address(anchorStateRegistry), + abi.encodePacked(anchorStateRegistry.updateRetirementTimestamp.selector), + "AnchorStateRegistry: updateRetirementTimestamp reverted" + ); + + // Call the function and expect a revert. + vm.prank(address(deputyGuardian)); + vm.expectRevert( + abi.encodeWithSelector( + IDeputyGuardianModule.DeputyGuardianModule_ExecutionFailed.selector, + "AnchorStateRegistry: updateRetirementTimestamp reverted" + ) + ); + deputyGuardianModule.updateRetirementTimestamp(anchorStateRegistry); } } @@ -294,18 +314,18 @@ contract DeputyGuardianModule_NoPortalCollisions_Test is DeputyGuardianModule_Te /// @dev tests that no function selectors in the L1 contracts collide with the OptimismPortal2 functions called by /// the DeputyGuardianModule. function test_noPortalCollisions_succeeds() external { - string[] memory excludes = new string[](5); - excludes[0] = "src/dispute/lib/*"; - excludes[1] = "src/L1/OptimismPortal2.sol"; - excludes[2] = "src/L1/OptimismPortalInterop.sol"; - excludes[3] = "interfaces/L1/IOptimismPortal2.sol"; - excludes[4] = "interfaces/L1/IOptimismPortalInterop.sol"; + string[] memory excludes = new string[](3); + uint256 excludeCounter; + excludes[excludeCounter++] = "src/dispute/lib/*"; + excludes[excludeCounter++] = "src/dispute/AnchorStateRegistry.sol"; + excludes[excludeCounter++] = "interfaces/dispute/IAnchorStateRegistry.sol"; + Abi[] memory abis = ForgeArtifacts.getContractFunctionAbis("src/{L1,dispute,universal}", excludes); for (uint256 i; i < abis.length; i++) { for (uint256 j; j < abis[i].entries.length; j++) { bytes4 sel = abis[i].entries[j].sel; - assertNotEq(sel, optimismPortal2.blacklistDisputeGame.selector); - assertNotEq(sel, optimismPortal2.setRespectedGameType.selector); + assertNotEq(sel, anchorStateRegistry.blacklistDisputeGame.selector); + assertNotEq(sel, anchorStateRegistry.setRespectedGameType.selector); } } } diff --git a/packages/contracts-bedrock/test/safe/DeputyPauseModule.t.sol b/packages/contracts-bedrock/test/safe/DeputyPauseModule.t.sol index 25c54a7d3d..ca87a54662 100644 --- a/packages/contracts-bedrock/test/safe/DeputyPauseModule.t.sol +++ b/packages/contracts-bedrock/test/safe/DeputyPauseModule.t.sol @@ -568,7 +568,8 @@ contract DeputyPauseModule_Pause_TestFail is DeputyPauseModule_TestInit { IDeputyPauseModule.DeputyPauseModule_ExecutionFailed.selector, string( abi.encodeWithSelector( - IDeputyGuardianModule.ExecutionFailed.selector, "SuperchainConfig: pause() reverted" + IDeputyGuardianModule.DeputyGuardianModule_ExecutionFailed.selector, + "SuperchainConfig: pause() reverted" ) ) ) diff --git a/packages/contracts-bedrock/test/setup/Events.sol b/packages/contracts-bedrock/test/setup/Events.sol index 7056f0cbdd..64c63e65e6 100644 --- a/packages/contracts-bedrock/test/setup/Events.sol +++ b/packages/contracts-bedrock/test/setup/Events.sol @@ -1,10 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; +// Libraries +import { Types } from "src/libraries/Types.sol"; import "src/dispute/lib/Types.sol"; -import { Types } from "src/libraries/Types.sol"; +// Interfaces +import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; /// @title Events /// @dev Contains various events that are tested against. This contract needs to @@ -106,4 +108,10 @@ contract Events { event Unpaused(); event BalanceChanged(address account, uint256 balance); + + event ETHMigrated(address indexed lockbox, uint256 ethBalance); + + event PortalMigrated( + address oldLockbox, address newLockbox, address oldAnchorStateRegistry, address newAnchorStateRegistry + ); } diff --git a/packages/contracts-bedrock/test/setup/FFIInterface.sol b/packages/contracts-bedrock/test/setup/FFIInterface.sol index c1e1612da8..97ed21cbc7 100644 --- a/packages/contracts-bedrock/test/setup/FFIInterface.sol +++ b/packages/contracts-bedrock/test/setup/FFIInterface.sol @@ -188,6 +188,28 @@ contract FFIInterface { return abi.decode(result, (bytes)); } + function encodeSuperRootProof(Types.SuperRootProof calldata proof) external returns (bytes memory) { + string[] memory cmds = new string[](4); + cmds[0] = "scripts/go-ffi/go-ffi"; + cmds[1] = "diff"; + cmds[2] = "encodeSuperRootProof"; + cmds[3] = vm.toString(abi.encode(proof)); + + bytes memory result = Process.run(cmds); + return abi.decode(result, (bytes)); + } + + function hashSuperRootProof(Types.SuperRootProof calldata proof) external returns (bytes32) { + string[] memory cmds = new string[](4); + cmds[0] = "scripts/go-ffi/go-ffi"; + cmds[1] = "diff"; + cmds[2] = "hashSuperRootProof"; + cmds[3] = vm.toString(abi.encode(proof)); + + bytes memory result = Process.run(cmds); + return abi.decode(result, (bytes32)); + } + function decodeVersionedNonce(uint256 nonce) external returns (uint256, uint256) { string[] memory cmds = new string[](4); cmds[0] = "scripts/go-ffi/go-ffi"; diff --git a/packages/contracts-bedrock/test/setup/ForkLive.s.sol b/packages/contracts-bedrock/test/setup/ForkLive.s.sol index 3de0214c8e..40e9c94e33 100644 --- a/packages/contracts-bedrock/test/setup/ForkLive.s.sol +++ b/packages/contracts-bedrock/test/setup/ForkLive.s.sol @@ -14,6 +14,7 @@ import { Deploy } from "scripts/deploy/Deploy.s.sol"; // Libraries import { GameTypes, Claim } from "src/dispute/lib/Types.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; +import { LibString } from "solady/src/utils/LibString.sol"; // Interfaces import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; @@ -21,9 +22,12 @@ import { IPermissionedDisputeGame } from "interfaces/dispute/IPermissionedDisput import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { IAddressManager } from "interfaces/legacy/IAddressManager.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; +import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; +import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; /// @title ForkLive /// @notice This script is called by Setup.sol as a preparation step for the foundry test suite, and is run as an @@ -34,8 +38,10 @@ import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.so /// Therefore this script can only be run against a fork of a production network which is listed in the /// superchain-registry. /// This contract must not have constructor logic because it is set into state using `etch`. + contract ForkLive is Deployer { using stdToml for string; + using LibString for string; bool public useOpsRepo; @@ -96,16 +102,24 @@ contract ForkLive is Deployer { /// using either the `saveProxyAndImpl` or `artifacts.save()` functions. function _readSuperchainRegistry() internal { string memory superchainBasePath = "./lib/superchain-registry/superchain/configs/"; + string memory validationBasePath = "./lib/superchain-registry/validation/standard/"; string memory superchainToml = vm.readFile(string.concat(superchainBasePath, baseChain(), "/superchain.toml")); string memory opToml = vm.readFile(string.concat(superchainBasePath, baseChain(), "/", opChain(), ".toml")); + string memory standardVersionsToml = + vm.readFile(string.concat(validationBasePath, "standard-versions-", baseChain(), ".toml")); + + standardVersionsToml = standardVersionsToml.replace('"op-contracts/v2.0.0-rc.1"', "RELEASE"); + // Slightly hacky, we encode the uint chainId as an address to save it in Artifacts artifacts.save("L2ChainId", address(uint160(vm.parseTomlUint(opToml, ".chain_id")))); // Superchain shared contracts saveProxyAndImpl("SuperchainConfig", superchainToml, ".superchain_config_addr"); saveProxyAndImpl("ProtocolVersions", superchainToml, ".protocol_versions_addr"); - artifacts.save("OPContractsManager", vm.parseTomlAddress(superchainToml, ".op_contracts_manager_proxy_addr")); + artifacts.save( + "OPContractsManager", vm.parseTomlAddress(standardVersionsToml, "$.RELEASE.op_contracts_manager.address") + ); // Core contracts artifacts.save("ProxyAdmin", vm.parseTomlAddress(opToml, ".addresses.ProxyAdmin")); @@ -116,6 +130,15 @@ contract ForkLive is Deployer { artifacts.save("OptimismPortalProxy", optimismPortal); artifacts.save("OptimismPortal2Impl", EIP1967Helper.getImplementation(optimismPortal)); + // Get the lockbox address from the portal, and save it + /// NOTE: Using try catch because this function could be called before or after the upgrade. + try IOptimismPortal2(payable(optimismPortal)).ethLockbox() returns (IETHLockbox ethLockbox_) { + console.log("ForkLive: ETHLockboxProxy found: %s", address(ethLockbox_)); + artifacts.save("ETHLockboxProxy", address(ethLockbox_)); + } catch { + console.log("ForkLive: ETHLockboxProxy not found"); + } + address addressManager = vm.parseTomlAddress(opToml, ".addresses.AddressManager"); artifacts.save("AddressManager", addressManager); artifacts.save( @@ -180,14 +203,46 @@ contract ForkLive is Deployer { absolutePrestate: Claim.wrap(bytes32(keccak256("absolutePrestate"))) }); + IOPContractsManager.OpChainConfig[] memory opmChain = new IOPContractsManager.OpChainConfig[](0); // Temporarily replace the upgrader with a DelegateCaller so we can test the upgrade, // then reset its code to the original code. + bytes memory upgraderCode = address(upgrader).code; vm.etch(upgrader, vm.getDeployedCode("test/mocks/Callers.sol:DelegateCaller")); + + // The 2.0.0 OPCM requires that the SuperchainConfig and ProtocolVersions contracts + // have been upgraded before it will upgrade other contracts. + // Those contracts can only be upgrade by the OP Mainnet ProxyAdminOwner. So for chains which have a different + // ProxyAdminOwner, we first need to call opcm.upgrade from the OP Mainnet PAO. + address mainnetPAO = artifacts.mustGetAddress("SuperchainConfigProxy"); + + if (upgrader != mainnetPAO) { + ISuperchainConfig superchainConfig = ISuperchainConfig(mainnetPAO); + + address opmUpgrader = IProxyAdmin(EIP1967Helper.getAdmin(address(superchainConfig))).owner(); + vm.etch(opmUpgrader, vm.getDeployedCode("test/mocks/Callers.sol:DelegateCaller")); + + DelegateCaller(opmUpgrader).dcForward( + address(0x026b2F158255Beac46c1E7c6b8BbF29A4b6A7B76), + abi.encodeCall(IOPContractsManager.upgrade, (opmChain)) + ); + } + + // Start by doing Upgrade 13. + DelegateCaller(upgrader).dcForward( address(0x026b2F158255Beac46c1E7c6b8BbF29A4b6A7B76), abi.encodeCall(IOPContractsManager.upgrade, (opChains)) ); + + // Then do Upgrade 14. + DelegateCaller(upgrader).dcForward( + address(0x3A1f523a4bc09cd344A2745a108Bb0398288094F), abi.encodeCall(IOPContractsManager.upgrade, (opChains)) + ); + + // Then do the final upgrade. DelegateCaller(upgrader).dcForward(address(opcm), abi.encodeCall(IOPContractsManager.upgrade, (opChains))); + + // Reset the upgrader to the original code. vm.etch(upgrader, upgraderCode); console.log("ForkLive: Saving newly deployed contracts"); @@ -205,6 +260,11 @@ contract ForkLive is Deployer { IAnchorStateRegistry newAnchorStateRegistry = IPermissionedDisputeGame(permissionedDisputeGame).anchorStateRegistry(); artifacts.save("AnchorStateRegistryProxy", address(newAnchorStateRegistry)); + + // Get the lockbox address from the portal, and save it + IOptimismPortal2 portal = IOptimismPortal2(artifacts.mustGetAddress("OptimismPortalProxy")); + address lockboxAddress = address(portal.ethLockbox()); + artifacts.save("ETHLockboxProxy", lockboxAddress); } /// @notice Saves the proxy and implementation addresses for a contract name diff --git a/packages/contracts-bedrock/test/setup/Setup.sol b/packages/contracts-bedrock/test/setup/Setup.sol index 62c7118ac7..f014acf8ed 100644 --- a/packages/contracts-bedrock/test/setup/Setup.sol +++ b/packages/contracts-bedrock/test/setup/Setup.sol @@ -22,7 +22,8 @@ import { Chains } from "scripts/libraries/Chains.sol"; // Interfaces import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; +import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPortal2.sol"; +import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; @@ -96,7 +97,8 @@ contract Setup { IDelayedWETH delayedWETHPermissionedGameProxy; // L1 contracts - core - IOptimismPortal2 optimismPortal2; + IOptimismPortal optimismPortal2; + IETHLockbox ethLockbox; ISystemConfig systemConfig; IL1StandardBridge l1StandardBridge; IL1CrossDomainMessenger l1CrossDomainMessenger; @@ -138,6 +140,12 @@ contract Setup { return vm.envOr("FORK_TEST", false); } + /// @notice Indicates whether a test is running against a forked network that is OP. + function isOpFork() public view returns (bool) { + string memory opChain = vm.envOr("FORK_OP_CHAIN", string("op")); + return keccak256(bytes(opChain)) == keccak256(bytes("op")); + } + /// @dev Deploys either the Deploy.s.sol or Fork.s.sol contract, by fetching the bytecode dynamically using /// `vm.getDeployedCode()` and etching it into the state. /// This enables us to avoid including the bytecode of those contracts in the bytecode of this contract. @@ -193,6 +201,14 @@ contract Setup { } } + /// @dev Skips tests when running against a forked production network that is not OP. + function skipIfNotOpFork(string memory message) public { + if (isForkTest() && !isOpFork()) { + vm.skip(true); + console.log(string.concat("Skipping non-OP fork test: ", message)); + } + } + /// @dev Skips tests when running against a forked production network using the superchain ops repo. function skipIfOpsRepoTest(string memory message) public { if (forkLive.useOpsRepo()) { @@ -229,7 +245,14 @@ contract Setup { console.log("Setup: completed L1 deployment, registering addresses now"); - optimismPortal2 = IOptimismPortal2(artifacts.mustGetAddress("OptimismPortalProxy")); + optimismPortal2 = IOptimismPortal(artifacts.mustGetAddress("OptimismPortalProxy")); + + // Only skip ETHLockbox assignment if we're in a fork test with non-upgraded fork + // TODO(#14691): Remove this check once Upgrade 15 is deployed on Mainnet. + if (!isForkTest() || deploy.cfg().useUpgradedFork()) { + ethLockbox = IETHLockbox(artifacts.mustGetAddress("ETHLockboxProxy")); + } + systemConfig = ISystemConfig(artifacts.mustGetAddress("SystemConfigProxy")); l1StandardBridge = IL1StandardBridge(artifacts.mustGetAddress("L1StandardBridgeProxy")); l1CrossDomainMessenger = IL1CrossDomainMessenger(artifacts.mustGetAddress("L1CrossDomainMessengerProxy")); diff --git a/packages/contracts-bedrock/test/universal/BenchmarkTest.t.sol b/packages/contracts-bedrock/test/universal/BenchmarkTest.t.sol index 0c82feda3b..d78aeb37ae 100644 --- a/packages/contracts-bedrock/test/universal/BenchmarkTest.t.sol +++ b/packages/contracts-bedrock/test/universal/BenchmarkTest.t.sol @@ -10,10 +10,8 @@ import { CommonTest } from "test/setup/CommonTest.sol"; // Libraries import { SafeCall } from "src/libraries/SafeCall.sol"; import { Encoding } from "src/libraries/Encoding.sol"; -import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; // Interfaces -import { IL1BlockInterop } from "interfaces/L2/IL1BlockInterop.sol"; // Free function for setting the prevBaseFee param in the OptimismPortal. function setPrevBaseFee(Vm _vm, address _op, uint128 _prevBaseFee) { @@ -88,106 +86,3 @@ contract GasBenchMark_L1Block_SetValuesEcotone_Warm is GasBenchMark_L1Block { assertLt(vm.lastCallGas().gasTotalUsed, 200_000); } } - -contract GasBenchMark_L1BlockInterop is GasBenchMark_L1Block { - IL1BlockInterop l1BlockInterop; - - function setUp() public virtual override { - super.setUp(); - - // Create the L1BlockInterop contract. - l1BlockInterop = IL1BlockInterop( - DeployUtils.create1({ - _name: "L1BlockInterop", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IL1BlockInterop.__constructor__, ())) - }) - ); - - // Set up the calldata for setting the values. - setValuesCalldata = Encoding.encodeSetL1BlockValuesInterop( - type(uint32).max, - type(uint32).max, - type(uint64).max, - type(uint64).max, - type(uint64).max, - type(uint256).max, - type(uint256).max, - keccak256(abi.encode(1)), - bytes32(type(uint256).max) - ); - } -} - -contract GasBenchMark_L1BlockInterop_SetValuesInterop is GasBenchMark_L1BlockInterop { - function test_setL1BlockValuesInterop_benchmark() external { - // Skip if the test is running in coverage. - skipIfCoverage(); - - // Test - SafeCall.call({ _target: address(l1BlockInterop), _calldata: setValuesCalldata }); - - // Assert - // setL1BlockValuesInterop system tx ONLY gets 1m gas. - // 200k is a safe boundary to prevent hitting the limit. - assertLt(vm.lastCallGas().gasTotalUsed, 200_000); - } -} - -contract GasBenchMark_L1BlockInterop_SetValuesInterop_Warm is GasBenchMark_L1BlockInterop { - function test_setL1BlockValuesInterop_benchmark() external { - // Skip if the test is running in coverage. - skipIfCoverage(); - - // Setup - // Trigger so storage is warm. - SafeCall.call({ _target: address(l1BlockInterop), _calldata: setValuesCalldata }); - - // Test - SafeCall.call({ _target: address(l1BlockInterop), _calldata: setValuesCalldata }); - - // Assert - // setL1BlockValuesInterop system tx ONLY gets 1m gas. - // 200k is a safe boundary to prevent hitting the limit. - assertLt(vm.lastCallGas().gasTotalUsed, 200_000); - } -} - -contract GasBenchMark_L1BlockInterop_DepositsComplete is GasBenchMark_L1BlockInterop { - function test_depositsComplete_benchmark() external { - // Skip if the test is running in coverage. - skipIfCoverage(); - - // Test - SafeCall.call({ - _target: address(l1BlockInterop), - _calldata: abi.encodeCall(IL1BlockInterop.depositsComplete, ()) - }); - - // Assert - // depositsComplete system tx ONLY gets 15k gas. - // 5_000 is a safe boundary to prevent hitting the limit. - assertLt(vm.lastCallGas().gasTotalUsed, 5_000); - } -} - -contract GasBenchMark_L1BlockInterop_DepositsComplete_Warm is GasBenchMark_L1BlockInterop { - function test_depositsComplete_benchmark() external { - // Skip if the test is running in coverage. - skipIfCoverage(); - - // Setup - // Trigger so storage is warm. - SafeCall.call({ _target: address(l1BlockInterop), _calldata: setValuesCalldata }); - - // Test - SafeCall.call({ - _target: address(l1BlockInterop), - _calldata: abi.encodeCall(l1BlockInterop.depositsComplete, ()) - }); - - // Assert - // depositsComplete system tx ONLY gets 15k gas. - // 5_000 is a safe boundary to prevent hitting the limit. - assertLt(vm.lastCallGas().gasTotalUsed, 5_000); - } -} diff --git a/packages/contracts-bedrock/test/universal/ReinitializableBase.t.sol b/packages/contracts-bedrock/test/universal/ReinitializableBase.t.sol new file mode 100644 index 0000000000..1c3f7d93eb --- /dev/null +++ b/packages/contracts-bedrock/test/universal/ReinitializableBase.t.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing +import { Test } from "forge-std/Test.sol"; + +// Contracts +import { ReinitializableBase } from "src/universal/ReinitializableBase.sol"; + +contract ReinitializableBase_Test is Test { + /// @notice Tests that the contract is created correctly and initVersion returns the right + /// value when the provided init version is non-zero. + /// @param _initVersion The init version to use when creating the contract. + function testFuzz_initVersion_validVersion_succeeds(uint8 _initVersion) public { + // Zero version not allowed. + _initVersion = uint8(bound(_initVersion, 1, type(uint8).max)); + + // Deploy the reinitializable contract. + ReinitializableBase_Harness harness = new ReinitializableBase_Harness(_initVersion); + + // Check the init version. + assertEq(harness.initVersion(), _initVersion); + } +} + +contract ReinitializableBase_TestFail is Test { + /// @notice Tests that the contract creation reverts when the init version is zero. + function test_initVersion_zeroVersion_reverts() public { + vm.expectRevert(ReinitializableBase.ReinitializableBase_ZeroInitVersion.selector); + new ReinitializableBase_Harness(0); + } +} + +contract ReinitializableBase_Harness is ReinitializableBase { + constructor(uint8 _initVersion) ReinitializableBase(_initVersion) { } +} diff --git a/packages/contracts-bedrock/test/universal/Specs.t.sol b/packages/contracts-bedrock/test/universal/Specs.t.sol index abd2ebfc8d..421c4a4f07 100644 --- a/packages/contracts-bedrock/test/universal/Specs.t.sol +++ b/packages/contracts-bedrock/test/universal/Specs.t.sol @@ -10,10 +10,8 @@ import { ForgeArtifacts, Abi, AbiEntry } from "scripts/libraries/ForgeArtifacts. // Interfaces import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; -import { IOptimismPortalInterop } from "interfaces/L1/IOptimismPortalInterop.sol"; +import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPortal2.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; -import { ISystemConfigInterop } from "interfaces/L1/ISystemConfigInterop.sol"; import { IDataAvailabilityChallenge } from "interfaces/L1/IDataAvailabilityChallenge.sol"; import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; @@ -40,8 +38,7 @@ contract Specification_Test is CommonTest { DISPUTEGAMEFACTORYOWNER, DELAYEDWETHOWNER, COUNCILSAFE, - COUNCILSAFEOWNER, - DEPENDENCYMANAGER + COUNCILSAFEOWNER } /// @notice Represents the specification of a function. @@ -208,87 +205,43 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "L1StandardBridge", _sel: _getSel("superchainConfig()") }); _addSpec({ _name: "L1StandardBridge", _sel: _getSel("version()") }); - // OptimismPortalInterop - _addSpec({ - _name: "OptimismPortalInterop", - _sel: _getSel("depositTransaction(address,uint256,uint64,bool,bytes)") - }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("donateETH()") }); - _addSpec({ - _name: "OptimismPortalInterop", - _sel: IOptimismPortal2.finalizeWithdrawalTransaction.selector, - _pausable: true - }); - _addSpec({ - _name: "OptimismPortalInterop", - _sel: IOptimismPortal2.finalizeWithdrawalTransactionExternalProof.selector, - _pausable: true - }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("finalizedWithdrawals(bytes32)") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("guardian()") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("initialize(address,address,address,uint32)") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("l2Sender()") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("minimumGasLimit(uint64)") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("params()") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("paused()") }); - _addSpec({ - _name: "OptimismPortalInterop", - _sel: IOptimismPortal2.proveWithdrawalTransaction.selector, - _pausable: true - }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("provenWithdrawals(bytes32,address)") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("superchainConfig()") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("systemConfig()") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("version()") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("disputeGameFactory()") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("disputeGameBlacklist(address)") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("respectedGameType()") }); - // Comment out the auth to not disturb the testDeputyGuardianAuth test. This code is not meant to run in - // production, - // and will be merged into the OptimismPortal2 contract itself in the future. - _addSpec({ - _name: "OptimismPortalInterop", - _sel: _getSel("blacklistDisputeGame(address)") /*, _auth: Role.GUARDIAN*/ - }); - _addSpec({ - _name: "OptimismPortalInterop", - _sel: _getSel("setRespectedGameType(uint32)") /*, _auth: Role.GUARDIAN*/ - }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("checkWithdrawal(bytes32,address)") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("proofMaturityDelaySeconds()") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("disputeGameFinalityDelaySeconds()") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("respectedGameTypeUpdatedAt()") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("proofSubmitters(bytes32,uint256)") }); - _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("numProofSubmitters(bytes32)") }); - _addSpec({ - _name: "OptimismPortalInterop", - _sel: IOptimismPortalInterop.setConfig.selector, - _auth: Role.SYSTEMCONFIGOWNER - }); - // OptimismPortal2 + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("anchorStateRegistry()") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("checkWithdrawal(bytes32,address)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("depositTransaction(address,uint256,uint64,bool,bytes)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("donateETH()") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("migrateToSuperRoots(address,address)") }); _addSpec({ _name: "OptimismPortal2", - _sel: IOptimismPortal2.finalizeWithdrawalTransaction.selector, + _sel: IOptimismPortal.finalizeWithdrawalTransaction.selector, _pausable: true }); _addSpec({ _name: "OptimismPortal2", - _sel: IOptimismPortal2.finalizeWithdrawalTransactionExternalProof.selector, + _sel: IOptimismPortal.finalizeWithdrawalTransactionExternalProof.selector, _pausable: true }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("finalizedWithdrawals(bytes32)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("guardian()") }); - _addSpec({ _name: "OptimismPortal2", _sel: _getSel("initialize(address,address,address,uint32)") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("initialize(address,address,address,address)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("l2Sender()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("minimumGasLimit(uint64)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("params()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("paused()") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("initVersion()") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("superRootsActive()") }); _addSpec({ _name: "OptimismPortal2", - _sel: IOptimismPortal2.proveWithdrawalTransaction.selector, + _sel: _getSel( + "proveWithdrawalTransaction((uint256,address,address,uint256,uint256,bytes),uint256,(bytes32,bytes32,bytes32,bytes32),bytes[])" + ), + _pausable: true + }); + _addSpec({ + _name: "OptimismPortal2", + _sel: _getSel( + "proveWithdrawalTransaction((uint256,address,address,uint256,uint256,bytes),address,uint256,(bytes1,uint64,(uint256,bytes32)[]),(bytes32,bytes32,bytes32,bytes32),bytes[])" + ), _pausable: true }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("provenWithdrawals(bytes32,address)") }); @@ -296,16 +249,19 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "OptimismPortal2", _sel: _getSel("systemConfig()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("version()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("disputeGameFactory()") }); - _addSpec({ _name: "OptimismPortal2", _sel: _getSel("disputeGameBlacklist(address)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("respectedGameType()") }); - _addSpec({ _name: "OptimismPortal2", _sel: _getSel("blacklistDisputeGame(address)"), _auth: Role.GUARDIAN }); - _addSpec({ _name: "OptimismPortal2", _sel: _getSel("setRespectedGameType(uint32)"), _auth: Role.GUARDIAN }); - _addSpec({ _name: "OptimismPortal2", _sel: _getSel("checkWithdrawal(bytes32,address)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("proofMaturityDelaySeconds()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("disputeGameFinalityDelaySeconds()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("respectedGameTypeUpdatedAt()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("proofSubmitters(bytes32,uint256)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("numProofSubmitters(bytes32)") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("upgrade(address,address)") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("ethLockbox()") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("migrateLiquidity()") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("proxyAdminOwner()") }); + + // ProxyAdminOwnedBase + _addSpec({ _name: "ProxyAdminOwnedBase", _sel: _getSel("proxyAdminOwner()") }); // ProtocolVersions _addSpec({ _name: "ProtocolVersions", _sel: _getSel("RECOMMENDED_SLOT()") }); @@ -329,6 +285,21 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "ProtocolVersions", _sel: _getSel("transferOwnership(address)") }); _addSpec({ _name: "ProtocolVersions", _sel: _getSel("version()") }); + // ETHLockbox + _addSpec({ _name: "ETHLockbox", _sel: _getSel("version()") }); + _addSpec({ _name: "ETHLockbox", _sel: _getSel("initialize(address,address[])") }); + _addSpec({ _name: "ETHLockbox", _sel: _getSel("superchainConfig()") }); + _addSpec({ _name: "ETHLockbox", _sel: _getSel("paused()") }); + _addSpec({ _name: "ETHLockbox", _sel: _getSel("authorizedPortals(address)") }); + _addSpec({ _name: "ETHLockbox", _sel: _getSel("authorizedLockboxes(address)") }); + _addSpec({ _name: "ETHLockbox", _sel: _getSel("receiveLiquidity()") }); + _addSpec({ _name: "ETHLockbox", _sel: _getSel("lockETH()") }); + _addSpec({ _name: "ETHLockbox", _sel: _getSel("unlockETH(uint256)") }); + _addSpec({ _name: "ETHLockbox", _sel: _getSel("authorizePortal(address)") }); + _addSpec({ _name: "ETHLockbox", _sel: _getSel("authorizeLockbox(address)") }); + _addSpec({ _name: "ETHLockbox", _sel: _getSel("migrateLiquidity(address)") }); + _addSpec({ _name: "ETHLockbox", _sel: _getSel("proxyAdminOwner()") }); + // ResourceMetering _addSpec({ _name: "ResourceMetering", _sel: _getSel("params()") }); @@ -389,7 +360,6 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "SystemConfig", _sel: _getSel("OPTIMISM_PORTAL_SLOT()") }); _addSpec({ _name: "SystemConfig", _sel: _getSel("OPTIMISM_MINTABLE_ERC20_FACTORY_SLOT()") }); _addSpec({ _name: "SystemConfig", _sel: _getSel("BATCH_INBOX_SLOT()") }); - _addSpec({ _name: "SystemConfig", _sel: _getSel("DISPUTE_GAME_FACTORY_SLOT()") }); _addSpec({ _name: "SystemConfig", _sel: _getSel("disputeGameFactory()") }); _addSpec({ _name: "SystemConfig", @@ -400,93 +370,9 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "SystemConfig", _sel: _getSel("blobbasefeeScalar()") }); _addSpec({ _name: "SystemConfig", _sel: _getSel("maximumGasLimit()") }); _addSpec({ _name: "SystemConfig", _sel: _getSel("getAddresses()") }); - - // SystemConfigInterop - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("UNSAFE_BLOCK_SIGNER_SLOT()") }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("START_BLOCK_SLOT()") }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("VERSION()") }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("batcherHash()") }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("gasLimit()") }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("eip1559Denominator()") }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("eip1559Elasticity()") }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("operatorFeeScalar()") }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("operatorFeeConstant()") }); - _addSpec({ _name: "SystemConfigInterop", _sel: ISystemConfigInterop.initialize.selector }); - _addSpec({ _name: "SystemConfigInterop", _sel: ISystemConfig.initialize.selector }); - _addSpec({ _name: "SystemConfigInterop", _sel: ISystemConfigInterop.minimumGasLimit.selector }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("overhead()") }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("owner()") }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("renounceOwnership()"), _auth: Role.SYSTEMCONFIGOWNER }); - _addSpec({ _name: "SystemConfigInterop", _sel: ISystemConfigInterop.resourceConfig.selector }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("scalar()") }); - _addSpec({ - _name: "SystemConfigInterop", - _sel: ISystemConfigInterop.setBatcherHash.selector, - _auth: Role.SYSTEMCONFIGOWNER - }); - _addSpec({ - _name: "SystemConfigInterop", - _sel: ISystemConfigInterop.setGasConfig.selector, - _auth: Role.SYSTEMCONFIGOWNER - }); - _addSpec({ - _name: "SystemConfigInterop", - _sel: ISystemConfigInterop.setGasLimit.selector, - _auth: Role.SYSTEMCONFIGOWNER - }); - _addSpec({ - _name: "SystemConfigInterop", - _sel: ISystemConfigInterop.setEIP1559Params.selector, - _auth: Role.SYSTEMCONFIGOWNER - }); - _addSpec({ - _name: "SystemConfigInterop", - _sel: ISystemConfigInterop.setOperatorFeeScalars.selector, - _auth: Role.SYSTEMCONFIGOWNER - }); - _addSpec({ - _name: "SystemConfigInterop", - _sel: ISystemConfigInterop.setUnsafeBlockSigner.selector, - _auth: Role.SYSTEMCONFIGOWNER - }); - _addSpec({ - _name: "SystemConfigInterop", - _sel: _getSel("transferOwnership(address)"), - _auth: Role.SYSTEMCONFIGOWNER - }); - _addSpec({ _name: "SystemConfigInterop", _sel: ISystemConfigInterop.unsafeBlockSigner.selector }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("version()") }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("l1CrossDomainMessenger()") }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("l1ERC721Bridge()") }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("l1StandardBridge()") }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("optimismPortal()") }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("optimismMintableERC20Factory()") }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("batchInbox()") }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("startBlock()") }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("L1_CROSS_DOMAIN_MESSENGER_SLOT()") }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("L1_ERC_721_BRIDGE_SLOT()") }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("L1_STANDARD_BRIDGE_SLOT()") }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("OPTIMISM_PORTAL_SLOT()") }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("OPTIMISM_MINTABLE_ERC20_FACTORY_SLOT()") }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("BATCH_INBOX_SLOT()") }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("DISPUTE_GAME_FACTORY_SLOT()") }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("disputeGameFactory()") }); - _addSpec({ - _name: "SystemConfigInterop", - _sel: _getSel("setGasConfigEcotone(uint32,uint32)"), - _auth: Role.SYSTEMCONFIGOWNER - }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("basefeeScalar()") }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("blobbasefeeScalar()") }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("maximumGasLimit()") }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("addDependency(uint256)"), _auth: Role.DEPENDENCYMANAGER }); - _addSpec({ - _name: "SystemConfigInterop", - _sel: _getSel("removeDependency(uint256)"), - _auth: Role.DEPENDENCYMANAGER - }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("dependencyManager()") }); - _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("getAddresses()") }); + _addSpec({ _name: "SystemConfig", _sel: _getSel("l2ChainId()") }); + _addSpec({ _name: "SystemConfig", _sel: _getSel("upgrade(uint256)") }); + _addSpec({ _name: "SystemConfig", _sel: _getSel("initVersion()") }); // ProxyAdmin _addSpec({ _name: "ProxyAdmin", _sel: _getSel("addressManager()") }); @@ -572,7 +458,7 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("anchors(uint32)") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("getAnchorRoot()") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("disputeGameFactory()") }); - _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("initialize(address,address,address,(bytes32,uint256))") }); + _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("initialize(address,address,(bytes32,uint256),uint32)") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameBlacklisted(address)") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameClaimValid(address)") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameFinalized(address)") }); @@ -581,11 +467,17 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameResolved(address)") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameRespected(address)") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("isGameRetired(address)") }); - _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("portal()") }); + _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("paused()") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("respectedGameType()") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("setAnchorState(address)") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("superchainConfig()") }); _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("version()") }); + _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("disputeGameFinalityDelaySeconds()") }); + _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("disputeGameBlacklist(address)") }); + _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("retirementTimestamp()") }); + _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("blacklistDisputeGame(address)"), _auth: Role.GUARDIAN }); + _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("setRespectedGameType(uint32)"), _auth: Role.GUARDIAN }); + _addSpec({ _name: "AnchorStateRegistry", _sel: _getSel("updateRetirementTimestamp()"), _auth: Role.GUARDIAN }); // PermissionedDisputeGame _addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("absolutePrestate()") }); @@ -627,6 +519,7 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("initialize()") }); _addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("l1Head()") }); _addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("l2BlockNumber()") }); + _addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("l2SequenceNumber()") }); _addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("l2BlockNumberChallenged()") }); _addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("l2BlockNumberChallenger()") }); _addSpec({ _name: "PermissionedDisputeGame", _sel: _getSel("l2ChainId()") }); @@ -691,6 +584,7 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("initialize()") }); _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("l1Head()") }); _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("l2BlockNumber()") }); + _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("l2SequenceNumber()") }); _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("l2BlockNumberChallenged()") }); _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("l2BlockNumberChallenger()") }); _addSpec({ _name: "FaultDisputeGame", _sel: _getSel("l2ChainId()") }); @@ -742,7 +636,7 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "SuperFaultDisputeGame", _sel: _getSel("hasUnlockedCredit(address)") }); _addSpec({ _name: "SuperFaultDisputeGame", _sel: _getSel("initialize()") }); _addSpec({ _name: "SuperFaultDisputeGame", _sel: _getSel("l1Head()") }); - _addSpec({ _name: "SuperFaultDisputeGame", _sel: _getSel("l2BlockNumber()") }); + _addSpec({ _name: "SuperFaultDisputeGame", _sel: _getSel("l2SequenceNumber()") }); _addSpec({ _name: "SuperFaultDisputeGame", _sel: _getSel("maxClockDuration()") }); _addSpec({ _name: "SuperFaultDisputeGame", _sel: _getSel("maxGameDepth()") }); _addSpec({ _name: "SuperFaultDisputeGame", _sel: _getSel("move(bytes32,uint256,bytes32,bool)") }); @@ -756,8 +650,8 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "SuperFaultDisputeGame", _sel: _getSel("resolvedSubgames(uint256)") }); _addSpec({ _name: "SuperFaultDisputeGame", _sel: _getSel("rootClaim()") }); _addSpec({ _name: "SuperFaultDisputeGame", _sel: _getSel("splitDepth()") }); - _addSpec({ _name: "SuperFaultDisputeGame", _sel: _getSel("startingBlockNumber()") }); - _addSpec({ _name: "SuperFaultDisputeGame", _sel: _getSel("startingOutputRoot()") }); + _addSpec({ _name: "SuperFaultDisputeGame", _sel: _getSel("startingSequenceNumber()") }); + _addSpec({ _name: "SuperFaultDisputeGame", _sel: _getSel("startingProposal()") }); _addSpec({ _name: "SuperFaultDisputeGame", _sel: _getSel("startingRootHash()") }); _addSpec({ _name: "SuperFaultDisputeGame", _sel: _getSel("status()") }); _addSpec({ _name: "SuperFaultDisputeGame", _sel: _getSel("step(uint256,bool,bytes,bytes)") }); @@ -801,7 +695,6 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "SuperPermissionedDisputeGame", _sel: _getSel("hasUnlockedCredit(address)") }); _addSpec({ _name: "SuperPermissionedDisputeGame", _sel: _getSel("initialize()") }); _addSpec({ _name: "SuperPermissionedDisputeGame", _sel: _getSel("l1Head()") }); - _addSpec({ _name: "SuperPermissionedDisputeGame", _sel: _getSel("l2BlockNumber()") }); _addSpec({ _name: "SuperPermissionedDisputeGame", _sel: _getSel("maxClockDuration()") }); _addSpec({ _name: "SuperPermissionedDisputeGame", _sel: _getSel("maxGameDepth()") }); _addSpec({ @@ -819,8 +712,9 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "SuperPermissionedDisputeGame", _sel: _getSel("resolvedSubgames(uint256)") }); _addSpec({ _name: "SuperPermissionedDisputeGame", _sel: _getSel("rootClaim()") }); _addSpec({ _name: "SuperPermissionedDisputeGame", _sel: _getSel("splitDepth()") }); - _addSpec({ _name: "SuperPermissionedDisputeGame", _sel: _getSel("startingBlockNumber()") }); - _addSpec({ _name: "SuperPermissionedDisputeGame", _sel: _getSel("startingOutputRoot()") }); + _addSpec({ _name: "SuperPermissionedDisputeGame", _sel: _getSel("l2SequenceNumber()") }); + _addSpec({ _name: "SuperPermissionedDisputeGame", _sel: _getSel("startingSequenceNumber()") }); + _addSpec({ _name: "SuperPermissionedDisputeGame", _sel: _getSel("startingProposal()") }); _addSpec({ _name: "SuperPermissionedDisputeGame", _sel: _getSel("startingRootHash()") }); _addSpec({ _name: "SuperPermissionedDisputeGame", _sel: _getSel("status()") }); _addSpec({ @@ -939,7 +833,7 @@ contract Specification_Test is CommonTest { }); _addSpec({ _name: "DeputyGuardianModule", - _sel: _getSel("setAnchorState(address,address)"), + _sel: _getSel("updateRetirementTimestamp(address)"), _auth: Role.DEPUTYGUARDIAN }); _addSpec({ _name: "DeputyGuardianModule", _sel: _getSel("pause()"), _auth: Role.DEPUTYGUARDIAN }); @@ -1104,13 +998,12 @@ contract Specification_Test is CommonTest { /// @notice Ensures that the DeputyGuardian is authorized to take all Guardian actions. function test_deputyGuardianAuth_works() public view { // Additional 2 roles for the DeputyPauseModule - // Additional role for `setAnchorState` which is in DGM but no longer role-restricted. - assertEq(specsByRole[Role.GUARDIAN].length, 4); - assertEq(specsByRole[Role.DEPUTYGUARDIAN].length, specsByRole[Role.GUARDIAN].length + 3); + assertEq(specsByRole[Role.GUARDIAN].length, 5); + assertEq(specsByRole[Role.DEPUTYGUARDIAN].length, specsByRole[Role.GUARDIAN].length + 2); mapping(bytes4 => Spec) storage dgmFuncSpecs = specs["DeputyGuardianModule"]; mapping(bytes4 => Spec) storage superchainConfigFuncSpecs = specs["SuperchainConfig"]; - mapping(bytes4 => Spec) storage portal2FuncSpecs = specs["OptimismPortal2"]; + mapping(bytes4 => Spec) storage asrSpecs = specs["AnchorStateRegistry"]; // Ensure that for each of the DeputyGuardianModule's methods there is a corresponding method on another // system contract authed to the Guardian role. @@ -1121,11 +1014,12 @@ contract Specification_Test is CommonTest { _assertRolesEq(superchainConfigFuncSpecs[_getSel("unpause()")].auth, Role.GUARDIAN); _assertRolesEq(dgmFuncSpecs[_getSel("blacklistDisputeGame(address,address)")].auth, Role.DEPUTYGUARDIAN); - _assertRolesEq(portal2FuncSpecs[_getSel("blacklistDisputeGame(address)")].auth, Role.GUARDIAN); + _assertRolesEq(asrSpecs[_getSel("blacklistDisputeGame(address)")].auth, Role.GUARDIAN); _assertRolesEq(dgmFuncSpecs[_getSel("setRespectedGameType(address,uint32)")].auth, Role.DEPUTYGUARDIAN); - _assertRolesEq(portal2FuncSpecs[_getSel("setRespectedGameType(uint32)")].auth, Role.GUARDIAN); + _assertRolesEq(asrSpecs[_getSel("setRespectedGameType(uint32)")].auth, Role.GUARDIAN); - _assertRolesEq(dgmFuncSpecs[_getSel("setAnchorState(address,address)")].auth, Role.DEPUTYGUARDIAN); + _assertRolesEq(dgmFuncSpecs[_getSel("updateRetirementTimestamp(address)")].auth, Role.DEPUTYGUARDIAN); + _assertRolesEq(asrSpecs[_getSel("updateRetirementTimestamp()")].auth, Role.GUARDIAN); } } diff --git a/packages/contracts-bedrock/test/vendor/Initializable.t.sol b/packages/contracts-bedrock/test/vendor/Initializable.t.sol index 9835e4e73a..22d919c3c2 100644 --- a/packages/contracts-bedrock/test/vendor/Initializable.t.sol +++ b/packages/contracts-bedrock/test/vendor/Initializable.t.sol @@ -10,7 +10,7 @@ import { Process } from "scripts/libraries/Process.sol"; // Libraries import { LibString } from "@solady/utils/LibString.sol"; -import { GameType, Hash, OutputRoot } from "src/dispute/lib/Types.sol"; +import { GameType, Hash, Proposal } from "src/dispute/lib/Types.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; // Interfaces @@ -18,8 +18,8 @@ import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; import { ProtocolVersion } from "interfaces/L1/IProtocolVersions.sol"; +import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; /// @title Initializer_Test /// @dev Ensures that the `initialize()` function on contracts cannot be called more than @@ -124,13 +124,7 @@ contract Initializer_Test is CommonTest { name: "OptimismPortal2Impl", target: EIP1967Helper.getImplementation(address(optimismPortal2)), initCalldata: abi.encodeCall( - optimismPortal2.initialize, - ( - disputeGameFactory, - systemConfig, - superchainConfig, - GameType.wrap(uint32(deploy.cfg().respectedGameType())) - ) + optimismPortal2.initialize, (systemConfig, superchainConfig, anchorStateRegistry, ethLockbox) ) }) ); @@ -140,16 +134,11 @@ contract Initializer_Test is CommonTest { name: "OptimismPortal2Proxy", target: address(optimismPortal2), initCalldata: abi.encodeCall( - optimismPortal2.initialize, - ( - disputeGameFactory, - systemConfig, - superchainConfig, - GameType.wrap(uint32(deploy.cfg().respectedGameType())) - ) + optimismPortal2.initialize, (systemConfig, superchainConfig, anchorStateRegistry, ethLockbox) ) }) ); + // SystemConfigImpl contracts.push( InitializeableContract({ @@ -177,10 +166,10 @@ contract Initializer_Test is CommonTest { l1CrossDomainMessenger: address(0), l1ERC721Bridge: address(0), l1StandardBridge: address(0), - disputeGameFactory: address(0), optimismPortal: address(0), optimismMintableERC20Factory: address(0) - }) + }), + 0 ) ) }) @@ -212,10 +201,10 @@ contract Initializer_Test is CommonTest { l1CrossDomainMessenger: address(0), l1ERC721Bridge: address(0), l1StandardBridge: address(0), - disputeGameFactory: address(0), optimismPortal: address(0), optimismMintableERC20Factory: address(0) - }) + }), + 0 ) ) }) @@ -314,8 +303,8 @@ contract Initializer_Test is CommonTest { ( ISuperchainConfig(address(0)), IDisputeGameFactory(address(0)), - IOptimismPortal2(payable(0)), - OutputRoot({ root: Hash.wrap(bytes32(0)), l2BlockNumber: 0 }) + Proposal({ root: Hash.wrap(bytes32(0)), l2SequenceNumber: 0 }), + GameType.wrap(uint32(deploy.cfg().respectedGameType())) ) ) }) @@ -330,12 +319,34 @@ contract Initializer_Test is CommonTest { ( ISuperchainConfig(address(0)), IDisputeGameFactory(address(0)), - IOptimismPortal2(payable(0)), - OutputRoot({ root: Hash.wrap(bytes32(0)), l2BlockNumber: 0 }) + Proposal({ root: Hash.wrap(bytes32(0)), l2SequenceNumber: 0 }), + GameType.wrap(uint32(deploy.cfg().respectedGameType())) ) ) }) ); + + // ETHLockboxImpl + contracts.push( + InitializeableContract({ + name: "ETHLockboxImpl", + target: EIP1967Helper.getImplementation(address(ethLockbox)), + initCalldata: abi.encodeCall( + ethLockbox.initialize, (ISuperchainConfig(address(0)), new IOptimismPortal2[](0)) + ) + }) + ); + + // ETHLockboxProxy + contracts.push( + InitializeableContract({ + name: "ETHLockboxProxy", + target: address(ethLockbox), + initCalldata: abi.encodeCall( + ethLockbox.initialize, (ISuperchainConfig(address(0)), new IOptimismPortal2[](0)) + ) + }) + ); } /// @notice Tests that: @@ -344,30 +355,25 @@ contract Initializer_Test is CommonTest { /// 3. The `initialize()` function of each contract cannot be called again. function test_cannotReinitialize_succeeds() public { // Collect exclusions. - string[] memory excludes = new string[](11); - // TODO: Neither of these contracts are labeled properly in the deployment script. Both are - // currently being labeled as their non-interop versions. Remove these exclusions once - // the deployment script is fixed. - excludes[0] = "src/L1/SystemConfigInterop.sol"; - excludes[1] = "src/L1/OptimismPortalInterop.sol"; + uint256 j; + string[] memory excludes = new string[](8); // Contract is currently not being deployed as part of the standard deployment script. - excludes[2] = "src/L2/OptimismSuperchainERC20.sol"; + excludes[j++] = "src/L2/OptimismSuperchainERC20.sol"; // Periphery contracts don't get deployed as part of the standard deployment script. - excludes[3] = "src/periphery/*"; + excludes[j++] = "src/periphery/*"; // TODO: Deployment script is currently "broken" in the sense that it doesn't properly // label the FaultDisputeGame, PermissionedDisputeGame, SuperFaultDisputeGame, and // SuperPermissionedDisputeGame // contracts and instead simply deploys them anonymously. Means that functions like "getInitializedSlot" // don't work properly. Remove these exclusions once the deployment script is fixed. - excludes[4] = "src/dispute/FaultDisputeGame.sol"; - excludes[5] = "src/dispute/SuperFaultDisputeGame.sol"; - excludes[6] = "src/dispute/PermissionedDisputeGame.sol"; - excludes[7] = "src/dispute/SuperPermissionedDisputeGame.sol"; + excludes[j++] = "src/dispute/FaultDisputeGame.sol"; + excludes[j++] = "src/dispute/SuperFaultDisputeGame.sol"; + excludes[j++] = "src/dispute/PermissionedDisputeGame.sol"; + excludes[j++] = "src/dispute/SuperPermissionedDisputeGame.sol"; // TODO: Eventually remove this exclusion. Same reason as above dispute contracts. - excludes[8] = "src/L1/OPContractsManager.sol"; - excludes[9] = "src/L1/OPContractsManagerInterop.sol"; + excludes[j++] = "src/L1/OPContractsManager.sol"; // L2 contract initialization is tested in Predeploys.t.sol - excludes[10] = "src/L2/*"; + excludes[j++] = "src/L2/*"; // Get all contract names in the src directory, minus the excluded contracts. string[] memory contractNames = ForgeArtifacts.getContractNames("src/*", excludes);