diff --git a/.circleci/config.yml b/.circleci/config.yml index db70f0527b3..6ae6249ad25 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -94,7 +94,7 @@ orbs: slack: circleci/slack@6.0.0 shellcheck: circleci/shellcheck@3.2.0 codecov: codecov/codecov@5.0.3 - utils: ethereum-optimism/circleci-utils@1.0.22 + utils: ethereum-optimism/circleci-utils@1.0.23 docker: circleci/docker@2.8.2 github-cli: circleci/github-cli@2.7.0 @@ -212,7 +212,7 @@ commands: command: | # Manually craft the submodule update command in order to take advantage # of the -j parameter, which speeds it up a lot. - git submodule update --init --recursive --force -j 8 + git submodule update --init --recursive --single-branch --force -j 8 working_directory: packages/contracts-bedrock # Notifies us on Slack a build fails on develop @@ -399,6 +399,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - setup_remote_docker: docker_layer_caching: true - run: @@ -520,6 +521,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - attach_workspace: at: . - check-changed: @@ -572,6 +574,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - run: name: Check `RISCV.sol` bytecode working_directory: packages/contracts-bedrock @@ -622,6 +625,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - install-zstd - install-contracts-dependencies - run: @@ -656,7 +660,8 @@ jobs: - image: <> resource_class: xlarge steps: - - utils/checkout-with-mise + - utils/checkout-with-mise: + enable-mise-cache: true - attach_workspace: at: . - install-contracts-dependencies @@ -720,6 +725,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - attach_workspace: at: . - run: @@ -806,7 +812,7 @@ jobs: fi # Let them cook! - KONA_VERSION=$(jq -r .version kona/version.json) \ + KONA_VERSION=$(jq -r .version kona-proofs/version.json) \ docker buildx bake \ --progress plain \ --builder=buildx-build \ @@ -922,14 +928,6 @@ jobs: test_list: description: List of test files to run type: string - test_command: - description: Test command to execute (test or coverage) - type: string - default: test - test_flags: - description: Additional flags to pass to the test command - type: string - default: "" test_timeout: description: Timeout for running tests type: string @@ -949,6 +947,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: full + enable-mise-cache: true - install-zstd - run: name: Check if test list is empty @@ -987,7 +986,8 @@ jobs: TEST_FILES=$(echo "$TEST_FILES" | circleci tests split --split-by=timings) TEST_FILES=$(echo "$TEST_FILES" | sed 's|^test/||') MATCH_PATH="./test/{$(echo "$TEST_FILES" | paste -sd "," -)}" - forge <> <> --match-path "$MATCH_PATH" + mkdir -p results + forge test --match-path "$MATCH_PATH" --junit > results/results.xml environment: FOUNDRY_PROFILE: <> working_directory: packages/contracts-bedrock @@ -999,6 +999,11 @@ jobs: FOUNDRY_PROFILE: ci working_directory: packages/contracts-bedrock when: on_fail + - when: + condition: always + steps: + - store_test_results: + path: packages/contracts-bedrock/results/results.xml - run: name: Lint forge test names command: just lint-forge-tests-check-no-build @@ -1013,6 +1018,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: full + enable-mise-cache: true - install-contracts-dependencies - install-zstd - run: @@ -1033,7 +1039,9 @@ jobs: working_directory: packages/contracts-bedrock - run: name: Run heavy fuzz tests - command: just test + command: | + mkdir -p results + just test --junit > results/results.xml environment: FOUNDRY_PROFILE: ciheavy working_directory: packages/contracts-bedrock @@ -1050,6 +1058,11 @@ jobs: key: golang-build-cache-contracts-bedrock-heavy-fuzz-{{ checksum "go.sum" }} paths: - "~/.cache/go-build" + - when: + condition: always + steps: + - store_test_results: + path: packages/contracts-bedrock/results/results.xml - notify-failures-on-develop # AI Contracts Test Maintenance System @@ -1062,6 +1075,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - run: name: Check Python version command: python3 --version @@ -1074,8 +1088,8 @@ jobs: path: ops/ai-eng/contracts-test-maintenance/log.json destination: log.json - run: - name: Prepare Slack notification - command: just prepare-slack-notification >> $BASH_ENV + name: Build Slack notification + command: just build-slack-notification >> $BASH_ENV working_directory: ops/ai-eng when: always - slack/notify: @@ -1090,10 +1104,6 @@ jobs: - image: <> resource_class: 2xlarge parameters: - test_flags: - description: Additional flags to pass to the test command - type: string - default: "" test_timeout: description: Timeout for running tests type: string @@ -1109,6 +1119,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: full + enable-mise-cache: true - install-contracts-dependencies - install-zstd - attach_workspace: @@ -1151,7 +1162,7 @@ jobs: working_directory: packages/contracts-bedrock - run: name: Run coverage tests - command: just coverage-lcov-all <> + command: just coverage-lcov-all environment: FOUNDRY_PROFILE: <> ETH_RPC_URL: https://ci-mainnet-l1-archive.optimism.io @@ -1202,7 +1213,8 @@ jobs: - image: <> resource_class: 2xlarge steps: - - utils/checkout-with-mise + - utils/checkout-with-mise: + enable-mise-cache: true - install-contracts-dependencies - install-zstd - attach_workspace: @@ -1234,7 +1246,9 @@ jobs: features: <> - run: name: Run tests - command: just test-upgrade + command: | + mkdir -p results + just test-upgrade --junit > results/results.xml environment: FOUNDRY_FUZZ_SEED: 42424242 FOUNDRY_FUZZ_RUNS: 1 @@ -1271,6 +1285,11 @@ jobs: - store_artifacts: path: packages/contracts-bedrock/failed-test-traces.log when: on_fail + - when: + condition: always + steps: + - store_test_results: + path: packages/contracts-bedrock/results/results.xml - notify-failures-on-develop contracts-bedrock-upload: @@ -1279,6 +1298,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - install-contracts-dependencies - check-changed: patterns: contracts-bedrock @@ -1293,7 +1313,8 @@ jobs: - image: <> resource_class: xlarge steps: - - utils/checkout-with-mise + - utils/checkout-with-mise: + enable-mise-cache: true - install-contracts-dependencies - attach_workspace: at: . @@ -1334,6 +1355,30 @@ jobs: - run-contracts-check: command: opcm-upgrade-checks-no-build + contracts-bedrock-checks-fast: + docker: + - image: <> + resource_class: 2xlarge + steps: + - utils/checkout-with-mise: + enable-mise-cache: true + - install-zstd + - install-contracts-dependencies + - check-changed: + patterns: contracts-bedrock + - run: + name: Print forge version + command: forge --version + - run: + name: Pull cached artifacts + command: bash scripts/ops/pull-artifacts.sh --fallback-to-latest + working_directory: packages/contracts-bedrock + - run: + name: Run checks + command: just check-fast + working_directory: packages/contracts-bedrock + - notify-failures-on-develop + todo-issues: parameters: check_closed: @@ -1344,6 +1389,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - run: name: Install ripgrep command: sudo apt-get install -y ripgrep @@ -1370,6 +1416,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - check-changed: patterns: "<>" - attach_workspace: @@ -1402,6 +1449,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - restore_cache: key: golangci-v1-{{ checksum ".golangci.yaml" }} - run: @@ -1451,6 +1499,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - attach_workspace: at: . - restore_cache: @@ -1526,6 +1575,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - attach_workspace: at: . - run: @@ -1590,6 +1640,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true # Restore cached Go modules - restore_cache: keys: @@ -1688,8 +1739,18 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - attach_workspace: at: . + - run: + name: Configure Rust binary paths (sysgo) + command: | + ROOT_DIR="$(pwd)" + BIN_DIR="$ROOT_DIR/.circleci-cache/rust-binaries" + echo "export RUST_BINARY_PATH_KONA_NODE=$BIN_DIR/kona-node" >> "$BASH_ENV" + echo "export RUST_BINARY_PATH_KONA_SUPERVISOR=$BIN_DIR/kona-supervisor" >> "$BASH_ENV" + echo "export RUST_BINARY_PATH_OP_RBUILDER=$BIN_DIR/op-rbuilder" >> "$BASH_ENV" + echo "export RUST_BINARY_PATH_ROLLUP_BOOST=$BIN_DIR/rollup-boost" >> "$BASH_ENV" # Restore cached Go modules - restore_cache: keys: @@ -1782,6 +1843,18 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true + - attach_workspace: + at: . + - run: + name: Configure Rust binary paths (sysgo) + command: | + ROOT_DIR="$(pwd)" + BIN_DIR="$ROOT_DIR/.circleci-cache/rust-binaries" + echo "export RUST_BINARY_PATH_KONA_NODE=$BIN_DIR/kona-node" >> "$BASH_ENV" + echo "export RUST_BINARY_PATH_KONA_SUPERVISOR=$BIN_DIR/kona-supervisor" >> "$BASH_ENV" + echo "export RUST_BINARY_PATH_OP_RBUILDER=$BIN_DIR/op-rbuilder" >> "$BASH_ENV" + echo "export RUST_BINARY_PATH_ROLLUP_BOOST=$BIN_DIR/rollup-boost" >> "$BASH_ENV" - restore_cache: keys: - go-mod-v1-{{ checksum "go.sum" }} @@ -1829,6 +1902,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - attach_workspace: at: . - clean-old-acceptor-logs @@ -1881,6 +1955,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - run: name: Lint/Vet/Build op-acceptance-tests/cmd working_directory: op-acceptance-tests @@ -1961,6 +2036,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - run: name: Install tools command: | @@ -1984,6 +2060,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - restore_cache: name: Restore cannon prestate cache key: cannon-prestate-{{ checksum "./cannon/bin/cannon" }}-{{ checksum "op-program/bin/op-program-client.elf" }} @@ -2012,6 +2089,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - setup_remote_docker - run: name: Build prestates @@ -2028,22 +2106,23 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - restore_cache: name: Restore kona cache - key: kona-prestate-{{ checksum "./kona/justfile" }}-{{ checksum "./kona/version.json" }} + key: kona-prestate-{{ checksum "./kona-proofs/justfile" }}-{{ checksum "./kona-proofs/version.json" }} - run: name: Build kona prestates command: just build-prestates - working_directory: kona + working_directory: kona-proofs - save_cache: - key: kona-prestate-{{ checksum "./kona/justfile" }}-{{ checksum "./kona/version.json" }} + key: kona-prestate-{{ checksum "./kona-proofs/justfile" }}-{{ checksum "./kona-proofs/version.json" }} name: Save Kona to cache paths: - - "kona/prestates/" + - "kona-proofs/prestates/" - persist_to_workspace: root: . paths: - - "kona/prestates/*" + - "kona-proofs/prestates/*" cannon-kona-host: docker: @@ -2051,9 +2130,10 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - restore_cache: name: Restore kona host cache - key: kona-host-{{ checksum "./kona/justfile" }}-{{ checksum "./kona/version.json" }} + key: kona-host-{{ checksum "./kona-proofs/justfile" }}-{{ checksum "./kona-proofs/version.json" }} - run: # The mise plugin for clang is broken so install via apt-get name: Install clang command: | @@ -2062,16 +2142,121 @@ jobs: - run: name: Build kona host command: just build-kona-host - working_directory: kona + working_directory: kona-proofs - save_cache: - key: kona-host-{{ checksum "./kona/justfile" }}-{{ checksum "./kona/version.json" }} + key: kona-host-{{ checksum "./kona-proofs/justfile" }}-{{ checksum "./kona-proofs/version.json" }} name: Save Kona host to cache paths: - - "kona/bin/kona-host" + - "kona-proofs/bin/kona-host" + - persist_to_workspace: + root: . + paths: + - "kona-proofs/bin/kona-host" + + # Build a single Rust binary for sysgo tests (parameterized). + rust-binary-build: + docker: + - image: <> + resource_class: xlarge + parameters: + directory: + description: "Directory containing the Cargo workspace" + type: string + binaries: + description: "Space-separated list of binary names to copy from target/release into the cache" + type: string + build_command: + description: "Cargo build command to run" + type: string + needs_clang: + description: "Whether to install clang (needed by bindgen for reth-mdbx-sys)" + type: boolean + default: false + steps: + - utils/checkout-with-mise: + checkout-method: blobless + - run: + name: Get << parameters.directory >> submodule HASH + command: | + mkdir -p .circleci-cache + # Verify the path is a git submodule (gitlink). For submodules, ls-tree prints: + # 160000 commit + MODE_AND_TYPE="$(git ls-tree HEAD << parameters.directory >> | awk '{print $1 " " $2}')" + if [ "$MODE_AND_TYPE" != "160000 commit" ]; then + echo "ERROR: << parameters.directory >> is not a submodule path (expected '160000 commit', got '$MODE_AND_TYPE')" >&2 + git ls-tree HEAD << parameters.directory >> >&2 || true + exit 1 + fi + + git ls-tree HEAD << parameters.directory >> | awk '{print $3}' > .circleci-cache/expected-submod-sha.txt + echo "Expected submodule HASH: $(cat .circleci-cache/expected-submod-sha.txt)" + echo "<< parameters.binaries >>" | tr ' ' '\n' | awk 'NF' | sort -u > .circleci-cache/expected-binaries.txt + echo "Expected binaries: $(tr '\n' ' ' < .circleci-cache/expected-binaries.txt)" + - restore_cache: + name: Restore << parameters.directory >> cache + keys: + - rust-<< parameters.directory >>-v8-{{ checksum ".circleci-cache/expected-submod-sha.txt" }}-{{ checksum ".circleci-cache/expected-binaries.txt" }} + - run: + name: Build << parameters.directory >> submodule (if needed) + command: | + ROOT_DIR="$(pwd)" + BIN_CACHE_DIR="$ROOT_DIR/.circleci-cache/rust-binaries" + + HIT=true + for bin in << parameters.binaries >>; do + if [ ! -f "$BIN_CACHE_DIR/$bin" ]; then + HIT=false + break + fi + done + if [ "$HIT" = "true" ]; then + echo "Cache hit - binaries exist" + exit 0 + fi + + echo "Cache miss - will build" + # Only run on cache miss. We intentionally do not cache into the submodule directory so that cache + # restore cannot make the submodule path non-empty (which breaks `git submodule update --init`). + git submodule update --init --recursive --depth 1 -j 8 --single-branch << parameters.directory >> + + if [ "<< parameters.needs_clang >>" = "true" ]; then + sudo apt-get update + sudo apt-get install -y --no-install-recommends clang + fi + + cd << parameters.directory >> && << parameters.build_command >> + + mkdir -p "$BIN_CACHE_DIR" + for bin in << parameters.binaries >>; do + SRC="$ROOT_DIR/<< parameters.directory >>/target/release/$bin" + DST="$BIN_CACHE_DIR/$bin" + if [ ! -f "$SRC" ]; then + echo "ERROR: expected built binary not found at $SRC" >&2 + exit 1 + fi + cp "$SRC" "$DST" + chmod +x "$DST" || true + done + no_output_timeout: 30m + - save_cache: + key: rust-<< parameters.directory >>-v8-{{ checksum ".circleci-cache/expected-submod-sha.txt" }}-{{ checksum ".circleci-cache/expected-binaries.txt" }} + name: Save << parameters.directory >> cache + paths: + - ".circleci-cache/rust-binaries" - persist_to_workspace: root: . paths: - - "kona/bin/kona-host" + - ".circleci-cache/rust-binaries" + + # Aggregator job - allows downstream jobs to depend on a single job instead of listing all build jobs. + rust-binaries-for-sysgo: + docker: + - image: <> + resource_class: small + steps: + - run: + name: All Rust binaries ready + command: echo "All Rust binaries built and persisted to workspace" publish-cannon-prestates: resource_class: medium @@ -2080,6 +2265,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - attach_workspace: at: "." - gcp-cli/install @@ -2141,6 +2327,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - run: name: Verify reproducibility command: make -C op-program verify-reproducibility @@ -2156,6 +2343,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - setup_remote_docker - run: name: Build cannon @@ -2224,6 +2412,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - setup_remote_docker - run: name: Run Analyzer @@ -2238,6 +2427,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - run: name: Verify Compatibility command: | @@ -2251,6 +2441,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - check-changed: patterns: op-node - run: @@ -2264,6 +2455,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - check-changed: patterns: op-service - run: @@ -2276,6 +2468,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - run: command: just check-forge-version working_directory: op-deployer @@ -2287,6 +2480,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - install-contracts-dependencies - check-changed: no_go_deps: "true" @@ -2326,6 +2520,7 @@ jobs: service_account_email: GCP_CONTRACTS_PUBLISHER_SERVICE_ACCOUNT_EMAIL - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - install-contracts-dependencies - install-zstd - run: @@ -2362,7 +2557,8 @@ jobs: - gcp-oidc-authenticate: gcp_cred_config_file_path: /tmp/gcp_cred_config.json oidc_token_file_path: /tmp/oidc_token.json - - utils/checkout-with-mise + - utils/checkout-with-mise: + enable-mise-cache: true - attach_workspace: { at: "." } - run: name: Configure Docker @@ -2380,6 +2576,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - install-contracts-dependencies - run: name: Build contracts @@ -2445,6 +2642,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - run: name: Collect devnet metrics for op-acceptance-tests command: | @@ -2469,6 +2667,7 @@ jobs: steps: - utils/checkout-with-mise: checkout-method: blobless + enable-mise-cache: true - run: name: Generate flaky acceptance tests report command: | @@ -2582,8 +2781,9 @@ workflows: - OPTIMISM_PORTAL_INTEROP - CANNON_KONA,DEPLOY_V2_DISPUTE_GAMES - OPCM_V2 - - OPCM_V2,OPTIMISM_PORTAL_INTEROP - CUSTOM_GAS_TOKEN + - OPCM_V2,CUSTOM_GAS_TOKEN + - OPCM_V2,OPTIMISM_PORTAL_INTEROP context: - circleci-repo-readonly-authenticated-github-token - contracts-bedrock-tests: @@ -2633,6 +2833,9 @@ workflows: - contracts-bedrock-build context: - circleci-repo-readonly-authenticated-github-token + - contracts-bedrock-checks-fast: + context: + - circleci-repo-readonly-authenticated-github-token - contracts-bedrock-upload: requires: - contracts-bedrock-build @@ -3169,12 +3372,41 @@ workflows: - cannon-prestate-quick: context: - circleci-repo-readonly-authenticated-github-token + - rust-binary-build: + name: rust-build-kona + directory: kona + binaries: "kona-node kona-supervisor" + build_command: cargo build --release --bin kona-node --bin kona-supervisor + needs_clang: true + context: + - circleci-repo-readonly-authenticated-github-token + - rust-binary-build: + name: rust-build-op-rbuilder + directory: op-rbuilder + binaries: "op-rbuilder" + build_command: cargo build --release -p op-rbuilder --bin op-rbuilder + needs_clang: true + context: + - circleci-repo-readonly-authenticated-github-token + - rust-binary-build: + name: rust-build-rollup-boost + directory: rollup-boost + binaries: "rollup-boost" + build_command: cargo build --release -p rollup-boost --bin rollup-boost + context: + - circleci-repo-readonly-authenticated-github-token + - rust-binaries-for-sysgo: + requires: + - rust-build-kona + - rust-build-op-rbuilder + - rust-build-rollup-boost - op-acceptance-tests-flake-shake: context: - circleci-repo-readonly-authenticated-github-token requires: - contracts-bedrock-build - cannon-prestate-quick + - rust-binaries-for-sysgo - op-acceptance-tests-flake-shake-report: requires: - op-acceptance-tests-flake-shake @@ -3280,6 +3512,34 @@ workflows: - cannon-kona-host: # needed for sysgo tests (if any package is in-memory) context: - circleci-repo-readonly-authenticated-github-token + - rust-binary-build: + name: rust-build-kona + directory: kona + binaries: "kona-node kona-supervisor" + build_command: cargo build --release --bin kona-node --bin kona-supervisor + needs_clang: true + context: + - circleci-repo-readonly-authenticated-github-token + - rust-binary-build: + name: rust-build-op-rbuilder + directory: op-rbuilder + binaries: "op-rbuilder" + build_command: cargo build --release -p op-rbuilder --bin op-rbuilder + needs_clang: true + context: + - circleci-repo-readonly-authenticated-github-token + - rust-binary-build: + name: rust-build-rollup-boost + directory: rollup-boost + binaries: "rollup-boost" + build_command: cargo build --release -p rollup-boost --bin rollup-boost + context: + - circleci-repo-readonly-authenticated-github-token + - rust-binaries-for-sysgo: + requires: + - rust-build-kona + - rust-build-op-rbuilder + - rust-build-rollup-boost # IN-MEMORY (all) - op-acceptance-tests: name: memory-all @@ -3294,6 +3554,7 @@ workflows: - cannon-prestate-quick - cannon-kona-prestate - cannon-kona-host + - rust-binaries-for-sysgo # Generate flaky test report - generate-flaky-report: name: generate-flaky-tests-report diff --git a/.cursor/rules/solidity-styles.mdc b/.cursor/rules/solidity-styles.mdc index 375a7d9fd07..f3292277ecb 100644 --- a/.cursor/rules/solidity-styles.mdc +++ b/.cursor/rules/solidity-styles.mdc @@ -3,12 +3,15 @@ description: globs: *.sol alwaysApply: false --- + # Optimism Solidity Style Guide Applies to Solidity files. ## Comments -- Use triple-slash solidity natspec comment style + +- NatSpec documentation comments must use the triple-slash `///` style +- Use `//` for regular inline comments that are not NatSpec - Always use `@notice` instead of `@dev` - Use a line-length of 100 characters - Custom tags: @@ -19,10 +22,12 @@ Applies to Solidity files. - `@custom:network-specific`: Add to state variables which vary between OP Chains ## Errors + - When adding new errors, always use custom Solidity errors - Custom errors should take the format `ContractName_ErrorDescription` ## Naming Conventions + - Function parameters should be prefixed with an underscore - Function return arguments should be suffixed with an underscore - Event parameters should NOT be prefixed with an underscore @@ -33,6 +38,7 @@ Applies to Solidity files. - Spacers must be named `spacer___` and be `private` ## Upgradeability + - Contracts should be built assuming upgradeability by default - Extend OpenZeppelin's `Initializable` or base contract - Use the `ReinitializableBase` contract @@ -43,6 +49,7 @@ Applies to Solidity files. - Set any immutables (though generally avoid immutables) ## Versioning + - All non-library/non-abstract contracts must inherit `ISemver` and expose `version()` - Production-ready contracts must have version `1.0.0` or greater - Version increments: @@ -53,12 +60,15 @@ Applies to Solidity files. AI code review tools should NOT comment on the choice of version increment. ## Dependencies + - Prefer OpenZeppelin's Upgradeable contracts for basic functionality ## State Changes + - All state changing functions should emit a corresponding event ## Testing + - Tests should be written using Foundry - For testing reverts with low-level calls, use the `revertsAsExpected` pattern - Test function naming: `[method]_[functionName]_[reason]_[status]` diff --git a/.gitmodules b/.gitmodules index 1591d79b2a8..3f1e08b2426 100644 --- a/.gitmodules +++ b/.gitmodules @@ -32,3 +32,12 @@ [submodule "packages/contracts-bedrock/lib/superchain-registry"] path = packages/contracts-bedrock/lib/superchain-registry url = https://github.com/ethereum-optimism/superchain-registry +[submodule "op-rbuilder"] + path = op-rbuilder + url = git@github.com:flashbots/op-rbuilder.git +[submodule "rollup-boost"] + path = rollup-boost + url = git@github.com:flashbots/rollup-boost.git +[submodule "kona"] + path = kona + url = git@github.com:op-rs/kona.git diff --git a/Makefile b/Makefile index b5a7f61d890..a0ada6a77d8 100644 --- a/Makefile +++ b/Makefile @@ -34,7 +34,7 @@ golang-docker: ## Builds Docker images for Go components using buildx GIT_COMMIT=$$(git rev-parse HEAD) \ GIT_DATE=$$(git show -s --format='%ct') \ IMAGE_TAGS=$$(git rev-parse HEAD),latest \ - KONA_VERSION=$$(jq -r .version kona/version.json) \ + KONA_VERSION=$$(jq -r .version kona-proofs/version.json) \ docker buildx bake \ --progress plain \ --load \ @@ -128,6 +128,10 @@ op-supernode: ## Builds op-supernode binary just $(JUSTFLAGS) ./op-supernode/op-supernode .PHONY: op-supernode +op-interop-filter: ## Builds op-interop-filter binary + just $(JUSTFLAGS) ./op-interop-filter/op-interop-filter +.PHONY: op-interop-filter + op-program: ## Builds op-program binary make -C ./op-program op-program .PHONY: op-program diff --git a/docker-bake.hcl b/docker-bake.hcl index 7a96988365f..f2b8fec9ff8 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -69,6 +69,10 @@ variable "OP_SUPERNODE_VERSION" { default = "${GIT_VERSION}" } +variable "OP_INTEROP_FILTER_VERSION" { + default = "${GIT_VERSION}" +} + variable "OP_TEST_SEQUENCER_VERSION" { default = "${GIT_VERSION}" } @@ -228,6 +232,19 @@ target "op-supernode" { tags = [for tag in split(",", IMAGE_TAGS) : "${REGISTRY}/${REPOSITORY}/op-supernode:${tag}"] } +target "op-interop-filter" { + dockerfile = "ops/docker/op-stack-go/Dockerfile" + context = "." + args = { + GIT_COMMIT = "${GIT_COMMIT}" + GIT_DATE = "${GIT_DATE}" + OP_INTEROP_FILTER_VERSION = "${OP_INTEROP_FILTER_VERSION}" + } + target = "op-interop-filter-target" + platforms = split(",", PLATFORMS) + tags = [for tag in split(",", IMAGE_TAGS) : "${REGISTRY}/${REPOSITORY}/op-interop-filter:${tag}"] +} + target "op-test-sequencer" { dockerfile = "ops/docker/op-stack-go/Dockerfile" context = "." diff --git a/docs/security-reviews/2025_10-Rev-Sharing-Spearbit.pdf b/docs/security-reviews/2025_10-Rev-Sharing-Spearbit.pdf new file mode 100644 index 00000000000..8dd8acd6fd5 Binary files /dev/null and b/docs/security-reviews/2025_10-Rev-Sharing-Spearbit.pdf differ diff --git a/docs/security-reviews/2025_11-Rev-Sharing-Contracts-Upgrader.pdf b/docs/security-reviews/2025_11-Rev-Sharing-Contracts-Upgrader.pdf new file mode 100644 index 00000000000..ca98c2c199a Binary files /dev/null and b/docs/security-reviews/2025_11-Rev-Sharing-Contracts-Upgrader.pdf differ diff --git a/docs/security-reviews/README.md b/docs/security-reviews/README.md index 5c133b0db2e..a6b07ac226b 100644 --- a/docs/security-reviews/README.md +++ b/docs/security-reviews/README.md @@ -5,49 +5,50 @@ The following is a list of past security reviews. Each review is focused on a different part of the codebase, and at a different point in time. Please see the report for the specific details. -| Date | Reviewer | Focus and Scope | Report Link | Commit | Subsequent Release | -| ------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | ------------------- | -| 2020-10 | Trail of Bits | Rollup | [2020_10-TrailOfBits.pdf](./2020_10-Rollup-TrailOfBits.pdf) | | | -| 2020-11 | Dapphub | ECDSA Wallet | [2020_11-Dapphub-ECDSA_Wallet.pdf](./2020_11-Dapphub-ECDSA_Wallet.pdf) | | | -| 2021-03 | OpenZeppelin | OVM and Rollup | [2021_03-OVM_and_Rollup-OpenZeppelin.pdf](./2021_03-OVM_and_Rollup-OpenZeppelin.pdf) | | | -| 2021-03 | ConsenSys Diligence | Safety Checker | [2021_03-SafetyChecker-ConsenSysDiligence.pdf](./2021_03-SafetyChecker-ConsenSysDiligence.pdf) | | | -| 2022-05 | Zeppelin | Bedrock Contracts | [2022_05-Bedrock_Contracts-Zeppelin.pdf](./2022_05-Bedrock_Contracts-Zeppelin.pdf) | | | -| 2022-05 | Trail of Bits | OpNode | [2022_05-OpNode-TrailOfBits.pdf](./2022_05-OpNode-TrailOfBits.pdf) | | | -| 2022-08 | Sigma Prime | Bedrock GoLang | [2022_08-Bedrock_GoLang-SigmaPrime.pdf](./2022_08-Bedrock_GoLang-SigmaPrime.pdf) | | | -| 2022-09 | Zeppelin | Bedrock and Periphery: All contracts in `packages/contracts-bedrock/contracts` | [2022_09-Bedrock_and_Periphery-Zeppelin.pdf](./2022_09-Bedrock_and_Periphery-Zeppelin.pdf) | 93d3bd411a8ae75702539ac9c5fe00bad21d4104 | op-contracts/v1.0.0 | -| 2022-10 | Spearbit | Drippie: `Drippie.sol` | [2022_10-Drippie-Spearbit.pdf](./2022_10-Drippie-Spearbit.pdf) | 2a7be367634f147736f960eb2f38a77291cdfcad | op-contracts/v1.0.0 | -| 2022-11 | Trail of Bits | Invariant Testing: `OptimismPortal.sol` | [2022_11-Invariant_Testing-TrailOfBits.pdf](./2022_11-Invariant_Testing-TrailOfBits.pdf) | b31d35b67755479645dd150e7cc8c6710f0b4a56 | op-contracts/v1.0.0 | -| 2022-12 | Runtime Verification | Deposit Transaction: `OptimismPortal.sol` | [2022_12-DepositTransaction-RuntimeVerification.pdf](./2022_12-DepositTransaction-RuntimeVerification.pdf) | | op-contracts/v1.0.0 | -| 2023-01 | Trail of Bits | Bedrock Updates: `SystemConfig.sol` | [2023_01-Bedrock_Updates-TrailOfBits.pdf](./2023_01-Bedrock_Updates-TrailOfBits.pdf) | ee96ff8585699b054c95c6ff4a2411ee9fedcc87 | op-contracts/v1.0.0 | -| 2023-01 | Sherlock | Bedrock: All contracts in `packages/contracts-bedrock/src` | Sherlock Bedrock Contest ([site](https://audits.sherlock.xyz/contests/38), [repo](https://github.com/sherlock-audit/2023-01-optimism)) | 3f4b3c328153a8aa03611158b6984d624b17c1d9 | op-contracts/v1.0.0 | -| 2023-03 | Sherlock | Bedrock Fixes: All contracts in `packages/contracts-bedrock/src` | Sherlock Bedrock Contest: Fix Review ([site](https://audits.sherlock.xyz/contests/63), [repo](https://github.com/sherlock-audit/2023-03-optimism)) | 9b9f78c6613c6ee53b93ca43c71bb74479f4b975 | op-contracts/v1.0.0 | -| 2023-12 | Trust | Superchain Config Upgrade: `SuperchainConfig.sol`, `L1CrossDomainMessenger.sol`, `L1ERC721Bridge.sol`, `L1StandardBridge.sol`, `OptimismPortal.sol`, `CrossDomainMessenger.sol`, `ERC721Bridge.sol`, `StandardBridge.sol` | [2023_12_SuperchainConfigUpgrade_Trust.pdf](./2023_12_SuperchainConfigUpgrade_Trust.pdf) | d1651bb22645ebd41ac4bb2ab4786f9a56fc1003 | op-contracts/v1.2.0 | -| 2024-02 | Runtime Verification | Pausability | [Kontrol Verification][kontrol] | | | -| 2024-02 | Cantina | MCP L1: `OptimismPortal.sol`, `L1CrossDomainMessenger.sol`, `L1StandardBridge.sol`, `L1ERC721Bridge.sol`, `OptimismMintableERC20Factory.sol`, `L2OutputOracle.sol`, `SystemConfig.sol` | [2024_02-MCP_L1-Cantina.pdf](./2024_02-MCP_L1-Cantina.pdf) | e6ef3a900c42c8722e72c2e2314027f85d12ced5 | op-contracts/v1.3.0 | -| 2024-03 | Sherlock | Fault Proofs | Sherlock Optimism Fault Proofs Contest ([site](https://audits.sherlock.xyz/contests/205), [repo](https://github.com/sherlock-audit/2024-02-optimism-2024)) | | | -| 2024-08 | Cantina | Fault proof MIPS: `MIPS.sol` | [2024_08_Fault-Proofs-MIPS_Cantina.pdf](./2024_08_Fault-Proofs-MIPS_Cantina.pdf) | 71b93116738ee98c9f8713b1a5dfe626ce06c1b2 | op-contracts/v1.4.0 | -| 2024-08 | Spearbit | Fault proof no-MIPS: All contracts in the `packages/contracts-bedrock/src/dispute` directory | [2024_08_Fault-Proofs-No-MIPS_Spearbit.pdf](./2024_08_Fault-Proofs-No-MIPS_Spearbit.pdf) | 1f7081798ce2d49b8643514663d10681cb853a3d | op-contracts/v1.6.0 | -| 2024-10 | 3Doc Security | Fault proof MIPS: `MIPS.sol` | [2024_10-Cannon-FGETFD-3DocSecurity.md](./2024_10-Cannon-FGETFD-3DocSecurity.md) | 52d0e60c16498ad4efec8798e3fc1b36b13f46a2 | op-contracts/v1.8.0 | -| 2024-12 | MiloTruck (independent) | DeputyPauseModule | [2024_12-DPM-MiloTruck.pdf](./2024_12-DPM-MiloTruck.pdf) | 2f17e6b67c61de5d8073d556272796d201bc740b | | -| 2024-12 | Radiant Labs | DeputyPauseModule | [2024_12-DPM-RadiantLabs.pdf](./2024_12-DPM-RadiantLabs.pdf) | 2f17e6b67c61de5d8073d556272796d201bc740b | | -| 2025-01 | Offbeat Labs | Incident Response Improvements | [2025_01-IRI-OffbeatLabs.pdf](./2025_01-IRI-OffbeatLabs.pdf) | 984bae9146398a2997ec13757bfe2438ca8f92eb | | -| 2025-01 | Spearbit | 64-bit Multithreaded Cannon: `MIPS64.sol` | [2025_01-MT-Cannon-Spearbit.pdf](./2025_01-MT-Cannon-Spearbit.pdf) | cc2715c3d6ebef374451b598f48980ad817e0a0e | | -| 2025-01 | Coinbase Protocol Security | Multi-thread & 64-bit Cannon | [2025_01-MT-Cannon-Base.pdf](./2025_01-MT-Cannon-Base.pdf) | b8c011f18c79d735e01168345fc1c6f02fac584f | | -| 2025-02 | Spearbit | Upgrade 13 | [2025_02-Upgrade13-Spearbit.pdf](./2025_02-Upgrade13-Spearbit.pdf) | 7d6d15437b7580b022f4c8c1ea9c0cd8d2e587e1 | op-contracts/v2.0.0 | -| 2025-03 | Spearbit | Interop Contracts | [2025_03-Interop-Contracts-Spearbit.pdf](./2025_03-Interop-Contracts-Spearbit.pdf) | 6c80f23ab3074b5c66ff06e390ae2448bd4d2240 | | -| 2025-03 | Wonderland | Interop Portal Contracts (u16) | [2025_03-Interop-Portal-Wonderland.pdf](./2025_03-Interop-Portal-Wonderland.pdf) | 9df1fc15d0bf0dc9464db249ce06424607d5f399 | op-contracts/v4.0.0 | -| 2025-04 | Aleph_v (independent) | op-program and op-challenger blob preimage handling | [2025_04-op-program-blob-handling-aleph_v.pdf](./2025_04-op-program-blob-handling-aleph_v.pdf) | 08d81d98237a3077fbc13fcd4b70f2e8d2e14115 | op-program/v1.6.0 | -| 2025-04 | Cantina (contest) | Interop Portal Contracts (u16) | [2025_04-Interop-Portal-Cantina.pdf](./2025_04-Interop-Portal-Cantina.pdf) | e4b921c9dbf8cd3a8db20ef4f15e0e2aa495fcc3 | op-contracts/v4.0.0 | -| 2025-05 | Spearbit | Interop Portal Contracts (u16) | [2025_05-Interop-Portal-Spearbit.pdf](./2025_05-Interop-Portal-Spearbit.pdf) | 7cd84fed9554193c2dcd683e1ff2d0e2605448f6 | op-contracts/v4.0.0 | -| 2025-05 | Coinbase Protocol Security | Cannon updates to support Go 1.23 and Kona | [2025_05-Cannon-Go-Updates-Coinbase.pdf](./2025_05-Cannon-Go-Updates-Coinbase.pdf) | 4c68444bc9b130e892b52cacf67b31f0424fb6d0 | | -| 2025-06 | Spearbit | Cannon Go 1.23 support fix review | [2025_06-Spearbit-Cannon-fix-review.pdf](./2025_06-Spearbit-Cannon-fix-review.pdf) | ffe3d5fed05cabf46a67ea00627a0959c0caa0b5 | op-contracts/v4.0.0 | -| 2025-06 | Radiant Labs | Cannon Go 1.24 support | [2025_06-Cannon-3DOC.pdf](./2025_06-Cannon-3DOC.pdf) | 689111fca9a10e6670ba0b5c7f1a549a212c855b | | -| 2025-05 | Spearbit | Upgrade 16 | [2025_05-Upgrade16-Spearbit.pdf](./2025_05-Upgrade16-Spearbit.pdf) / [Auditor hosted report][SpearbitMay25] | 54c19f6acb7a6d3505f884bae601733d3d54a3a6 | op-contracts/v4.0.0 | -| 2025-07 | Spearbit | VerifyOPCM | [2025_07-VerifyOPCM-Spearbit.pdf](./2025_07-VerifyOPCM-Spearbit.pdf) / [Auditor hosted report][SpearbitJuly25] | 731280c6fc0ad184d252e0fb1d0ad12b5f59fd60 | op-contracts/v4.0.0 | -| 2025-09 | Spearbit | U16a | [2025_09-U16a-Spearbit.pdf](./2025_09-U16a-Spearbit.pdf) | 475801690f7a451469ee4da87b5fe3c54c92f372 | op-contracts/v4.1.0 | -| 2025-10 | Spearbit | U17 | [2025_10-U17-Spearbit.pdf](./2025_10-U17-Spearbit.pdf) | aeed7033f7f739d8ecd4bd70a42ff09013bbc91e | op-contracts/v5.0.0 | -| 2025-11 | Spearbit | Safer Safes | [2025_11-SaferSafes-Spearbit.pdf](./2025_11-SaferSafes-Spearbit.pdf) | cb54822c5e18925498f48d8677b71992bf402631 | op-safe-contracts/v1.0.0 | -| 2025-11 | Spearbit | Custom Gas Token | [2025_11-Custom-Gas-Token-Spearbit.pdf](./2025_11-Custom-Gas-Token-Spearbit.pdf) | 1f888ede1940fce20f71db89fc13039fdd96757e | op-contracts/v6.0.0 | +| Date | Reviewer | Focus and Scope | Report Link | Commit | Subsequent Release | +| ------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | --------------------------------------------- | +| 2020-10 | Trail of Bits | Rollup | [2020_10-TrailOfBits.pdf](./2020_10-Rollup-TrailOfBits.pdf) | | | +| 2020-11 | Dapphub | ECDSA Wallet | [2020_11-Dapphub-ECDSA_Wallet.pdf](./2020_11-Dapphub-ECDSA_Wallet.pdf) | | | +| 2021-03 | OpenZeppelin | OVM and Rollup | [2021_03-OVM_and_Rollup-OpenZeppelin.pdf](./2021_03-OVM_and_Rollup-OpenZeppelin.pdf) | | | +| 2021-03 | ConsenSys Diligence | Safety Checker | [2021_03-SafetyChecker-ConsenSysDiligence.pdf](./2021_03-SafetyChecker-ConsenSysDiligence.pdf) | | | +| 2022-05 | Zeppelin | Bedrock Contracts | [2022_05-Bedrock_Contracts-Zeppelin.pdf](./2022_05-Bedrock_Contracts-Zeppelin.pdf) | | | +| 2022-05 | Trail of Bits | OpNode | [2022_05-OpNode-TrailOfBits.pdf](./2022_05-OpNode-TrailOfBits.pdf) | | | +| 2022-08 | Sigma Prime | Bedrock GoLang | [2022_08-Bedrock_GoLang-SigmaPrime.pdf](./2022_08-Bedrock_GoLang-SigmaPrime.pdf) | | | +| 2022-09 | Zeppelin | Bedrock and Periphery: All contracts in `packages/contracts-bedrock/contracts` | [2022_09-Bedrock_and_Periphery-Zeppelin.pdf](./2022_09-Bedrock_and_Periphery-Zeppelin.pdf) | 93d3bd411a8ae75702539ac9c5fe00bad21d4104 | op-contracts/v1.0.0 | +| 2022-10 | Spearbit | Drippie: `Drippie.sol` | [2022_10-Drippie-Spearbit.pdf](./2022_10-Drippie-Spearbit.pdf) | 2a7be367634f147736f960eb2f38a77291cdfcad | op-contracts/v1.0.0 | +| 2022-11 | Trail of Bits | Invariant Testing: `OptimismPortal.sol` | [2022_11-Invariant_Testing-TrailOfBits.pdf](./2022_11-Invariant_Testing-TrailOfBits.pdf) | b31d35b67755479645dd150e7cc8c6710f0b4a56 | op-contracts/v1.0.0 | +| 2022-12 | Runtime Verification | Deposit Transaction: `OptimismPortal.sol` | [2022_12-DepositTransaction-RuntimeVerification.pdf](./2022_12-DepositTransaction-RuntimeVerification.pdf) | | op-contracts/v1.0.0 | +| 2023-01 | Trail of Bits | Bedrock Updates: `SystemConfig.sol` | [2023_01-Bedrock_Updates-TrailOfBits.pdf](./2023_01-Bedrock_Updates-TrailOfBits.pdf) | ee96ff8585699b054c95c6ff4a2411ee9fedcc87 | op-contracts/v1.0.0 | +| 2023-01 | Sherlock | Bedrock: All contracts in `packages/contracts-bedrock/src` | Sherlock Bedrock Contest ([site](https://audits.sherlock.xyz/contests/38), [repo](https://github.com/sherlock-audit/2023-01-optimism)) | 3f4b3c328153a8aa03611158b6984d624b17c1d9 | op-contracts/v1.0.0 | +| 2023-03 | Sherlock | Bedrock Fixes: All contracts in `packages/contracts-bedrock/src` | Sherlock Bedrock Contest: Fix Review ([site](https://audits.sherlock.xyz/contests/63), [repo](https://github.com/sherlock-audit/2023-03-optimism)) | 9b9f78c6613c6ee53b93ca43c71bb74479f4b975 | op-contracts/v1.0.0 | +| 2023-12 | Trust | Superchain Config Upgrade: `SuperchainConfig.sol`, `L1CrossDomainMessenger.sol`, `L1ERC721Bridge.sol`, `L1StandardBridge.sol`, `OptimismPortal.sol`, `CrossDomainMessenger.sol`, `ERC721Bridge.sol`, `StandardBridge.sol` | [2023_12_SuperchainConfigUpgrade_Trust.pdf](./2023_12_SuperchainConfigUpgrade_Trust.pdf) | d1651bb22645ebd41ac4bb2ab4786f9a56fc1003 | op-contracts/v1.2.0 | +| 2024-02 | Runtime Verification | Pausability | [Kontrol Verification][kontrol] | | | +| 2024-02 | Cantina | MCP L1: `OptimismPortal.sol`, `L1CrossDomainMessenger.sol`, `L1StandardBridge.sol`, `L1ERC721Bridge.sol`, `OptimismMintableERC20Factory.sol`, `L2OutputOracle.sol`, `SystemConfig.sol` | [2024_02-MCP_L1-Cantina.pdf](./2024_02-MCP_L1-Cantina.pdf) | e6ef3a900c42c8722e72c2e2314027f85d12ced5 | op-contracts/v1.3.0 | +| 2024-03 | Sherlock | Fault Proofs | Sherlock Optimism Fault Proofs Contest ([site](https://audits.sherlock.xyz/contests/205), [repo](https://github.com/sherlock-audit/2024-02-optimism-2024)) | | | +| 2024-08 | Cantina | Fault proof MIPS: `MIPS.sol` | [2024_08_Fault-Proofs-MIPS_Cantina.pdf](./2024_08_Fault-Proofs-MIPS_Cantina.pdf) | 71b93116738ee98c9f8713b1a5dfe626ce06c1b2 | op-contracts/v1.4.0 | +| 2024-08 | Spearbit | Fault proof no-MIPS: All contracts in the `packages/contracts-bedrock/src/dispute` directory | [2024_08_Fault-Proofs-No-MIPS_Spearbit.pdf](./2024_08_Fault-Proofs-No-MIPS_Spearbit.pdf) | 1f7081798ce2d49b8643514663d10681cb853a3d | op-contracts/v1.6.0 | +| 2024-10 | 3Doc Security | Fault proof MIPS: `MIPS.sol` | [2024_10-Cannon-FGETFD-3DocSecurity.md](./2024_10-Cannon-FGETFD-3DocSecurity.md) | 52d0e60c16498ad4efec8798e3fc1b36b13f46a2 | op-contracts/v1.8.0 | +| 2024-12 | MiloTruck (independent) | DeputyPauseModule | [2024_12-DPM-MiloTruck.pdf](./2024_12-DPM-MiloTruck.pdf) | 2f17e6b67c61de5d8073d556272796d201bc740b | | +| 2024-12 | Radiant Labs | DeputyPauseModule | [2024_12-DPM-RadiantLabs.pdf](./2024_12-DPM-RadiantLabs.pdf) | 2f17e6b67c61de5d8073d556272796d201bc740b | | +| 2025-01 | Offbeat Labs | Incident Response Improvements | [2025_01-IRI-OffbeatLabs.pdf](./2025_01-IRI-OffbeatLabs.pdf) | 984bae9146398a2997ec13757bfe2438ca8f92eb | | +| 2025-01 | Spearbit | 64-bit Multithreaded Cannon: `MIPS64.sol` | [2025_01-MT-Cannon-Spearbit.pdf](./2025_01-MT-Cannon-Spearbit.pdf) | cc2715c3d6ebef374451b598f48980ad817e0a0e | | +| 2025-01 | Coinbase Protocol Security | Multi-thread & 64-bit Cannon | [2025_01-MT-Cannon-Base.pdf](./2025_01-MT-Cannon-Base.pdf) | b8c011f18c79d735e01168345fc1c6f02fac584f | | +| 2025-02 | Spearbit | Upgrade 13 | [2025_02-Upgrade13-Spearbit.pdf](./2025_02-Upgrade13-Spearbit.pdf) | 7d6d15437b7580b022f4c8c1ea9c0cd8d2e587e1 | op-contracts/v2.0.0 | +| 2025-03 | Spearbit | Interop Contracts | [2025_03-Interop-Contracts-Spearbit.pdf](./2025_03-Interop-Contracts-Spearbit.pdf) | 6c80f23ab3074b5c66ff06e390ae2448bd4d2240 | | +| 2025-03 | Wonderland | Interop Portal Contracts (u16) | [2025_03-Interop-Portal-Wonderland.pdf](./2025_03-Interop-Portal-Wonderland.pdf) | 9df1fc15d0bf0dc9464db249ce06424607d5f399 | op-contracts/v4.0.0 | +| 2025-04 | Aleph_v (independent) | op-program and op-challenger blob preimage handling | [2025_04-op-program-blob-handling-aleph_v.pdf](./2025_04-op-program-blob-handling-aleph_v.pdf) | 08d81d98237a3077fbc13fcd4b70f2e8d2e14115 | op-program/v1.6.0 | +| 2025-04 | Cantina (contest) | Interop Portal Contracts (u16) | [2025_04-Interop-Portal-Cantina.pdf](./2025_04-Interop-Portal-Cantina.pdf) | e4b921c9dbf8cd3a8db20ef4f15e0e2aa495fcc3 | op-contracts/v4.0.0 | +| 2025-05 | Spearbit | Interop Portal Contracts (u16) | [2025_05-Interop-Portal-Spearbit.pdf](./2025_05-Interop-Portal-Spearbit.pdf) | 7cd84fed9554193c2dcd683e1ff2d0e2605448f6 | op-contracts/v4.0.0 | +| 2025-05 | Coinbase Protocol Security | Cannon updates to support Go 1.23 and Kona | [2025_05-Cannon-Go-Updates-Coinbase.pdf](./2025_05-Cannon-Go-Updates-Coinbase.pdf) | 4c68444bc9b130e892b52cacf67b31f0424fb6d0 | | +| 2025-06 | Spearbit | Cannon Go 1.23 support fix review | [2025_06-Spearbit-Cannon-fix-review.pdf](./2025_06-Spearbit-Cannon-fix-review.pdf) | ffe3d5fed05cabf46a67ea00627a0959c0caa0b5 | op-contracts/v4.0.0 | +| 2025-06 | Radiant Labs | Cannon Go 1.24 support | [2025_06-Cannon-3DOC.pdf](./2025_06-Cannon-3DOC.pdf) | 689111fca9a10e6670ba0b5c7f1a549a212c855b | | +| 2025-05 | Spearbit | Upgrade 16 | [2025_05-Upgrade16-Spearbit.pdf](./2025_05-Upgrade16-Spearbit.pdf) / [Auditor hosted report][SpearbitMay25] | 54c19f6acb7a6d3505f884bae601733d3d54a3a6 | op-contracts/v4.0.0 | +| 2025-07 | Spearbit | VerifyOPCM | [2025_07-VerifyOPCM-Spearbit.pdf](./2025_07-VerifyOPCM-Spearbit.pdf) / [Auditor hosted report][SpearbitJuly25] | 731280c6fc0ad184d252e0fb1d0ad12b5f59fd60 | op-contracts/v4.0.0 | +| 2025-09 | Spearbit | U16a | [2025_09-U16a-Spearbit.pdf](./2025_09-U16a-Spearbit.pdf) | 475801690f7a451469ee4da87b5fe3c54c92f372 | op-contracts/v4.1.0 | +| 2025-10 | Spearbit | U17 | [2025_10-U17-Spearbit.pdf](./2025_10-U17-Spearbit.pdf) | aeed7033f7f739d8ecd4bd70a42ff09013bbc91e | op-contracts/v5.0.0 | +| 2025-10 | Spearbit | Revenue sharing and FeeVaults | [2025_10-Rev-Sharing-Spearbit.pdf](./2025_10-Rev-Sharing-Spearbit.pdf) / [2025_11-Rev-Sharing-Contracts-Upgrader.pdf](./2025_11-Rev-Sharing-Contracts-Upgrader.pdf) | f1fcd96406d895f37c2d1a422d50ea7dbd03a491 | op-contracts/v5.2.0+l2-fee-splitter-contracts | +| 2025-11 | Spearbit | Safer Safes | [2025_11-SaferSafes-Spearbit.pdf](./2025_11-SaferSafes-Spearbit.pdf) | cb54822c5e18925498f48d8677b71992bf402631 | op-safe-contracts/v1.0.0 | +| 2025-11 | Spearbit | Custom Gas Token | [2025_11-Custom-Gas-Token-Spearbit.pdf](./2025_11-Custom-Gas-Token-Spearbit.pdf) | 1f888ede1940fce20f71db89fc13039fdd96757e | op-contracts/v6.0.0 | diff --git a/go.mod b/go.mod index 6bfc9a7027f..791784f4b85 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/bmatcuk/doublestar/v4 v4.8.1 github.com/btcsuite/btcd v0.24.2 github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 + github.com/chelnak/ysmrr v0.6.0 github.com/cockroachdb/pebble v1.1.5 github.com/coder/websocket v1.8.13 github.com/consensys/gnark-crypto v0.18.0 @@ -21,7 +22,7 @@ require ( github.com/docker/docker v27.5.1+incompatible github.com/docker/go-connections v0.5.0 github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.4-0.20251001155152-4eb15ccedf7e - github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20251009180028-9b4658b9b7af + github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20251121143344-5ac16e0fbb00 github.com/ethereum/go-ethereum v1.16.3 github.com/fatih/color v1.18.0 github.com/fsnotify/fsnotify v1.9.0 @@ -194,7 +195,7 @@ require ( github.com/libp2p/go-yamux/v4 v4.0.1 // indirect github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mholt/archiver v3.1.1+incompatible // indirect github.com/miekg/dns v1.1.62 // indirect diff --git a/go.sum b/go.sum index 56cab806065..d4297507e1c 100644 --- a/go.sum +++ b/go.sum @@ -121,6 +121,8 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chelnak/ysmrr v0.6.0 h1:kMhO0oI02tl/9szvxrOE0yeImtrK4KQhER0oXu1K/iM= +github.com/chelnak/ysmrr v0.6.0/go.mod h1:56JSrmQgb7/7xoMvuD87h3PE/qW6K1+BQcrgWtVLTUo= github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= @@ -240,8 +242,8 @@ github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.4-0.20251001155152-4eb15c github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.4-0.20251001155152-4eb15ccedf7e/go.mod h1:DYj7+vYJ4cIB7zera9mv4LcAynCL5u4YVfoeUu6Wa+w= github.com/ethereum-optimism/op-geth v1.101604.0-synctest.0.0.20251208094937-ba6bdcfef423 h1:5xVkCCBRWkOt+bzVWL1p3mOwrpZLjxi/+yWUsja0E48= github.com/ethereum-optimism/op-geth v1.101604.0-synctest.0.0.20251208094937-ba6bdcfef423/go.mod h1:fCNAwDynfAP6EKsmLqwSDUDgi+GtJIir74Ui3fXXMps= -github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20251009180028-9b4658b9b7af h1:WWz0gJM/boaUImtJnROecPirAerKCLpAU4m6Tx0ArOg= -github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20251009180028-9b4658b9b7af/go.mod h1:NZ816PzLU1TLv1RdAvYAb6KWOj4Zm5aInT0YpDVml2Y= +github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20251121143344-5ac16e0fbb00 h1:TR5Y7B+5m63V0Dno7MHcFqv/XZByQzx/4THV1T1A7+U= +github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20251121143344-5ac16e0fbb00/go.mod h1:NZ816PzLU1TLv1RdAvYAb6KWOj4Zm5aInT0YpDVml2Y= github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= @@ -579,13 +581,12 @@ github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= @@ -1137,7 +1138,6 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/justfile b/justfile index 14665b5f726..ccc0c0e8d0a 100644 --- a/justfile +++ b/justfile @@ -1,3 +1,9 @@ +# Build all Rust binaries (release) for sysgo tests. +build-rust-release: + cd kona && cargo build --release --bin kona-node --bin kona-supervisor + cd op-rbuilder && cargo build --release -p op-rbuilder --bin op-rbuilder + cd rollup-boost && cargo build --release -p rollup-boost --bin rollup-boost + # Checks that TODO comments have corresponding issues. todo-checker: ./ops/scripts/todo-checker.sh diff --git a/kona b/kona new file mode 160000 index 00000000000..be9d6734eff --- /dev/null +++ b/kona @@ -0,0 +1 @@ +Subproject commit be9d6734effed58a906577b5198201f8c4cd3b4f diff --git a/kona/.gitignore b/kona-proofs/.gitignore similarity index 100% rename from kona/.gitignore rename to kona-proofs/.gitignore diff --git a/kona/justfile b/kona-proofs/justfile similarity index 100% rename from kona/justfile rename to kona-proofs/justfile diff --git a/kona-proofs/version.json b/kona-proofs/version.json new file mode 100644 index 00000000000..f99d3f5867b --- /dev/null +++ b/kona-proofs/version.json @@ -0,0 +1,5 @@ +{ + "version": "1.2.7", + "prestateHash": "0x0323914d3050e80c3d09da528be54794fde60cd26849cd3410dde0da7cd7d4fa", + "interopPrestateHash": "0x03f03018773fae0603f7c110ef1defa8d19b601b32ee530f9951987baec435b0" +} diff --git a/kona/version.json b/kona/version.json deleted file mode 100644 index d0d416dab10..00000000000 --- a/kona/version.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "1.2.4", - "prestateHash": "0x036d1def0a2815e1cb8370c17b4f6346cf83551d853f3bfe7b3ab4bfed935671", - "interopPrestateHash": "0x03bb838f039a019830e1b73b7fcddd28aa212ff78104a574bc5833e677bdb331" -} diff --git a/mise.toml b/mise.toml index 7eeb19d7bae..19f107a05bb 100644 --- a/mise.toml +++ b/mise.toml @@ -39,7 +39,7 @@ anvil = "1.2.3" codecov-uploader = "0.8.0" goreleaser-pro = "2.11.2" kurtosis = "1.8.1" -op-acceptor = "op-acceptor/v3.8.0" +op-acceptor = "op-acceptor/v3.8.1" # Fake dependencies # Put things here if you need to track versions of tools or projects that can't diff --git a/op-acceptance-tests/justfile b/op-acceptance-tests/justfile index a13688a4924..593d177143e 100644 --- a/op-acceptance-tests/justfile +++ b/op-acceptance-tests/justfile @@ -1,6 +1,6 @@ REPO_ROOT := `realpath ..` # path to the root of the optimism monorepo KURTOSIS_DIR := REPO_ROOT + "/kurtosis-devnet" -ACCEPTOR_VERSION := env_var_or_default("ACCEPTOR_VERSION", "v3.8.0") +ACCEPTOR_VERSION := env_var_or_default("ACCEPTOR_VERSION", "v3.8.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) @@ -46,7 +46,7 @@ acceptance-test devnet="" gate="base": echo "Building contracts (local build)..." cd {{REPO_ROOT}} echo " - Updating submodules..." - git submodule update --init --recursive + git submodule update --init --recursive --single-branch -j 8 echo " - Installing mise..." mise install cd packages/contracts-bedrock @@ -62,6 +62,10 @@ acceptance-test devnet="" gate="base": cd {{REPO_ROOT}} make cannon-prestates fi + + echo "Building Rust binaries (kona-node, kona-supervisor, op-rbuilder, rollup-boost)..." + cd {{REPO_ROOT}} + just build-rust-debug fi cd {{REPO_ROOT}}/op-acceptance-tests diff --git a/op-acceptance-tests/tests/base/disputegame_v2/init_test.go b/op-acceptance-tests/tests/base/disputegame_v2/init_test.go deleted file mode 100644 index 2b3ef1852e9..00000000000 --- a/op-acceptance-tests/tests/base/disputegame_v2/init_test.go +++ /dev/null @@ -1,11 +0,0 @@ -package disputegame_v2 - -import ( - "testing" - - "github.com/ethereum-optimism/optimism/op-devstack/presets" -) - -func TestMain(m *testing.M) { - presets.DoMain(m, presets.WithProofs(), presets.WithDisputeGameV2()) -} diff --git a/op-acceptance-tests/tests/batcher/batcher_test.go b/op-acceptance-tests/tests/batcher/batcher_test.go index 5cfc2b58036..accc3c10dbc 100644 --- a/op-acceptance-tests/tests/batcher/batcher_test.go +++ b/op-acceptance-tests/tests/batcher/batcher_test.go @@ -42,13 +42,14 @@ func TestBatcherFullChannelsAfterDowntime(gt *testing.T) { for i := 0; i < 5; i++ { l.Debug("Sequencing L2 block", "iteration", i, "parent", parent) - sequenceBlockWithL1Origin(t, ts_L2, parent, l1Origin, alice, cathrine, nonce) + sequenceBlockWithL1Origin(t, ts_L2, parent, l1Origin, cathrine, alice, nonce) nonce++ parent = sys.L2CL.HeadBlockRef(types.LocalUnsafe).Hash + sys.L2EL.WaitForPendingNonceMatch(cathrine.Address(), nonce, 10, 1*time.Second) + sys.AdvanceTime(time.Second * 2) - time.Sleep(20 * time.Millisecond) // failed to force-include tx: type: 2 sender; err: nonce too high } l.Debug("Sequencing L1 block", "iteration_j", j) @@ -95,12 +96,12 @@ func TestBatcherFullChannelsAfterDowntime(gt *testing.T) { spew.Dump(status) } -func sequenceBlockWithL1Origin(t devtest.T, ts apis.TestSequencerControlAPI, parent common.Hash, l1Origin common.Hash, alice *dsl.EOA, cathrine *dsl.EOA, nonce uint64) { +func sequenceBlockWithL1Origin(t devtest.T, ts apis.TestSequencerControlAPI, parent common.Hash, l1Origin common.Hash, from *dsl.EOA, to *dsl.EOA, nonce uint64) { require.NoError(t, ts.New(t.Ctx(), seqtypes.BuildOpts{Parent: parent, L1Origin: &l1Origin})) // include simple transfer tx in opened block { - to := cathrine.PlanTransfer(alice.Address(), eth.OneWei) + to := from.PlanTransfer(to.Address(), eth.OneWei) opt := txplan.Combine(to, txplan.WithStaticNonce(nonce)) ptx := txplan.NewPlannedTx(opt) signed_tx, err := ptx.Signed.Eval(t.Ctx()) diff --git a/op-acceptance-tests/tests/flashblocks/flashblocks_stream_test.go b/op-acceptance-tests/tests/flashblocks/flashblocks_stream_test.go index 323ffe51759..377e201d689 100644 --- a/op-acceptance-tests/tests/flashblocks/flashblocks_stream_test.go +++ b/op-acceptance-tests/tests/flashblocks/flashblocks_stream_test.go @@ -1,6 +1,3 @@ -//go:build !ci - -// use a tag prefixed with "!". Such tag ensures that the default behaviour of this test would be to be built/run even when the go toolchain (go test) doesn't specify any tag filter. package flashblocks import ( diff --git a/op-acceptance-tests/tests/proofs/cannon/init_test.go b/op-acceptance-tests/tests/proofs/cannon/init_test.go index de6c2bab333..bdba42260d3 100644 --- a/op-acceptance-tests/tests/proofs/cannon/init_test.go +++ b/op-acceptance-tests/tests/proofs/cannon/init_test.go @@ -14,7 +14,6 @@ func TestMain(m *testing.M) { presets.WithProofs(), stack.MakeCommon(sysgo.WithDeployerOptions(sysgo.WithJovianAtGenesis)), presets.WithSafeDBEnabled(), - presets.WithCannonKona(), // Requires access to a challenger config which only sysgo provides // These tests would also be exceptionally slow on real L1s presets.WithCompatibleTypes(compat.SysGo), diff --git a/op-acceptance-tests/tests/base/disputegame_v2/smoke_test.go b/op-acceptance-tests/tests/proofs/cannon/smoke_test.go similarity index 98% rename from op-acceptance-tests/tests/base/disputegame_v2/smoke_test.go rename to op-acceptance-tests/tests/proofs/cannon/smoke_test.go index b93ad395624..ed890f8baad 100644 --- a/op-acceptance-tests/tests/base/disputegame_v2/smoke_test.go +++ b/op-acceptance-tests/tests/proofs/cannon/smoke_test.go @@ -1,4 +1,4 @@ -package disputegame_v2 +package cannon import ( "testing" diff --git a/op-acceptance-tests/tests/sync/unsafe_only/init_test.go b/op-acceptance-tests/tests/sync/follow_l2/init_test.go similarity index 65% rename from op-acceptance-tests/tests/sync/unsafe_only/init_test.go rename to op-acceptance-tests/tests/sync/follow_l2/init_test.go index f2e144bf063..47024bf47db 100644 --- a/op-acceptance-tests/tests/sync/unsafe_only/init_test.go +++ b/op-acceptance-tests/tests/sync/follow_l2/init_test.go @@ -1,4 +1,4 @@ -package unsafe_only +package follow_l2 import ( "testing" @@ -8,11 +8,9 @@ import ( ) func TestMain(m *testing.M) { - presets.DoMain(m, presets.WithSingleChainTwoVerifiers(), - presets.WithExecutionLayerSyncOnVerifiers(), + presets.DoMain(m, presets.WithSingleChainTwoVerifiersFollowL2(), presets.WithReqRespSyncDisabled(), presets.WithNoDiscovery(), presets.WithCompatibleTypes(compat.SysGo), - presets.WithUnsafeOnly(), ) } diff --git a/op-acceptance-tests/tests/sync/follow_l2/sync_test.go b/op-acceptance-tests/tests/sync/follow_l2/sync_test.go new file mode 100644 index 00000000000..11c38822e34 --- /dev/null +++ b/op-acceptance-tests/tests/sync/follow_l2/sync_test.go @@ -0,0 +1,199 @@ +package follow_l2 + +import ( + "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-devstack/stack/match" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" + "github.com/ethereum-optimism/optimism/op-test-sequencer/sequencer/seqtypes" + "github.com/ethereum/go-ethereum/common" +) + +func TestFollowL2_Safe_Finalized_CurrentL1(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewSingleChainTwoVerifiersWithoutCheck(t) + logger := t.Logger() + + // Takes about 2 minutes for L1 finalization + attempts := 70 + target := uint64(3) + + // L2CL is the sequencer with CL follow source, derivation disabled + // L2CLB is the verifier without follow source, derivation enabled + // L2CLC is the verifier with CL follow source, derivation disabled + // All verifiers must eventually advance unsafe, safe, finalized + checkMatchedAll := func(lvl types.SafetyLevel) { + dsl.CheckAll(t, + sys.L2CL.ReachedFn(lvl, target, attempts), + sys.L2CLB.ReachedFn(lvl, target, attempts), + sys.L2CLC.ReachedFn(lvl, target, attempts), + ) + dsl.CheckAll(t, + sys.L2CLB.MatchedFn(sys.L2CL, lvl, attempts), + sys.L2CLB.MatchedFn(sys.L2CLC, lvl, attempts), + ) + } + + checkMatchedAll(types.LocalUnsafe) + logger.Info("Unsafe head advanced due to CLP2P", "target", target) + + checkMatchedAll(types.LocalSafe) + logger.Info("Safe head followed source", "target", target) + + checkMatchedAll(types.Finalized) + logger.Info("Finalized head followed source", "target", target) + + attempts = 10 + dsl.CheckAll(t, + sys.L2CLC.CurrentL1MatchedFn(sys.L2CLB, attempts), + sys.L2CL.CurrentL1MatchedFn(sys.L2CLB, attempts), + ) + logger.Info("CurrentL1 followed source", "currentL1", sys.L2CL.SyncStatus().CurrentL1, "currentL1C", sys.L2CLC.SyncStatus().CurrentL1) +} + +func TestFollowL2_ReorgRecovery(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewSingleChainTwoVerifiersWithoutCheck(t) + require := t.Require() + logger := t.Logger() + ctx := t.Ctx() + + // L2CLB is the verifier without follow source, derivation enabled + + ts := sys.TestSequencer.Escape().ControlAPI(sys.L1Network.ChainID()) + cl := sys.L1Network.Escape().L1CLNode(match.FirstL1CL) + + // Pass the L1 genesis + sys.L1Network.WaitForBlock() + + // Stop auto advancing L1 + sys.ControlPlane.FakePoSState(cl.ID(), stack.Stop) + + startL1Block := sys.L1EL.BlockRefByLabel(eth.Unsafe) + + require.Eventually(func() bool { + // Advance single L1 block + require.NoError(ts.New(ctx, seqtypes.BuildOpts{Parent: common.Hash{}})) + require.NoError(ts.Next(ctx)) + l1head := sys.L1EL.BlockRefByLabel(eth.Unsafe) + l2Safe := sys.L2ELB.BlockRefByLabel(eth.Safe) + + logger.Info("l1 info", "l1_head", l1head, "l1_origin", l2Safe.L1Origin, "l2Safe", l2Safe) + // Wait until safe L2 block has L1 origin point after the startL1Block + return l2Safe.Number > 0 && l2Safe.L1Origin.Number > startL1Block.Number + }, 120*time.Second, 2*time.Second) + + l2BlockBeforeReorg := sys.L2ELB.BlockRefByLabel(eth.Safe) + logger.Info("Target L2 Block to reorg", "l2", l2BlockBeforeReorg, "l1_origin", l2BlockBeforeReorg.L1Origin) + + // Make sure verifier safe head is also advanced from reorgL2Block or matched + sys.L2ELB.Reached(eth.Safe, l2BlockBeforeReorg.Number, 3) + + // Reorg L1 block which safe block L1 Origin points to + l1BlockBeforeReorg := sys.L1EL.BlockRefByNumber(l2BlockBeforeReorg.L1Origin.Number) + logger.Info("Triggering L1 reorg", "l1", l1BlockBeforeReorg) + require.NoError(ts.New(ctx, seqtypes.BuildOpts{Parent: l1BlockBeforeReorg.ParentHash})) + require.NoError(ts.Next(ctx)) + + // Start advancing L1 + sys.ControlPlane.FakePoSState(cl.ID(), stack.Start) + + // Make sure L1 reorged + sys.L1EL.WaitForBlockNumber(l1BlockBeforeReorg.Number) + l1BlockAfterReorg := sys.L1EL.BlockRefByNumber(l1BlockBeforeReorg.Number) + logger.Info("Triggered L1 reorg", "l1", l1BlockAfterReorg) + require.NotEqual(l1BlockAfterReorg.Hash, l1BlockBeforeReorg.Hash) + + // Need to poll until the L2CL detects L1 Reorg and trigger L2 Reorg + // What happens: + // L2CL detects L1 Reorg and reset the pipeline. op-node example logs: "reset: detected L1 reorg" + // L2ELB detects L2 Reorg and reorgs. op-geth example logs: "Chain reorg detected" + sys.L2ELB.ReorgTriggered(l2BlockBeforeReorg, 30) + l2BlockAfterReorg := sys.L2ELB.BlockRefByNumber(l2BlockBeforeReorg.Number) + require.NotEqual(l2BlockAfterReorg.Hash, l2BlockBeforeReorg.Hash) + logger.Info("Triggered L2 reorg", "l2", l2BlockAfterReorg) + + attempts := 30 + dsl.CheckAll(t, + sys.L2CL.MatchedFn(sys.L2CLB, types.LocalUnsafe, attempts), + sys.L2CLC.MatchedFn(sys.L2CLB, types.LocalUnsafe, attempts), + sys.L2CL.MatchedFn(sys.L2CLB, types.LocalSafe, attempts), + sys.L2CLC.MatchedFn(sys.L2CLB, types.LocalSafe, attempts), + ) +} + +func TestFollowL2_WithoutCLP2P(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewSingleChainTwoVerifiersWithoutCheck(t) + require := t.Require() + logger := t.Logger() + + attempts := 20 + target := uint64(3) + + // L2CLB is the verifier without follow source, derivation enabled + sys.L2CLB.Advanced(types.LocalUnsafe, target, attempts) + + // The test's primary target is the L2CLC, with follow source and derivation disabled + // Normally there should be delta between safe head between unsafe head + status := sys.L2CLC.SyncStatus() + require.NotEqual(status.LocalSafeL2, status.UnsafeL2) + + logger.Info("Disconnect CLP2P") + // L2CLC is the verifier with follow source, derivation disabled + // Disconnect CLP2P of verifier which follow source is enabled + sys.L2CLC.DisconnectPeer(sys.L2CLB) + sys.L2CLB.DisconnectPeer(sys.L2CLC) + sys.L2CLC.DisconnectPeer(sys.L2CL) + sys.L2CL.DisconnectPeer(sys.L2CLC) + + // Advance few safe blocks + sys.L2CLC.Advanced(types.LocalSafe, target, attempts) + sys.L2CLC.Matched(sys.L2CLB, types.LocalSafe, attempts) + + // Make sure the safe head reaches non-moving unsafe head + sys.L2CLC.Reached(types.LocalSafe, sys.L2CLC.UnsafeHead().BlockRef.Number, attempts) + // The only data source for L2CLC is the follow source. + // L2CLC unsafe head will only be advancing with safe head together + status = sys.L2CLC.SyncStatus() + require.Equal(status.LocalSafeL2, status.UnsafeL2) + sys.L2CLC.Advanced(types.LocalSafe, target, attempts) + + // Advance few safe blocks + sys.L2CLC.Advanced(types.LocalSafe, target, attempts) + + // Check once again that the unsafe head is moving together with safe head + status = sys.L2CLC.SyncStatus() + require.Equal(status.LocalSafeL2, status.UnsafeL2) + sys.L2CLC.Advanced(types.LocalSafe, target, attempts) + + // Recover CLP2P + logger.Info("Recover CLP2P") + sys.L2CLC.ConnectPeer(sys.L2CLB) + sys.L2CLB.ConnectPeer(sys.L2CLC) + sys.L2CLC.ConnectPeer(sys.L2CL) + sys.L2CL.ConnectPeer(sys.L2CLC) + + // Sequencer unsafe payload will arrive to the verifier, triggering EL sync and filling in the unsafe gap + dsl.CheckAll(t, + // Match with sequencer with derivation disabled + sys.L2CLC.MatchedFn(sys.L2CL, types.LocalSafe, attempts), + sys.L2CLC.MatchedFn(sys.L2CL, types.LocalUnsafe, attempts), + // Match with other verifier with derivation enabled + sys.L2CLC.MatchedFn(sys.L2CLB, types.LocalSafe, attempts), + sys.L2CLC.MatchedFn(sys.L2CLB, types.LocalUnsafe, attempts), + ) + + t.Cleanup(func() { + sys.L2CLC.ConnectPeer(sys.L2CLB) + sys.L2CLB.ConnectPeer(sys.L2CLC) + sys.L2CLC.ConnectPeer(sys.L2CL) + sys.L2CL.ConnectPeer(sys.L2CLC) + }) +} diff --git a/op-acceptance-tests/tests/sync/unsafe_only/sync_test.go b/op-acceptance-tests/tests/sync/unsafe_only/sync_test.go deleted file mode 100644 index aec7fc54e13..00000000000 --- a/op-acceptance-tests/tests/sync/unsafe_only/sync_test.go +++ /dev/null @@ -1,103 +0,0 @@ -package unsafe_only - -import ( - "testing" - - "github.com/ethereum-optimism/optimism/op-devstack/devtest" - "github.com/ethereum-optimism/optimism/op-devstack/presets" - "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" -) - -func TestUnsafeOnly_VerifierUnsafeGapClosed(gt *testing.T) { - t := devtest.SerialT(gt) - sys := presets.NewSingleChainTwoVerifiersWithoutCheck(t) - require := t.Require() - attempts := 10 - - sys.L2CL.AdvancedUnsafe(3, attempts) - sys.L2EL.MatchedUnsafe(sys.L2ELB, attempts) - sys.L2CL.MatchedUnsafe(sys.L2CLB, attempts) - - // Case 1: Closing the gap starting from genesis - sys.L2CLB.Stop() - sys.L2ELB.DisconnectPeerWith(sys.L2EL) - // Wipe EL to genesis - sys.L2ELB.Stop() - sys.L2ELB.Start() - // Check EL rewinded to genesis. Unsafe gap introduced - sys.L2ELB.UnsafeHead().IsGenesis() - // Verifier CL triggers EL Sync to close the gap including genesis - sys.L2CLB.Start() - sys.L2CLB.ConnectPeer(sys.L2CL) - sys.L2ELB.PeerWith(sys.L2EL) - // Gap is closed - sys.L2CLB.MatchedUnsafe(sys.L2CL, attempts) - sys.L2ELB.MatchedUnsafe(sys.L2EL, attempts) - - // Case 2: Closing the gap not starting from genesis - sys.L2CLB.DisconnectPeer(sys.L2CL) - sys.L2CL.AdvancedUnsafe(3, attempts) - sys.L2CLB.NotAdvanced(types.LocalUnsafe, 3) - // Turn back the CLP2P - sys.L2CLB.ConnectPeer(sys.L2CL) - // gap is closed again - sys.L2CLB.MatchedUnsafe(sys.L2CL, attempts) - sys.L2ELB.MatchedUnsafe(sys.L2EL, attempts) - - // Derivation did not happen - sys.L2CL.SafeHead().IsGenesis() - - // Derivation happened at the second verifier - require.Greater(sys.L2CLC.SafeHead().BlockRef.Number, uint64(0)) - - t.Cleanup(func() { - sys.L2ELB.Start() - sys.L2ELB.PeerWith(sys.L2EL) - sys.L2CLB.Start() - sys.L2CLB.ConnectPeer(sys.L2CL) - }) -} - -func TestUnsafeOnly_SequencerRestart(gt *testing.T) { - t := devtest.SerialT(gt) - sys := presets.NewSingleChainTwoVerifiersWithoutCheck(t) - require := t.Require() - - attempts := 10 - - sys.L2CL.AdvancedUnsafe(3, attempts) - sys.L2EL.MatchedUnsafe(sys.L2ELB, attempts) - sys.L2CL.MatchedUnsafe(sys.L2CLB, attempts) - - // Stop the sequencer - sys.L2CL.Stop() - sys.L2ELB.NotAdvancedUnsafe(3) - - // Restart the sequencer - sys.L2CL.Start() - // Sequencer produces blocks again - sys.L2CL.AdvancedUnsafe(3, attempts) - - // Derivation did not happen at sequencer - sys.L2CL.SafeHead().IsGenesis() - - // Stop the sequencer with API - sys.L2CL.StopSequencer() - sys.L2ELB.NotAdvancedUnsafe(3) - - // Restart the sequencer with API - sys.L2CL.StartSequencer() - // Sequencer produces blocks again - sys.L2CL.AdvancedUnsafe(3, attempts) - - // Derivation did not happen at sequencer - sys.L2CL.SafeHead().IsGenesis() - - // Derivation happened at the second verifier - safeHeadNum := sys.L2CLC.SafeHead().BlockRef.Number - require.Greater(safeHeadNum, uint64(0)) - - t.Cleanup(func() { - sys.L2CL.Start() - }) -} diff --git a/op-acceptance-tests/tests/sync_tester/sync_tester_unsafe_only_ext/init_test.go b/op-acceptance-tests/tests/sync_tester/sync_tester_unsafe_only_ext/init_test.go deleted file mode 100644 index d566ebef267..00000000000 --- a/op-acceptance-tests/tests/sync_tester/sync_tester_unsafe_only_ext/init_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package sync_tester_unsafe_only_ext - -import ( - "testing" - - "github.com/ethereum-optimism/optimism/op-acceptance-tests/tests/sync_tester/sync_tester_ext_el" - bss "github.com/ethereum-optimism/optimism/op-batcher/batcher" - "github.com/ethereum-optimism/optimism/op-devstack/compat" - "github.com/ethereum-optimism/optimism/op-devstack/presets" - "github.com/ethereum-optimism/optimism/op-devstack/stack" - "github.com/ethereum-optimism/optimism/op-devstack/sysgo" - "github.com/ethereum-optimism/optimism/op-node/chaincfg" - "github.com/ethereum-optimism/optimism/op-service/eth" -) - -func TestMain(m *testing.M) { - // Target op-sepolia - networkName := "op-sepolia" - config, _ := sync_tester_ext_el.GetNetworkPreset(networkName) - chainCfg := chaincfg.ChainByName(networkName) - presets.DoMain(m, - presets.WithExternalELWithSuperchainRegistry(config), - // CL connected to sync tester EL is verifier - presets.WithExecutionLayerSyncOnVerifiers(), - // Make sync tester EL mock EL Sync - presets.WithELSyncActive(), - // Only rely on EL sync for unsafe gap filling - presets.WithReqRespSyncDisabled(), - presets.WithNoDiscovery(), - presets.WithCompatibleTypes(compat.SysGo), - presets.WithUnsafeOnly(), - stack.MakeCommon(sysgo.WithBatcherOption(func(id stack.L2BatcherID, cfg *bss.CLIConfig) { - // For stopping derivation, not to advance safe heads - cfg.Stopped = true - })), - // Sync tester EL at genesis - presets.WithSyncTesterELInitialState(eth.FCUState{ - Latest: chainCfg.Genesis.L2.Number, - Safe: chainCfg.Genesis.L2.Number, - Finalized: chainCfg.Genesis.L2.Number, - }), - ) -} diff --git a/op-acceptance-tests/tests/sync_tester/sync_tester_unsafe_only_ext/sync_test.go b/op-acceptance-tests/tests/sync_tester/sync_tester_unsafe_only_ext/sync_test.go deleted file mode 100644 index 74bee285dfb..00000000000 --- a/op-acceptance-tests/tests/sync_tester/sync_tester_unsafe_only_ext/sync_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package sync_tester_unsafe_only_ext - -import ( - "testing" - - "github.com/ethereum-optimism/optimism/op-devstack/devtest" - "github.com/ethereum-optimism/optimism/op-devstack/presets" - "github.com/ethereum-optimism/optimism/op-service/eth" - "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" -) - -func TestSyncTesterUnsafeOnlyReachUnsafeTip(gt *testing.T) { - t := devtest.SerialT(gt) - require := t.Require() - - sys := presets.NewMinimalExternalEL(t) - sys.L2EL.UnsafeHead().IsGenesis() - - // Check external read only EL is advancing - sys.L2ELReadOnly.Advanced(eth.Unsafe, 3) - - unsafeTip := sys.L2ELReadOnly.UnsafeHead() - unsafeTipNum := unsafeTip.BlockRef.Number - startNum := unsafeTipNum - 3 - // Trigger and finish EL Sync - for i := startNum; i <= unsafeTipNum; i++ { - sys.L2CL.SignalTarget(sys.L2ELReadOnly, i) - } - - sys.L2EL.Reached(eth.Unsafe, unsafeTipNum, 5) - require.Equal(unsafeTip.BlockRef, sys.L2EL.UnsafeHead().BlockRef) - - // Make sure the unsafe only CL can still advance unsafe - target := unsafeTipNum + 3 - sys.L2ELReadOnly.Reached(eth.Unsafe, target, 3) - for i := unsafeTipNum + 1; i <= target; i++ { - sys.L2CL.SignalTarget(sys.L2ELReadOnly, i) - } - sys.L2EL.Reached(eth.Unsafe, target, 5) - sys.L2CL.Reached(types.LocalUnsafe, target, 5) - - // Check unsafe gap is closed - target = unsafeTipNum + 9 - sys.L2ELReadOnly.Reached(eth.Unsafe, target, 6) - for i := unsafeTipNum + 6; i <= target; i++ { - sys.L2CL.SignalTarget(sys.L2ELReadOnly, i) - } - sys.L2EL.Reached(eth.Unsafe, target, 5) - sys.L2CL.Reached(types.LocalUnsafe, target, 5) -} diff --git a/op-conductor/rpc/ws/flashblocks_handler.go b/op-conductor/rpc/ws/flashblocks_handler.go index b4b159c5eb2..068f60de61c 100644 --- a/op-conductor/rpc/ws/flashblocks_handler.go +++ b/op-conductor/rpc/ws/flashblocks_handler.go @@ -117,7 +117,12 @@ func (h *Handler) Start(ctx context.Context) error { func (h *Handler) Stop() { // Signal the hub to stop if it exists if h.hub != nil { - close(h.hub.done) + select { + case <-h.hub.done: + // already closed + default: + close(h.hub.done) + } } // Cancel the rollup boost context if it exists @@ -142,6 +147,15 @@ func (h *Handler) Stop() { } h.log.Info("WebSocket server closed") } + + // Wait for hub shutdown to complete to avoid leaking goroutines + if h.hub != nil { + select { + case <-h.hub.stopped: + case <-time.After(5 * time.Second): + h.log.Warn("Timed out waiting for hub shutdown") + } + } } // BroadcastMessage sends a message to all connected WebSocket clients @@ -196,6 +210,11 @@ func (h *Handler) listenToRollupBoost(ctx context.Context) { case <-ctx.Done(): return default: + // If not leader, avoid pulling messages to reduce allocation pressure + if !h.isLeaderFn(ctx) { + time.Sleep(500 * time.Millisecond) + continue + } // Try to connect if not connected indefinitely if h.rollupBoostConn == nil { h.log.Info("reconnecting to rollup boost WebSocket", "url", h.cfg.RollupBoostWsURL) diff --git a/op-conductor/rpc/ws/server.go b/op-conductor/rpc/ws/server.go index 159eb474b24..3fac0b8d297 100644 --- a/op-conductor/rpc/ws/server.go +++ b/op-conductor/rpc/ws/server.go @@ -17,6 +17,9 @@ type Hub struct { // Registered clients clients map[*Client]bool + // Protects access to clients map + mu sync.Mutex + // Register requests from the clients register chan *Client @@ -29,6 +32,9 @@ type Hub struct { // Signal to stop the hub done chan struct{} + // Signals that the hub has fully stopped + stopped chan struct{} + // Logger log log.Logger @@ -47,6 +53,7 @@ func newHub(m metrics.Metricer) *Hub { unregister: make(chan *Client), clients: make(map[*Client]bool), done: make(chan struct{}), + stopped: make(chan struct{}), log: log.New("component", "websocket-hub"), metrics: m, } @@ -54,6 +61,9 @@ func newHub(m metrics.Metricer) *Hub { // registerClient adds a client to the hub and updates metrics func (h *Hub) registerClient(client *Client) { + h.mu.Lock() + defer h.mu.Unlock() + h.clients[client] = true clientCount := len(h.clients) h.log.Info("Client registered with hub", "totalClients", clientCount) @@ -66,6 +76,9 @@ func (h *Hub) registerClient(client *Client) { // unregisterClient removes a client from the hub, closes it, and updates metrics func (h *Hub) unregisterClient(client *Client) { + h.mu.Lock() + defer h.mu.Unlock() + if _, ok := h.clients[client]; ok { delete(h.clients, client) client.Close() @@ -85,7 +98,14 @@ func (h *Hub) run() { select { case <-h.done: // Close all remaining client connections + h.mu.Lock() + var remaining []*Client for client := range h.clients { + remaining = append(remaining, client) + } + h.mu.Unlock() + + for _, client := range remaining { h.unregisterClient(client) } h.metrics.RecordWebSocketClientCount(0) @@ -93,6 +113,7 @@ func (h *Hub) run() { if h.callbacks.OnShutdown != nil { h.callbacks.OnShutdown() } + close(h.stopped) return case client := <-h.register: h.registerClient(client) @@ -101,19 +122,27 @@ func (h *Hub) run() { case message := <-h.broadcast: successCount := 0 dropCount := 0 + var toClose []*Client + h.mu.Lock() for client := range h.clients { select { case client.send <- message: // Message sent successfully successCount++ default: - // Channel is full, client is likely slow/dead - // The ping mechanism will detect and clean up dead clients - h.log.Debug("Failed to send message to client, channel full") + // Channel is full, client is likely slow/dead; mark for close + h.log.Warn("Client send channel full, dropping and closing client") dropCount++ + toClose = append(toClose, client) } } + h.mu.Unlock() + + for _, client := range toClose { + h.unregisterClient(client) + } + if dropCount > 0 { h.log.Warn("Failed to send message to all clients, dropped", "successCount", successCount, "dropCount", dropCount) } @@ -188,7 +217,12 @@ func (h *Handler) serveWs(w http.ResponseWriter, r *http.Request) { func (h *Handler) readPump(client *Client) { defer func() { // Unregister the client when the read pump exits - h.hub.unregister <- client + select { + case h.hub.unregister <- client: + case <-h.hub.done: + // Hub already stopping; unregister directly to avoid blocking + h.hub.unregisterClient(client) + } h.log.Info("WebSocket read pump exited, client unregistered") }() diff --git a/op-deployer/pkg/deployer/bootstrap/implementations.go b/op-deployer/pkg/deployer/bootstrap/implementations.go index f876cef273d..1432b4b2822 100644 --- a/op-deployer/pkg/deployer/bootstrap/implementations.go +++ b/op-deployer/pkg/deployer/bootstrap/implementations.go @@ -95,21 +95,17 @@ func (c *ImplementationsConfig) Check() error { if c.DisputeGameFinalityDelaySeconds == 0 { return errors.New("dispute game finality delay in seconds must be specified") } - // Check V2 fault game parameters only if V2 dispute games feature is enabled - deployV2Games := deployer.IsDevFeatureEnabled(c.DevFeatureBitmap, deployer.DeployV2DisputeGamesDevFlag) - if deployV2Games { - if c.FaultGameMaxGameDepth == 0 { - return errors.New("fault game max game depth must be specified when V2 dispute games feature is enabled") - } - if c.FaultGameSplitDepth == 0 { - return errors.New("fault game split depth must be specified when V2 dispute games feature is enabled") - } - if c.FaultGameClockExtension == 0 { - return errors.New("fault game clock extension must be specified when V2 dispute games feature is enabled") - } - if c.FaultGameMaxClockDuration == 0 { - return errors.New("fault game max clock duration must be specified when V2 dispute games feature is enabled") - } + if c.FaultGameMaxGameDepth == 0 { + return errors.New("fault game max game depth must be specified") + } + if c.FaultGameSplitDepth == 0 { + return errors.New("fault game split depth must be specified") + } + if c.FaultGameClockExtension == 0 { + return errors.New("fault game clock extension must be specified") + } + if c.FaultGameMaxClockDuration == 0 { + return errors.New("fault game max clock duration must be specified") } if c.SuperchainConfigProxy == (common.Address{}) { return errors.New("superchain config proxy must be specified") diff --git a/op-deployer/pkg/deployer/bootstrap/implementations_test.go b/op-deployer/pkg/deployer/bootstrap/implementations_test.go index c332553207d..32359609f07 100644 --- a/op-deployer/pkg/deployer/bootstrap/implementations_test.go +++ b/op-deployer/pkg/deployer/bootstrap/implementations_test.go @@ -84,6 +84,10 @@ func testImplementations(t *testing.T, forkRPCURL string) { L1ProxyAdminOwner: proxyAdminOwner, Challenger: common.Address{'C'}, CacheDir: testCacheDir, + FaultGameMaxGameDepth: standard.DisputeMaxGameDepth, + FaultGameSplitDepth: standard.DisputeSplitDepth, + FaultGameClockExtension: standard.DisputeClockExtension, + FaultGameMaxClockDuration: standard.DisputeMaxClockDuration, }) require.NoError(t, err) return out diff --git a/op-deployer/pkg/deployer/forge/binary.go b/op-deployer/pkg/deployer/forge/binary.go index aac8f86b62c..4e630f90023 100644 --- a/op-deployer/pkg/deployer/forge/binary.go +++ b/op-deployer/pkg/deployer/forge/binary.go @@ -254,7 +254,7 @@ func (b *StandardBin) downloadBinary(ctx context.Context, dest string) error { if err := ioutil.Untar(tmpDir, tr); err != nil { return fmt.Errorf("failed to untar: %w", err) } - if err := os.Rename(path.Join(tmpDir, "forge"), path.Join(dest, "forge")); err != nil { + if err := ioutil.SafeRename(path.Join(tmpDir, "forge"), path.Join(dest, "forge")); err != nil { return fmt.Errorf("failed to move binary: %w", err) } if err := os.Chmod(path.Join(dest, "forge"), 0o755); err != nil { diff --git a/op-deployer/pkg/deployer/integration_test/apply_test.go b/op-deployer/pkg/deployer/integration_test/apply_test.go index d217c7db74e..cefade057cc 100644 --- a/op-deployer/pkg/deployer/integration_test/apply_test.go +++ b/op-deployer/pkg/deployer/integration_test/apply_test.go @@ -121,6 +121,10 @@ func TestEndToEndBootstrapApply(t *testing.T) { CacheDir: testCacheDir, Logger: lgr, Challenger: common.Address{'C'}, + FaultGameMaxGameDepth: standard.DisputeMaxGameDepth, + FaultGameSplitDepth: standard.DisputeSplitDepth, + FaultGameClockExtension: standard.DisputeClockExtension, + FaultGameMaxClockDuration: standard.DisputeMaxClockDuration, }) require.NoError(t, err) @@ -216,12 +220,6 @@ func TestEndToEndBootstrapApplyWithUpgrade(t *testing.T) { Logger: lgr, Challenger: common.Address{'C'}, } - if deployer.IsDevFeatureEnabled(tt.devFeature, deployer.DeployV2DisputeGamesDevFlag) { - cfg.FaultGameMaxGameDepth = standard.DisputeMaxGameDepth - cfg.FaultGameSplitDepth = standard.DisputeSplitDepth - cfg.FaultGameClockExtension = standard.DisputeClockExtension - cfg.FaultGameMaxClockDuration = standard.DisputeMaxClockDuration - } if deployer.IsDevFeatureEnabled(tt.devFeature, deployer.OpcmV2DevFlag) { cfg.DevFeatureBitmap = deployer.OpcmV2DevFlag } @@ -458,114 +456,91 @@ func TestApplyGenesisStrategy(t *testing.T) { func TestProofParamOverrides(t *testing.T) { op_e2e.InitParallel(t) - for _, useV2 := range []bool{true, false} { - t.Run(fmt.Sprintf("useV2=%v", useV2), func(t *testing.T) { - op_e2e.InitParallel(t) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - opts, intent, st := setupGenesisChain(t, devnet.DefaultChainID) - devFeatureBitmap := common.Hash{} - if useV2 { - devFeatureBitmap = deployer.DeployV2DisputeGamesDevFlag - } - intent.GlobalDeployOverrides = map[string]any{ - "faultGameWithdrawalDelay": standard.WithdrawalDelaySeconds + 1, - "preimageOracleMinProposalSize": standard.MinProposalSizeBytes + 1, - "preimageOracleChallengePeriod": standard.ChallengePeriodSeconds + 1, - "proofMaturityDelaySeconds": standard.ProofMaturityDelaySeconds + 1, - "disputeGameFinalityDelaySeconds": standard.DisputeGameFinalityDelaySeconds + 1, - "mipsVersion": standard.MIPSVersion, // Contract enforces a valid value be used - "respectedGameType": standard.DisputeGameType, // This must be set to the permissioned game - "faultGameAbsolutePrestate": common.Hash{'A', 'B', 'S', 'O', 'L', 'U', 'T', 'E'}, - "faultGameMaxDepth": standard.DisputeMaxGameDepth + 1, - "faultGameSplitDepth": standard.DisputeSplitDepth + 1, - "faultGameClockExtension": standard.DisputeClockExtension + 1, - "faultGameMaxClockDuration": standard.DisputeMaxClockDuration + 1, - "dangerouslyAllowCustomDisputeParameters": true, - "devFeatureBitmap": devFeatureBitmap, - } + opts, intent, st := setupGenesisChain(t, devnet.DefaultChainID) + intent.GlobalDeployOverrides = map[string]any{ + "faultGameWithdrawalDelay": standard.WithdrawalDelaySeconds + 1, + "preimageOracleMinProposalSize": standard.MinProposalSizeBytes + 1, + "preimageOracleChallengePeriod": standard.ChallengePeriodSeconds + 1, + "proofMaturityDelaySeconds": standard.ProofMaturityDelaySeconds + 1, + "disputeGameFinalityDelaySeconds": standard.DisputeGameFinalityDelaySeconds + 1, + "mipsVersion": standard.MIPSVersion, // Contract enforces a valid value be used + "respectedGameType": standard.DisputeGameType, // This must be set to the permissioned game + "faultGameAbsolutePrestate": common.Hash{'A', 'B', 'S', 'O', 'L', 'U', 'T', 'E'}, + "faultGameMaxDepth": standard.DisputeMaxGameDepth + 1, + "faultGameSplitDepth": standard.DisputeSplitDepth + 1, + "faultGameClockExtension": standard.DisputeClockExtension + 1, + "faultGameMaxClockDuration": standard.DisputeMaxClockDuration + 1, + "dangerouslyAllowCustomDisputeParameters": true, + "devFeatureBitmap": common.Hash{}, + } - require.NoError(t, deployer.ApplyPipeline(ctx, opts)) + require.NoError(t, deployer.ApplyPipeline(ctx, opts)) - allocs := st.L1StateDump.Data.Accounts - chainState := st.Chains[0] + allocs := st.L1StateDump.Data.Accounts - uint64Caster := func(t *testing.T, val any) common.Hash { - return common.BigToHash(new(big.Int).SetUint64(val.(uint64))) - } + uint64Caster := func(t *testing.T, val any) common.Hash { + return common.BigToHash(new(big.Int).SetUint64(val.(uint64))) + } - pdgImpl := chainState.PermissionedDisputeGameImpl - if useV2 { - pdgImpl = st.ImplementationsDeployment.PermissionedDisputeGameV2Impl - } - tests := []struct { - name string - caster func(t *testing.T, val any) common.Hash - address common.Address - }{ - { - "faultGameWithdrawalDelay", - uint64Caster, - st.ImplementationsDeployment.DelayedWethImpl, - }, - { - "preimageOracleMinProposalSize", - uint64Caster, - st.ImplementationsDeployment.PreimageOracleImpl, - }, - { - "preimageOracleChallengePeriod", - uint64Caster, - st.ImplementationsDeployment.PreimageOracleImpl, - }, - { - "proofMaturityDelaySeconds", - uint64Caster, - st.ImplementationsDeployment.OptimismPortalImpl, - }, - { - "disputeGameFinalityDelaySeconds", - uint64Caster, - st.ImplementationsDeployment.AnchorStateRegistryImpl, - }, - { - "faultGameMaxDepth", - uint64Caster, - pdgImpl, - }, - { - "faultGameSplitDepth", - uint64Caster, - pdgImpl, - }, - { - "faultGameClockExtension", - uint64Caster, - pdgImpl, - }, - { - "faultGameMaxClockDuration", - uint64Caster, - pdgImpl, - }, - { - "faultGameAbsolutePrestate", - func(t *testing.T, val any) common.Hash { - return val.(common.Hash) - }, - pdgImpl, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if useV2 && tt.name == "faultGameAbsolutePrestate" { - t.Skip("absolute prestate is not an immutable in V2 contracts") - } - checkImmutable(t, allocs, tt.address, tt.caster(t, intent.GlobalDeployOverrides[tt.name])) - }) - } + pdgImpl := st.ImplementationsDeployment.PermissionedDisputeGameV2Impl + tests := []struct { + name string + caster func(t *testing.T, val any) common.Hash + address common.Address + }{ + { + "faultGameWithdrawalDelay", + uint64Caster, + st.ImplementationsDeployment.DelayedWethImpl, + }, + { + "preimageOracleMinProposalSize", + uint64Caster, + st.ImplementationsDeployment.PreimageOracleImpl, + }, + { + "preimageOracleChallengePeriod", + uint64Caster, + st.ImplementationsDeployment.PreimageOracleImpl, + }, + { + "proofMaturityDelaySeconds", + uint64Caster, + st.ImplementationsDeployment.OptimismPortalImpl, + }, + { + "disputeGameFinalityDelaySeconds", + uint64Caster, + st.ImplementationsDeployment.AnchorStateRegistryImpl, + }, + { + "faultGameMaxDepth", + uint64Caster, + pdgImpl, + }, + { + "faultGameSplitDepth", + uint64Caster, + pdgImpl, + }, + { + "faultGameClockExtension", + uint64Caster, + pdgImpl, + }, + { + "faultGameMaxClockDuration", + uint64Caster, + pdgImpl, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + checkImmutable(t, allocs, tt.address, tt.caster(t, intent.GlobalDeployOverrides[tt.name])) }) } } @@ -826,10 +801,6 @@ func runEndToEndBootstrapAndApplyUpgradeTest(t *testing.T, afactsFS foundry.Stat } // Then run the OPCM upgrade - var cannonKonaPrestate common.Hash - if deployer.IsDevFeatureEnabled(implementationsConfig.DevFeatureBitmap, deployer.CannonKonaDevFlag) { - cannonKonaPrestate = common.Hash{'K', 'O', 'N', 'A'} - } t.Run("upgrade opcm", func(t *testing.T) { if deployer.IsDevFeatureEnabled(implementationsConfig.DevFeatureBitmap, deployer.OpcmV2DevFlag) { t.Skip("Skipping OPCM upgrade for OPCM V2") @@ -842,7 +813,7 @@ func runEndToEndBootstrapAndApplyUpgradeTest(t *testing.T, afactsFS foundry.Stat { SystemConfigProxy: common.HexToAddress("034edD2A225f7f429A63E0f1D2084B9E0A93b538"), CannonPrestate: common.Hash{'C', 'A', 'N', 'N', 'O', 'N'}, - CannonKonaPrestate: cannonKonaPrestate, + CannonKonaPrestate: common.Hash{'K', 'O', 'N', 'A'}, }, }, } @@ -960,8 +931,8 @@ func mustEncodeGameArgs(absolutePrestate common.Hash, proposer, challenger commo // This is 96 bytes: bytes32 + address (left-padded to 32) + address (left-padded to 32) result := make([]byte, 96) copy(result[0:32], absolutePrestate[:]) - copy(result[44:64], proposer[:]) // address at offset 32, left-padded (12 zero bytes + 20 address bytes) - copy(result[76:96], challenger[:]) // address at offset 64, left-padded (12 zero bytes + 20 address bytes) + copy(result[44:64], proposer[:]) // address at offset 32, left-padded (12 zero bytes + 20 address bytes) + copy(result[76:96], challenger[:]) // address at offset 64, left-padded (12 zero bytes + 20 address bytes) return result } diff --git a/op-deployer/pkg/deployer/integration_test/cli/upgrade_test.go b/op-deployer/pkg/deployer/integration_test/cli/upgrade_test.go index ea20a606f1c..0f1832a0290 100644 --- a/op-deployer/pkg/deployer/integration_test/cli/upgrade_test.go +++ b/op-deployer/pkg/deployer/integration_test/cli/upgrade_test.go @@ -54,7 +54,11 @@ func TestCLIUpgrade(t *testing.T) { version: "v4.1.0", forkBlock: 9165154, // one block past the opcm deployment block }, - // TODO: Add v5.0.0 test case + { + contractTag: standard.ContractsV500Tag, + version: "v5.0.0", + forkBlock: 9629972, // one block past the opcm deployment block + }, } for _, tc := range testCases { @@ -88,7 +92,7 @@ func TestCLIUpgrade(t *testing.T) { configData, err := json.MarshalIndent(testConfig, "", " ") require.NoError(t, err) - require.NoError(t, os.WriteFile(configFile, configData, 0644)) + require.NoError(t, os.WriteFile(configFile, configData, 0o644)) // Run full cli command to write calldata to outfile output := runner.ExpectSuccess(t, []string{ @@ -116,7 +120,6 @@ func TestCLIUpgrade(t *testing.T) { dataHex := hex.EncodeToString(dump[0].Data) require.True(t, strings.HasPrefix(dataHex, "ff2dd5a1"), "calldata should have opcm.upgrade fcn selector ff2dd5a1, got: %s", dataHex[:8]) - }) } } diff --git a/op-deployer/pkg/deployer/manage/add_game_type_test.go b/op-deployer/pkg/deployer/manage/add_game_type_test.go index 0f7aa80435a..7349f0d669f 100644 --- a/op-deployer/pkg/deployer/manage/add_game_type_test.go +++ b/op-deployer/pkg/deployer/manage/add_game_type_test.go @@ -66,6 +66,10 @@ func TestAddGameType(t *testing.T) { CacheDir: testCacheDir, Logger: lgr, Challenger: common.Address{'C'}, + FaultGameMaxGameDepth: standard.DisputeMaxGameDepth, + FaultGameSplitDepth: standard.DisputeSplitDepth, + FaultGameClockExtension: standard.DisputeClockExtension, + FaultGameMaxClockDuration: standard.DisputeMaxClockDuration, }) require.NoError(t, err) diff --git a/op-deployer/pkg/deployer/opcm/dispute_game.go b/op-deployer/pkg/deployer/opcm/dispute_game.go index 4346fea42a1..8ec4fd67137 100644 --- a/op-deployer/pkg/deployer/opcm/dispute_game.go +++ b/op-deployer/pkg/deployer/opcm/dispute_game.go @@ -10,7 +10,6 @@ import ( type DeployDisputeGameInput struct { Release string - UseV2 bool GameKind string GameType uint32 AbsolutePrestate common.Hash diff --git a/op-deployer/pkg/deployer/opcm/dispute_game_factory.go b/op-deployer/pkg/deployer/opcm/dispute_game_factory.go index a7138ca95e6..6fe4839abaa 100644 --- a/op-deployer/pkg/deployer/opcm/dispute_game_factory.go +++ b/op-deployer/pkg/deployer/opcm/dispute_game_factory.go @@ -11,7 +11,6 @@ type SetDisputeGameImplInput struct { AnchorStateRegistry common.Address GameType uint32 GameArgs []byte - UseV2 bool } func SetDisputeGameImpl( diff --git a/op-deployer/pkg/deployer/opcm/dispute_game_factory_test.go b/op-deployer/pkg/deployer/opcm/dispute_game_factory_test.go index 8765d971b45..c4b5047e49c 100644 --- a/op-deployer/pkg/deployer/opcm/dispute_game_factory_test.go +++ b/op-deployer/pkg/deployer/opcm/dispute_game_factory_test.go @@ -60,7 +60,6 @@ func TestSetDisputeGameImpl(t *testing.T) { input := SetDisputeGameImplInput{ Factory: factoryAddr, - UseV2: len(gameArgs) > 0, Impl: common.Address{'I'}, GameType: 999, AnchorStateRegistry: common.Address{}, // Do not set as respected game type as we aren't authorized diff --git a/op-deployer/pkg/deployer/opcm/dispute_game_test.go b/op-deployer/pkg/deployer/opcm/dispute_game_test.go index 235ccbf42e0..0c51c89c0e6 100644 --- a/op-deployer/pkg/deployer/opcm/dispute_game_test.go +++ b/op-deployer/pkg/deployer/opcm/dispute_game_test.go @@ -1,7 +1,6 @@ package opcm import ( - "fmt" "math/big" "testing" @@ -32,36 +31,31 @@ func TestDeployDisputeGame(t *testing.T) { vmAddr := deployDisputeGameScriptVM(t, host) - for _, useV2 := range []bool{false, true} { - t.Run(fmt.Sprintf("useV2=%v", useV2), func(t *testing.T) { - input := DeployDisputeGameInput{ - Release: "dev", - UseV2: useV2, - VmAddress: vmAddr, - GameKind: "PermissionedDisputeGame", - GameType: 1, - AbsolutePrestate: common.Hash{'A'}, - MaxGameDepth: big.NewInt(int64(standard.DisputeMaxGameDepth)), - SplitDepth: big.NewInt(int64(standard.DisputeSplitDepth)), - ClockExtension: standard.DisputeClockExtension, - MaxClockDuration: standard.DisputeMaxClockDuration, - DelayedWethProxy: common.Address{'D'}, - AnchorStateRegistryProxy: common.Address{'A'}, - L2ChainId: big.NewInt(69), - Proposer: common.Address{'P'}, - Challenger: common.Address{'C'}, - } - - script, err := NewDeployDisputeGameScript(host) - require.NoError(t, err) - - output, err := script.Run(input) - require.NoError(t, err) - - require.NotEmpty(t, output.DisputeGameImpl) - require.NotEmpty(t, host.GetCode(output.DisputeGameImpl)) - }) + input := DeployDisputeGameInput{ + Release: "dev", + VmAddress: vmAddr, + GameKind: "PermissionedDisputeGame", + GameType: 1, + AbsolutePrestate: common.Hash{'A'}, + MaxGameDepth: big.NewInt(int64(standard.DisputeMaxGameDepth)), + SplitDepth: big.NewInt(int64(standard.DisputeSplitDepth)), + ClockExtension: standard.DisputeClockExtension, + MaxClockDuration: standard.DisputeMaxClockDuration, + DelayedWethProxy: common.Address{'D'}, + AnchorStateRegistryProxy: common.Address{'A'}, + L2ChainId: big.NewInt(69), + Proposer: common.Address{'P'}, + Challenger: common.Address{'C'}, } + + script, err := NewDeployDisputeGameScript(host) + require.NoError(t, err) + + output, err := script.Run(input) + require.NoError(t, err) + + require.NotEmpty(t, output.DisputeGameImpl) + require.NotEmpty(t, host.GetCode(output.DisputeGameImpl)) } func deployDisputeGameScriptVM(t *testing.T, host *script.Host) common.Address { diff --git a/op-deployer/pkg/deployer/pipeline/dispute_games.go b/op-deployer/pkg/deployer/pipeline/dispute_games.go index 8a6850e5328..566750b9f1f 100644 --- a/op-deployer/pkg/deployer/pipeline/dispute_games.go +++ b/op-deployer/pkg/deployer/pipeline/dispute_games.go @@ -88,24 +88,20 @@ func deployDisputeGame( } lgr.Info("vm deployed", "vmAddr", vmAddr) - useV2 := st.ImplementationsDeployment.PermissionedDisputeGameV2Impl != (common.Address{}) - var gameArgs []byte - if useV2 { // Only set game args if V2 contracts are used. - args := gameargs.GameArgs{ - AbsolutePrestate: game.DisputeAbsolutePrestate, - Vm: vmAddr, - AnchorStateRegistry: thisState.OpChainContracts.AnchorStateRegistryProxy, - Weth: thisState.OpChainContracts.DelayedWethPermissionedGameProxy, - L2ChainID: eth.ChainIDFromBytes32(thisIntent.ID), - Proposer: thisIntent.Roles.Proposer, - Challenger: thisIntent.Roles.Challenger, - } - if game.DisputeGameType == uint32(gameTypes.PermissionedGameType) { - gameArgs = args.PackPermissioned() - } else { - gameArgs = args.PackPermissionless() - } + args := gameargs.GameArgs{ + AbsolutePrestate: game.DisputeAbsolutePrestate, + Vm: vmAddr, + AnchorStateRegistry: thisState.OpChainContracts.AnchorStateRegistryProxy, + Weth: thisState.OpChainContracts.DelayedWethPermissionedGameProxy, + L2ChainID: eth.ChainIDFromBytes32(thisIntent.ID), + Proposer: thisIntent.Roles.Proposer, + Challenger: thisIntent.Roles.Challenger, + } + if game.DisputeGameType == uint32(gameTypes.PermissionedGameType) { + gameArgs = args.PackPermissioned() + } else { + gameArgs = args.PackPermissionless() } lgr.Info("deploying dispute game") @@ -113,7 +109,6 @@ func deployDisputeGame( out, err := env.Scripts.DeployDisputeGame.Run( opcm.DeployDisputeGameInput{ Release: "dev", - UseV2: useV2, VmAddress: vmAddr, GameKind: "FaultDisputeGame", GameType: game.DisputeGameType, @@ -136,7 +131,6 @@ func deployDisputeGame( lgr.Info("setting dispute game impl on factory", "respected", game.MakeRespected) sdgiInput := opcm.SetDisputeGameImplInput{ - UseV2: useV2, Factory: thisState.OpChainContracts.DisputeGameFactoryProxy, Impl: out.DisputeGameImpl, GameType: game.DisputeGameType, diff --git a/op-deployer/pkg/deployer/standard/standard.go b/op-deployer/pkg/deployer/standard/standard.go index bcfb6062db6..210400df753 100644 --- a/op-deployer/pkg/deployer/standard/standard.go +++ b/op-deployer/pkg/deployer/standard/standard.go @@ -42,8 +42,8 @@ const ( ContractsV300Tag = "op-contracts/v3.0.0" ContractsV400Tag = "op-contracts/v4.0.0-rc.7" ContractsV410Tag = "op-contracts/v4.1.0" - ContractsV500Tag = "op-contracts/v5.0.0-rc.2" - CurrentTag = ContractsV410Tag + ContractsV500Tag = "op-contracts/v5.0.0" + CurrentTag = ContractsV500Tag ) var L1FeesDepositor = common.HexToAddress("0xed9B99a703BaD32AC96FDdc313c0652e379251Fd") diff --git a/op-deployer/pkg/deployer/upgrade/flags.go b/op-deployer/pkg/deployer/upgrade/flags.go index 90f39ea8b00..917775f9977 100644 --- a/op-deployer/pkg/deployer/upgrade/flags.go +++ b/op-deployer/pkg/deployer/upgrade/flags.go @@ -7,6 +7,7 @@ import ( v300 "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/upgrade/v3_0_0" v400 "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/upgrade/v4_0_0" v410 "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/upgrade/v4_1_0" + v500 "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/upgrade/v5_0_0" oplog "github.com/ethereum-optimism/optimism/op-service/log" "github.com/urfave/cli/v2" ) @@ -73,6 +74,17 @@ var Commands = cli.Commands{ Action: UpgradeCLI(v410.DefaultUpgrader), }, // TODO: Add v5.0.0 test case + &cli.Command{ + Name: "v5.0.0", + Usage: "upgrades a chain to version v5.0.0 (U17)", + Flags: append([]cli.Flag{ + deployer.L1RPCURLFlag, + ConfigFlag, + OverrideArtifactsURLFlag, + OutfileFlag, + }, oplog.CLIFlags(deployer.EnvVarPrefix)...), + Action: UpgradeCLI(v500.DefaultUpgrader), + }, &cli.Command{ Name: "embedded", Usage: "upgrades a chain to version of contracts embedded in op-deployer", diff --git a/op-deployer/pkg/deployer/upgrade/v5_0_0/upgrade.go b/op-deployer/pkg/deployer/upgrade/v5_0_0/upgrade.go new file mode 100644 index 00000000000..6df7a280843 --- /dev/null +++ b/op-deployer/pkg/deployer/upgrade/v5_0_0/upgrade.go @@ -0,0 +1,24 @@ +// Package v5_0_0 implements the upgrade to v5.0.0 (U17). 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 v5_0_0 + +import ( + "encoding/json" + + "github.com/ethereum-optimism/optimism/op-chain-ops/script" + "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/artifacts" + 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 artifacts.CreateHttpLocator("b112b16f8939fbb732c0693de3d3bd1e8e3e2f0771f91d5ab300a6c9b7b1af73") +} + +var DefaultUpgrader = new(Upgrader) diff --git a/op-devstack/dsl/l2_cl.go b/op-devstack/dsl/l2_cl.go index 862394fc522..f478410f17f 100644 --- a/op-devstack/dsl/l2_cl.go +++ b/op-devstack/dsl/l2_cl.go @@ -434,3 +434,19 @@ func (cl *L2CLNode) UnsafeHead() *BlockRefResult { func (cl *L2CLNode) SafeHead() *BlockRefResult { return &BlockRefResult{T: cl.t, BlockRef: cl.HeadBlockRef(types.CrossSafe)} } + +func (cl *L2CLNode) CurrentL1MatchedFn(refNode *L2CLNode, attempts int) CheckFunc { + return func() error { + return retry.Do0(cl.ctx, attempts, &retry.FixedStrategy{Dur: 1 * time.Second}, + func() error { + currentL1 := cl.SyncStatus().CurrentL1 + ref := refNode.SyncStatus().CurrentL1 + if currentL1 == ref { + cl.log.Info("CurrentL1 reached", "currentL1", currentL1) + return nil + } + cl.log.Info("Chain sync status", "currentL1", currentL1.Number, "ref", ref) + return fmt.Errorf("expected currentL1 to match") + }) + } +} diff --git a/op-devstack/dsl/l2_el.go b/op-devstack/dsl/l2_el.go index 19b76925dd2..b226386aeed 100644 --- a/op-devstack/dsl/l2_el.go +++ b/op-devstack/dsl/l2_el.go @@ -357,6 +357,34 @@ func (el *L2ELNode) MatchedUnsafe(refNode SyncStatusProvider, attempts int) { el.Matched(refNode, types.LocalUnsafe, attempts) } +// WaitForPendingNonceMatchFn returns a lambda that waits for the pending nonce of an account to match the provided reference nonce +func (el *L2ELNode) WaitForPendingNonceMatchFn(account common.Address, nonce uint64, attempts int, duration time.Duration) CheckFunc { + return func() error { + logger := el.log.With("id", el.inner.ID(), "account", account) + logger.Debug("Expecting pending nonce to match with reference nonce", "nonce", nonce) + return retry.Do0(el.ctx, attempts, &retry.FixedStrategy{Dur: duration}, + func() error { + baseNonce, err := el.inner.EthClient().PendingNonceAt(el.ctx, account) + if err != nil { + return fmt.Errorf("failed to get pending nonce from node: %w", err) + } + + if baseNonce == nonce { + logger.Debug("Pending nonce matched", "nonce", baseNonce) + return nil + } + + logger.Debug("Pending nonce mismatch", "node nonce", baseNonce, "nonce", nonce) + return fmt.Errorf("expected pending nonce to match: node nonce=%d, reference nonce=%d", baseNonce, nonce) + }) + } +} + +// WaitForPendingNonceMatch waits for the pending nonce of an account to match the reference nonce +func (el *L2ELNode) WaitForPendingNonceMatch(account common.Address, nonce uint64, attempts int, duration time.Duration) { + el.require.NoError(el.WaitForPendingNonceMatchFn(account, nonce, attempts, duration)()) +} + func (el *L2ELNode) UnsafeHead() *BlockRefResult { return &BlockRefResult{T: el.t, BlockRef: el.BlockRefByLabel(eth.Unsafe)} } @@ -365,6 +393,10 @@ func (el *L2ELNode) SafeHead() *BlockRefResult { return &BlockRefResult{T: el.t, BlockRef: el.BlockRefByLabel(eth.Safe)} } +func (el *L2ELNode) FinalizedHead() *BlockRefResult { + return &BlockRefResult{T: el.t, BlockRef: el.BlockRefByLabel(eth.Finalized)} +} + type BlockRefResult struct { T devtest.T BlockRef eth.L2BlockRef diff --git a/op-devstack/presets/cl_config.go b/op-devstack/presets/cl_config.go index 41109fc23e3..3760cb3ab9a 100644 --- a/op-devstack/presets/cl_config.go +++ b/op-devstack/presets/cl_config.go @@ -56,12 +56,3 @@ func WithNoDiscovery() stack.CommonOption { cfg.NoDiscovery = true }))) } - -func WithUnsafeOnly() stack.CommonOption { - return stack.MakeCommon( - sysgo.WithGlobalL2CLOption(sysgo.L2CLOptionFn( - func(_ devtest.P, id stack.L2CLNodeID, cfg *sysgo.L2CLConfig) { - cfg.SequencerUnsafeOnly = true - cfg.VerifierUnsafeOnly = true - }))) -} diff --git a/op-devstack/presets/disputegame_v2.go b/op-devstack/presets/disputegame_v2.go deleted file mode 100644 index 46490f82b1a..00000000000 --- a/op-devstack/presets/disputegame_v2.go +++ /dev/null @@ -1,27 +0,0 @@ -package presets - -import ( - "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer" - "github.com/ethereum-optimism/optimism/op-devstack/stack" - "github.com/ethereum-optimism/optimism/op-devstack/sysgo" -) - -func WithDisputeGameV2() stack.CommonOption { - return stack.MakeCommon(sysgo.WithDeployerOptions(sysgo.WithDevFeatureEnabled(deployer.DeployV2DisputeGamesDevFlag))) -} - -func WithCannonKona() stack.CommonOption { - return stack.Combine( - // Enable dev features required - WithCannonKonaFeatureEnabled(), - // Add cannon-kona game type - stack.MakeCommon(sysgo.WithCannonKonaGameTypeAdded()), - ) -} - -func WithCannonKonaFeatureEnabled() stack.CommonOption { - return stack.MakeCommon(sysgo.WithDeployerOptions( - sysgo.WithDevFeatureEnabled(deployer.DeployV2DisputeGamesDevFlag), // Required for cannon kona - sysgo.WithDevFeatureEnabled(deployer.CannonKonaDevFlag), - )) -} diff --git a/op-devstack/presets/proof.go b/op-devstack/presets/proof.go index c40c855e022..0a2d3d30e2c 100644 --- a/op-devstack/presets/proof.go +++ b/op-devstack/presets/proof.go @@ -29,7 +29,6 @@ func WithAddedGameType(gameType gameTypes.GameType) stack.CommonOption { if gameType == gameTypes.CannonKonaGameType { opts = stack.Combine( opts, - WithCannonKonaFeatureEnabled(), stack.MakeCommon(sysgo.WithChallengerCannonKonaEnabled()), ) } diff --git a/op-devstack/presets/singlechain_twoverifiers.go b/op-devstack/presets/singlechain_twoverifiers.go index 76af5ef8bb7..898af17b4bc 100644 --- a/op-devstack/presets/singlechain_twoverifiers.go +++ b/op-devstack/presets/singlechain_twoverifiers.go @@ -14,10 +14,8 @@ type SingleChainTwoVerifiers struct { L2ELC *dsl.L2ELNode L2CLC *dsl.L2CLNode -} -func WithSingleChainTwoVerifiers() stack.CommonOption { - return stack.MakeCommon(sysgo.DefaultSingleChainTwoVerifiersSystem(&sysgo.DefaultSingleChainTwoVerifiersSystemIDs{})) + TestSequencer *dsl.TestSequencer } func NewSingleChainTwoVerifiersWithoutCheck(t devtest.T) *SingleChainTwoVerifiers { @@ -41,6 +39,11 @@ func NewSingleChainTwoVerifiersWithoutCheck(t devtest.T) *SingleChainTwoVerifier SingleChainMultiNode: *singleChainMultiNode, L2ELC: dsl.NewL2ELNode(verifierEL, orch.ControlPlane()), L2CLC: dsl.NewL2CLNode(verifierCL, orch.ControlPlane()), + TestSequencer: dsl.NewTestSequencer(system.TestSequencer(match.Assume(t, match.FirstTestSequencer))), } return preset } + +func WithSingleChainTwoVerifiersFollowL2() stack.CommonOption { + return stack.MakeCommon(sysgo.DefaultSingleChainTwoVerifiersFollowL2System(&sysgo.DefaultSingleChainTwoVerifiersSystemIDs{})) +} diff --git a/op-devstack/shared/challenger/challenger.go b/op-devstack/shared/challenger/challenger.go index 2b0fa928834..68ea35f2376 100644 --- a/op-devstack/shared/challenger/challenger.go +++ b/op-devstack/shared/challenger/challenger.go @@ -79,12 +79,12 @@ func applyCannonKonaConfig(c *config.Config, rollupCfgs []*rollup.Config, l1Gene if err := applyVmConfig(root, &c.CannonKona, c.Datadir, rollupCfgs, l1Genesis, l2Geneses); err != nil { return err } - c.CannonKona.Server = root + "kona/bin/kona-host" + c.CannonKona.Server = root + "kona-proofs/bin/kona-host" absRoot, err := filepath.Abs(root) if err != nil { return fmt.Errorf("failed to get absolute path to prestate dir: %w", err) } - c.CannonKonaAbsolutePreStateBaseURL, err = url.Parse("file:" + absRoot + "/kona/prestates") + c.CannonKonaAbsolutePreStateBaseURL, err = url.Parse("file:" + absRoot + "/kona-proofs/prestates") if err != nil { return fmt.Errorf("failed to create kona prestates url: %w", err) } diff --git a/op-devstack/sysgo/l2_cl.go b/op-devstack/sysgo/l2_cl.go index f64c245953b..c6da8e5f143 100644 --- a/op-devstack/sysgo/l2_cl.go +++ b/op-devstack/sysgo/l2_cl.go @@ -37,9 +37,7 @@ type L2CLConfig struct { // NoDiscovery is the flag to enable/disable discovery NoDiscovery bool - // UnsafeOnly is the flag to disable derivation - SequencerUnsafeOnly bool - VerifierUnsafeOnly bool + FollowSource string } func L2CLSequencer() L2CLOption { @@ -54,24 +52,23 @@ func L2CLIndexing() L2CLOption { }) } -func L2CLVerifierDisableUnsafeOnly() L2CLOption { +func L2CLFollowSource(source string) L2CLOption { return L2CLOptionFn(func(p devtest.P, id stack.L2CLNodeID, cfg *L2CLConfig) { - cfg.VerifierUnsafeOnly = false + cfg.FollowSource = source }) } func DefaultL2CLConfig() *L2CLConfig { return &L2CLConfig{ - SequencerSyncMode: nodeSync.CLSync, - VerifierSyncMode: nodeSync.CLSync, - SafeDBPath: "", - IsSequencer: false, - IndexingMode: false, - EnableReqRespSync: true, - UseReqRespSync: true, - NoDiscovery: false, - SequencerUnsafeOnly: false, - VerifierUnsafeOnly: false, + SequencerSyncMode: nodeSync.CLSync, + VerifierSyncMode: nodeSync.CLSync, + SafeDBPath: "", + IsSequencer: false, + IndexingMode: false, + EnableReqRespSync: true, + UseReqRespSync: true, + NoDiscovery: false, + FollowSource: "", } } @@ -119,3 +116,14 @@ func WithL2CLNode(l2CLID stack.L2CLNodeID, l1CLID stack.L1CLNodeID, l1ELID stack return WithOpNode(l2CLID, l1CLID, l1ELID, l2ELID, opts...) } } + +func WithL2CLNodeFollowL2(l2CLID stack.L2CLNodeID, l1CLID stack.L1CLNodeID, l1ELID stack.L1ELNodeID, l2ELID stack.L2ELNodeID, l2FollowSourceID stack.L2CLNodeID, opts ...L2CLOption) stack.Option[*Orchestrator] { + switch os.Getenv("DEVSTACK_L2CL_KIND") { + case "kona": + panic("kona does not support following") + case "supernode": + panic("supernode does not support following") + default: + return WithOpNodeFollowL2(l2CLID, l1CLID, l1ELID, l2ELID, l2FollowSourceID, opts...) + } +} diff --git a/op-devstack/sysgo/l2_cl_kona.go b/op-devstack/sysgo/l2_cl_kona.go index b9dfe61c327..dce2891ed6e 100644 --- a/op-devstack/sysgo/l2_cl_kona.go +++ b/op-devstack/sysgo/l2_cl_kona.go @@ -254,10 +254,13 @@ func WithKonaNode(l2CLID stack.L2CLNodeID, l1CLID stack.L1CLNodeID, l1ELID stack ) } - execPath := os.Getenv("KONA_NODE_EXEC_PATH") - p.Require().NotEmpty(execPath, "KONA_NODE_EXEC_PATH environment variable must be set") - _, err = os.Stat(execPath) - p.Require().NotErrorIs(err, os.ErrNotExist, "executable must exist") + execPath, err := EnsureRustBinary(p, RustBinarySpec{ + SrcDir: "kona", + Package: "kona-node", + Binary: "kona-node", + }) + p.Require().NoError(err, "prepare kona-node binary") + p.Require().NotEmpty(execPath, "kona-node binary path resolved") k := &KonaNode{ id: l2CLID, diff --git a/op-devstack/sysgo/l2_cl_opnode.go b/op-devstack/sysgo/l2_cl_opnode.go index 2831a4666e2..8358c57be4c 100644 --- a/op-devstack/sysgo/l2_cl_opnode.go +++ b/op-devstack/sysgo/l2_cl_opnode.go @@ -162,8 +162,25 @@ func (n *OpNode) Stop() { n.opNode = nil } -func WithOpNode(l2CLID stack.L2CLNodeID, l1CLID stack.L1CLNodeID, l1ELID stack.L1ELNodeID, l2ELID stack.L2ELNodeID, opts ...L2CLOption) stack.Option[*Orchestrator] { +func WithOpNodeFollowL2(l2CLID stack.L2CLNodeID, l1CLID stack.L1CLNodeID, l1ELID stack.L1ELNodeID, l2ELID stack.L2ELNodeID, l2FollowSourceID stack.L2CLNodeID, opts ...L2CLOption) stack.Option[*Orchestrator] { return stack.AfterDeploy(func(orch *Orchestrator) { + followSource := func(orch *Orchestrator) string { + p := orch.P().WithCtx(stack.ContextWithID(orch.P().Ctx(), l2CLID)) + l2CLFollowSource, ok := orch.l2CLs.Get(l2FollowSourceID) + p.Require().True(ok, "l2 CL Follow Source required") + return l2CLFollowSource.UserRPC() + }(orch) + opts = append(opts, L2CLFollowSource(followSource)) + withOpNode(l2CLID, l1CLID, l1ELID, l2ELID, opts...)(orch) + }) +} + +func WithOpNode(l2CLID stack.L2CLNodeID, l1CLID stack.L1CLNodeID, l1ELID stack.L1ELNodeID, l2ELID stack.L2ELNodeID, opts ...L2CLOption) stack.Option[*Orchestrator] { + return stack.AfterDeploy(withOpNode(l2CLID, l1CLID, l1ELID, l2ELID, opts...)) +} + +func withOpNode(l2CLID stack.L2CLNodeID, l1CLID stack.L1CLNodeID, l1ELID stack.L1ELNodeID, l2ELID stack.L2ELNodeID, opts ...L2CLOption) func(orch *Orchestrator) { + return func(orch *Orchestrator) { p := orch.P().WithCtx(stack.ContextWithID(orch.P().Ctx(), l2CLID)) require := p.Require() @@ -195,16 +212,12 @@ func WithOpNode(l2CLID stack.L2CLNodeID, l1CLID stack.L1CLNodeID, l1ELID stack.L L2CLOptionBundle(opts).Apply(p, l2CLID, cfg) // apply specific options syncMode := cfg.VerifierSyncMode - unsafeOnly := false if cfg.IsSequencer { syncMode = cfg.SequencerSyncMode // Sanity check, to navigate legacy sync-mode test assumptions. // Can't enable ELSync on the sequencer or it will never start sequencing because // ELSync needs to receive gossip from the sequencer to drive the sync p.Require().NotEqual(nodeSync.ELSync, syncMode, "sequencer cannot use EL sync") - unsafeOnly = cfg.SequencerUnsafeOnly - } else { - unsafeOnly = cfg.VerifierUnsafeOnly } jwtPath, jwtSecret := orch.writeDefaultJWT() @@ -286,6 +299,9 @@ func WithOpNode(l2CLID stack.L2CLNodeID, l1CLID stack.L1CLNodeID, l1ELID stack.L L2EngineAddr: l2EL.EngineRPC(), L2EngineJWTSecret: jwtSecret, }, + L2FollowSource: &config.L2FollowSourceConfig{ + L2RPCAddr: cfg.FollowSource, + }, Beacon: &config.L1BeaconEndpointConfig{ BeaconAddr: l1CL.beaconHTTPAddr, }, @@ -313,9 +329,8 @@ func WithOpNode(l2CLID stack.L2CLNodeID, l1CLID stack.L1CLNodeID, l1ELID stack.L SyncModeReqResp: cfg.UseReqRespSync, SkipSyncStartCheck: false, SupportsPostFinalizationELSync: false, - UnsafeOnly: unsafeOnly, - L2FollowSourceEndpoint: "", - NeedInitialResetEngine: cfg.IsSequencer && unsafeOnly, + L2FollowSourceEndpoint: cfg.FollowSource, + NeedInitialResetEngine: cfg.IsSequencer && cfg.FollowSource != "", }, ConfigPersistence: config.DisabledConfigPersistence{}, Metrics: opmetrics.CLIConfig{}, @@ -350,5 +365,5 @@ func WithOpNode(l2CLID stack.L2CLNodeID, l1CLID stack.L1CLNodeID, l1ELID stack.L require.True(orch.l2CLs.SetIfMissing(l2CLID, l2CLNode), fmt.Sprintf("must not already exist: %s", l2CLID)) l2CLNode.Start() p.Cleanup(l2CLNode.Stop) - }) + } } diff --git a/op-devstack/sysgo/op_rbuilder.go b/op-devstack/sysgo/op_rbuilder.go index fc92a5aba2d..93d4235ffa9 100644 --- a/op-devstack/sysgo/op_rbuilder.go +++ b/op-devstack/sysgo/op_rbuilder.go @@ -378,10 +378,15 @@ func (b *OPRBuilderNode) Start() { b.sub = NewSubProcess(b.p, stdOut, stdErr) - exec := os.Getenv("OP_RBUILDER_EXEC_PATH") - b.p.Require().NotEmpty(exec, "OP_RBUILDER_EXEC_PATH must be set") + execPath, err := EnsureRustBinary(b.p, RustBinarySpec{ + SrcDir: "op-rbuilder", + Package: "op-rbuilder", + Binary: "op-rbuilder", + }) + b.p.Require().NoError(err, "prepare op-rbuilder binary") + b.p.Require().NotEmpty(execPath, "op-rbuilder binary path resolved") - err := b.sub.Start(exec, args, env) + err = b.sub.Start(execPath, args, env) b.p.Require().NoError(err, "start OPRBuilderNode") const readinessTimeout = 15 * time.Second diff --git a/op-devstack/sysgo/rollup_boost.go b/op-devstack/sysgo/rollup_boost.go index ece089e00e4..f7bffc40273 100644 --- a/op-devstack/sysgo/rollup_boost.go +++ b/op-devstack/sysgo/rollup_boost.go @@ -3,7 +3,6 @@ package sysgo import ( "net" "net/http" - "os" "strconv" "strings" "sync" @@ -112,10 +111,15 @@ func (r *RollupBoostNode) Start() { r.sub = NewSubProcess(r.p, stdOut, stdErr) - exec := os.Getenv("ROLLUP_BOOST_EXEC_PATH") - r.p.Require().NotEmpty(exec, "ROLLUP_BOOST_EXEC_PATH must be set") + execPath, err := EnsureRustBinary(r.p, RustBinarySpec{ + SrcDir: "rollup-boost", + Package: "rollup-boost", + Binary: "rollup-boost", + }) + r.p.Require().NoError(err, "prepare rollup-boost binary") + r.p.Require().NotEmpty(execPath, "rollup-boost binary path resolved") - err := r.sub.Start(exec, args, env) + err = r.sub.Start(execPath, args, env) r.p.Require().NoError(err, "start rollup-boost") rpcUpstreamURL := "http://" + cfg.RPCHost + ":" + strconv.Itoa(int(cfg.RPCPort)) diff --git a/op-devstack/sysgo/rust_binary.go b/op-devstack/sysgo/rust_binary.go new file mode 100644 index 00000000000..cd74a11e897 --- /dev/null +++ b/op-devstack/sysgo/rust_binary.go @@ -0,0 +1,96 @@ +package sysgo + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + opservice "github.com/ethereum-optimism/optimism/op-service" +) + +// RustBinarySpec describes a Rust binary to be built and located. +type RustBinarySpec struct { + SrcDir string // directory name relative to monorepo root, e.g. "rollup-boost" + Package string // cargo package name, e.g. "rollup-boost" + Binary string // binary name, e.g. "rollup-boost" +} + +// EnsureRustBinary locates or builds a Rust binary as needed. +// +// Env var overrides (suffix derived from binary name, e.g. "rollup-boost" -> "ROLLUP_BOOST"): +// - RUST_BINARY_PATH_: absolute path to pre-built binary (skips build, must exist) +// - RUST_SRC_DIR_: overrides SrcDir (absolute path to cargo project root) +// +// Build behavior: +// - RUST_JIT_BUILD=1: runs cargo build --release (letting cargo handle rebuild detection) +// - Otherwise: only checks binary exists, errors if missing +func EnsureRustBinary(p devtest.P, spec RustBinarySpec) (string, error) { + envSuffix := toEnvVarSuffix(spec.Binary) + + // Check for explicit binary path override + if pathOverride := os.Getenv("RUST_BINARY_PATH_" + envSuffix); pathOverride != "" { + if _, err := os.Stat(pathOverride); os.IsNotExist(err) { + return "", fmt.Errorf("%s binary not found at overridden path %s", spec.Binary, pathOverride) + } + p.Logger().Info("Using overridden binary path", "binary", spec.Binary, "path", pathOverride) + return pathOverride, nil + } + + // Determine source root + srcRoot, err := resolveSrcRoot(spec.SrcDir, envSuffix) + if err != nil { + return "", err + } + + binaryPath := filepath.Join(srcRoot, "target", "release", spec.Binary) + jitBuild := os.Getenv("RUST_JIT_BUILD") != "" + + if jitBuild { + p.Logger().Info("Building Rust binary (JIT)", "binary", spec.Binary, "dir", srcRoot) + if err := buildRustBinary(p.Ctx(), srcRoot, spec.Package, spec.Binary); err != nil { + return "", err + } + } else { + if _, err := os.Stat(binaryPath); os.IsNotExist(err) { + return "", fmt.Errorf("%s binary not found at %s; "+ + "run 'just build-rust-debug' before the test or set RUST_JIT_BUILD=1", spec.Binary, binaryPath) + } + } + + return binaryPath, nil +} + +// resolveSrcRoot determines the cargo project root, checking for env var override first. +func resolveSrcRoot(defaultSrcDir, envSuffix string) (string, error) { + if srcOverride := os.Getenv("RUST_SRC_DIR_" + envSuffix); srcOverride != "" { + return srcOverride, nil + } + + rootDir, err := os.Getwd() + if err != nil { + return "", err + } + monorepoRoot, err := opservice.FindMonorepoRoot(rootDir) + if err != nil { + return "", err + } + return filepath.Join(monorepoRoot, defaultSrcDir), nil +} + +// toEnvVarSuffix converts a binary name to an env var suffix. +// e.g. "rollup-boost" -> "ROLLUP_BOOST" +func toEnvVarSuffix(binary string) string { + return strings.ToUpper(strings.ReplaceAll(binary, "-", "_")) +} + +func buildRustBinary(ctx context.Context, root, pkg, bin string) error { + cmd := exec.CommandContext(ctx, "cargo", "build", "--release", "-p", pkg, "--bin", bin) + cmd.Dir = root + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} diff --git a/op-devstack/sysgo/superroot.go b/op-devstack/sysgo/superroot.go index 3b358ec7704..7e5d48e6b19 100644 --- a/op-devstack/sysgo/superroot.go +++ b/op-devstack/sysgo/superroot.go @@ -241,7 +241,7 @@ func getCannonKonaAbsolutePrestate(t devtest.CommonT) common.Hash { } func loadKonaVersions(t devtest.CommonT) konaVersions { - konaVersionPath := "kona/version.json" + konaVersionPath := "kona-proofs/version.json" root, err := findMonorepoRoot(konaVersionPath) t.Require().NoError(err) p := path.Join(root, konaVersionPath) diff --git a/op-devstack/sysgo/supervisor_kona.go b/op-devstack/sysgo/supervisor_kona.go index 8d4b925b570..926c87b2255 100644 --- a/op-devstack/sysgo/supervisor_kona.go +++ b/op-devstack/sysgo/supervisor_kona.go @@ -158,10 +158,13 @@ func WithKonaSupervisor(supervisorID stack.SupervisorID, clusterID stack.Cluster "KONA_LOG_STDOUT_FORMAT=json", } - execPath := os.Getenv("KONA_SUPERVISOR_EXEC_PATH") - p.Require().NotEmpty(execPath, "KONA_SUPERVISOR_EXEC_PATH environment variable must be set") - _, err = os.Stat(execPath) - p.Require().NotErrorIs(err, os.ErrNotExist, "executable must exist") + execPath, err := EnsureRustBinary(p, RustBinarySpec{ + SrcDir: "kona", + Package: "kona-supervisor", + Binary: "kona-supervisor", + }) + p.Require().NoError(err, "prepare kona-supervisor binary") + p.Require().NotEmpty(execPath, "kona-supervisor binary path resolved") konaSupervisor := &KonaSupervisor{ id: supervisorID, diff --git a/op-devstack/sysgo/system.go b/op-devstack/sysgo/system.go index a925d4703e2..01af0b2f4c9 100644 --- a/op-devstack/sysgo/system.go +++ b/op-devstack/sysgo/system.go @@ -1,9 +1,6 @@ package sysgo import ( - "os" - "strings" - "github.com/ethereum-optimism/optimism/op-chain-ops/devkeys" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer" "github.com/ethereum-optimism/optimism/op-devstack/stack" @@ -575,6 +572,7 @@ func ProofSystem(dest *DefaultMinimalSystemIDs) stack.Option[*Orchestrator] { ids := NewDefaultMinimalSystemIDs(DefaultL1ID, DefaultL2AID) opt := defaultMinimalSystemOpts(&ids, dest) opt.Add(WithCannonGameTypeAdded(ids.L1EL, ids.L2.ChainID())) + opt.Add(WithCannonKonaGameTypeAdded()) return opt } @@ -625,22 +623,6 @@ func singleChainSystemWithFlashblocksOpts(ids *SingleChainSystemWithFlashblocksI seqID := NewELNodeIdentity("127.0.0.1", 0) builderID := NewELNodeIdentity("127.0.0.1", 0) // allocate dynamic port for builder - var missingEnv []string - if os.Getenv("OP_RBUILDER_EXEC_PATH") == "" { - missingEnv = append(missingEnv, "OP_RBUILDER_EXEC_PATH") - } - if os.Getenv("ROLLUP_BOOST_EXEC_PATH") == "" { - missingEnv = append(missingEnv, "ROLLUP_BOOST_EXEC_PATH") - } - if len(missingEnv) > 0 { - missing := strings.Join(missingEnv, ", ") - opt.Add(stack.BeforeDeploy(func(o *Orchestrator) { - o.P().Logger().Warn("Skipping single-chain flashblocks system; missing executables", "missing_env", missing) - o.P().SkipNow() - })) - return opt - } - opt.Add(stack.BeforeDeploy(func(o *Orchestrator) { o.P().Logger().Info("Setting up") })) diff --git a/op-devstack/sysgo/system_singlechain_twoverifiers.go b/op-devstack/sysgo/system_singlechain_twoverifiers.go index 9b47b2b0f04..14c69f0534b 100644 --- a/op-devstack/sysgo/system_singlechain_twoverifiers.go +++ b/op-devstack/sysgo/system_singlechain_twoverifiers.go @@ -1,6 +1,7 @@ package sysgo import ( + "github.com/ethereum-optimism/optimism/op-chain-ops/devkeys" "github.com/ethereum-optimism/optimism/op-devstack/stack" "github.com/ethereum-optimism/optimism/op-service/eth" ) @@ -10,6 +11,8 @@ type DefaultSingleChainTwoVerifiersSystemIDs struct { L2CLC stack.L2CLNodeID L2ELC stack.L2ELNodeID + + TestSequencer stack.TestSequencerID } func NewDefaultSingleChainTwoVerifiersSystemIDs(l1ID, l2ID eth.ChainID) DefaultSingleChainTwoVerifiersSystemIDs { @@ -20,24 +23,56 @@ func NewDefaultSingleChainTwoVerifiersSystemIDs(l1ID, l2ID eth.ChainID) DefaultS } } -func DefaultSingleChainTwoVerifiersSystem(dest *DefaultSingleChainTwoVerifiersSystemIDs) stack.Option[*Orchestrator] { +func DefaultSingleChainTwoVerifiersFollowL2System(dest *DefaultSingleChainTwoVerifiersSystemIDs) stack.Option[*Orchestrator] { ids := NewDefaultSingleChainTwoVerifiersSystemIDs(DefaultL1ID, DefaultL2AID) opt := stack.Combine[*Orchestrator]() - opt.Add(DefaultSingleChainMultiNodeSystem(&dest.DefaultSingleChainMultiNodeSystemIDs)) + opt.Add(stack.BeforeDeploy(func(o *Orchestrator) { + o.P().Logger().Info("Setting up") + })) + + opt.Add(WithMnemonicKeys(devkeys.TestMnemonic)) + + opt.Add(WithDeployer(), + WithDeployerOptions( + WithLocalContractSources(), + WithCommons(ids.L1.ChainID()), + WithPrefundedL2(ids.L1.ChainID(), ids.L2.ChainID()), + ), + ) + + opt.Add(WithL1Nodes(ids.L1EL, ids.L1CL)) + + opt.Add(WithL2ELNode(ids.L2ELB)) + opt.Add(WithL2CLNode(ids.L2CLB, ids.L1CL, ids.L1EL, ids.L2ELB)) + + opt.Add(WithL2ELNode(ids.L2EL)) + opt.Add(WithL2CLNodeFollowL2(ids.L2CL, ids.L1CL, ids.L1EL, ids.L2EL, ids.L2CLB, L2CLSequencer())) opt.Add(WithL2ELNode(ids.L2ELC)) - // Specific options are applied after global options - // this means unsafeOnly is always disabled for the second verifier - opt.Add(WithL2CLNode(ids.L2CLC, ids.L1CL, ids.L1EL, ids.L2ELC, L2CLVerifierDisableUnsafeOnly())) + opt.Add(WithL2CLNodeFollowL2(ids.L2CLC, ids.L1CL, ids.L1EL, ids.L2ELC, ids.L2CLB)) + opt.Add(WithL2CLP2PConnection(ids.L2CL, ids.L2CLB)) + opt.Add(WithL2ELP2PConnection(ids.L2EL, ids.L2ELB)) opt.Add(WithL2CLP2PConnection(ids.L2CL, ids.L2CLC)) opt.Add(WithL2ELP2PConnection(ids.L2EL, ids.L2ELC)) opt.Add(WithL2CLP2PConnection(ids.L2CLB, ids.L2CLC)) opt.Add(WithL2ELP2PConnection(ids.L2ELB, ids.L2ELC)) + opt.Add(WithBatcher(ids.L2Batcher, ids.L1EL, ids.L2CL, ids.L2EL)) + opt.Add(WithProposer(ids.L2Proposer, ids.L1EL, &ids.L2CL, nil)) + + opt.Add(WithFaucets([]stack.L1ELNodeID{ids.L1EL}, []stack.L2ELNodeID{ids.L2EL})) + + opt.Add(WithTestSequencer(ids.TestSequencer, ids.L1CL, ids.L2CLB, ids.L1EL, ids.L2ELB)) + + opt.Add(WithL2Challenger(ids.L2Challenger, ids.L1EL, ids.L1CL, nil, nil, &ids.L2CL, []stack.L2ELNodeID{ + ids.L2EL, + })) + opt.Add(stack.Finally(func(orch *Orchestrator) { *dest = ids })) + return opt } diff --git a/op-devstack/sysgo/system_synctester_ext.go b/op-devstack/sysgo/system_synctester_ext.go index aa8dd829f28..fe784f65832 100644 --- a/op-devstack/sysgo/system_synctester_ext.go +++ b/op-devstack/sysgo/system_synctester_ext.go @@ -88,10 +88,11 @@ func ExternalELSystemWithEndpointAndSuperchainRegistry(dest *DefaultMinimalExter // Add SyncTesterL2ELNode as the L2EL replacement for real-world EL endpoint opt.Add(WithSyncTesterL2ELNode(ids.L2EL, ids.L2EL)) - opt.Add(WithL2CLNode(ids.L2CL, ids.L1CL, ids.L1EL, ids.L2EL)) opt.Add(WithExtL2Node(ids.L2ELReadOnly, networkPreset.L2ELEndpoint)) + opt.Add(WithL2CLNode(ids.L2CL, ids.L1CL, ids.L1EL, ids.L2EL)) + opt.Add(WithL2MetricsDashboard()) opt.Add(stack.Finally(func(orch *Orchestrator) { diff --git a/op-devstack/sysgo/util.go b/op-devstack/sysgo/util.go index 893d9f17051..b3903c26d69 100644 --- a/op-devstack/sysgo/util.go +++ b/op-devstack/sysgo/util.go @@ -37,24 +37,45 @@ func propagateEnvVarOrDefault(envVarName string, defaultValue string) string { } var availableLocalPortMutex sync.Mutex +var recentlyAllocatedPorts = make(map[int]struct{}) // getAvailableLocalPort searches for and returns a currently unused local port. +// Tracks recently allocated ports to avoid returning the same port twice +// (the OS may recycle a port immediately after we release it). // Note: this function is threadsafe. func getAvailableLocalPort() (string, error) { availableLocalPortMutex.Lock() defer availableLocalPortMutex.Unlock() - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - return "", fmt.Errorf("could not listen on ephemeral port: %w", err) - } - defer ln.Close() + // Keep listeners open while looping so the OS won't return the same port twice + var heldListeners []net.Listener + defer func() { + for _, ln := range heldListeners { + ln.Close() + } + }() + + const maxAttempts = 100 + for range maxAttempts { + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return "", fmt.Errorf("could not listen on ephemeral port: %w", err) + } + heldListeners = append(heldListeners, ln) - addr, ok := ln.Addr().(*net.TCPAddr) - if !ok { - return "", errors.New("listener did not return a TCP addr") + addr, ok := ln.Addr().(*net.TCPAddr) + if !ok { + return "", errors.New("listener did not return a TCP addr") + } + port := addr.Port + + if _, used := recentlyAllocatedPorts[port]; used { + continue + } + recentlyAllocatedPorts[port] = struct{}{} + return strconv.Itoa(port), nil } - return strconv.Itoa(addr.Port), nil + return "", errors.New("failed to allocate unique port after max attempts") } // waitTCPReady parses a URL and waits for its TCP endpoint to become ready using EventuallyWithT. diff --git a/op-e2e/faultproofs/util.go b/op-e2e/faultproofs/util.go index 31e76cb3b0e..fbc5da4b615 100644 --- a/op-e2e/faultproofs/util.go +++ b/op-e2e/faultproofs/util.go @@ -57,14 +57,14 @@ func WithLatestFork() faultDisputeConfigOpts { genesisActivation := hexutil.Uint64(0) cfg.DeployConfig.L1CancunTimeOffset = &genesisActivation cfg.DeployConfig.L1PragueTimeOffset = &genesisActivation + cfg.DeployConfig.L1OsakaTimeOffset = &genesisActivation cfg.DeployConfig.L2GenesisDeltaTimeOffset = &genesisActivation cfg.DeployConfig.L2GenesisEcotoneTimeOffset = &genesisActivation cfg.DeployConfig.L2GenesisFjordTimeOffset = &genesisActivation cfg.DeployConfig.L2GenesisGraniteTimeOffset = &genesisActivation cfg.DeployConfig.L2GenesisHoloceneTimeOffset = &genesisActivation cfg.DeployConfig.L2GenesisIsthmusTimeOffset = &genesisActivation - // TODO(#17348): Jovian is not supported in op-e2e tests yet - //cfg.DeployConfig.L2GenesisJovianTimeOffset = &genesisActivation + cfg.DeployConfig.L2GenesisJovianTimeOffset = &genesisActivation }) } } diff --git a/op-interop-filter/Makefile b/op-interop-filter/Makefile new file mode 100644 index 00000000000..9d1abda16ca --- /dev/null +++ b/op-interop-filter/Makefile @@ -0,0 +1,3 @@ +DEPRECATED_TARGETS := op-interop-filter clean test + +include ../justfiles/deprecated.mk diff --git a/op-interop-filter/README.md b/op-interop-filter/README.md new file mode 100644 index 00000000000..3b3f32eee8b --- /dev/null +++ b/op-interop-filter/README.md @@ -0,0 +1,26 @@ +# op-interop-filter + +A lightweight service that validates interop executing messages for op-geth or op-reth transaction filtering. + +Any reorg will trigger the failsafe which disables all interop transactions. + +## Usage + +### Build from source + +```bash +just op-interop-filter +./bin/op-interop-filter --help +``` + +### Run from source + +```bash +go run ./cmd --help +``` + +### Build docker image + +```bash +docker buildx bake op-interop-filter +``` diff --git a/op-interop-filter/cmd/main.go b/op-interop-filter/cmd/main.go new file mode 100644 index 00000000000..d04dc4b95af --- /dev/null +++ b/op-interop-filter/cmd/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "context" + "os" + + "github.com/ethereum/go-ethereum/log" + "github.com/urfave/cli/v2" + + opservice "github.com/ethereum-optimism/optimism/op-service" + "github.com/ethereum-optimism/optimism/op-service/cliapp" + "github.com/ethereum-optimism/optimism/op-service/ctxinterrupt" + oplog "github.com/ethereum-optimism/optimism/op-service/log" + "github.com/ethereum-optimism/optimism/op-service/metrics/doc" + + "github.com/ethereum-optimism/optimism/op-interop-filter/filter" + "github.com/ethereum-optimism/optimism/op-interop-filter/flags" + "github.com/ethereum-optimism/optimism/op-interop-filter/metrics" +) + +var ( + Version = "v0.0.0" + GitCommit = "" + GitDate = "" +) + +func main() { + oplog.SetupDefaults() + + app := cli.NewApp() + app.Flags = cliapp.ProtectFlags(flags.Flags) + app.Version = opservice.FormatVersion(Version, GitCommit, GitDate, "") + app.Name = "op-interop-filter" + app.Usage = "Interop transaction filter service" + app.Description = "Validates interop executing messages for transaction filtering" + app.Action = cliapp.LifecycleCmd(filter.Main(app.Version)) + app.Commands = []*cli.Command{ + { + Name: "doc", + Subcommands: doc.NewSubcommands(metrics.NewMetrics("default")), + }, + } + + ctx := ctxinterrupt.WithSignalWaiterMain(context.Background()) + err := app.RunContext(ctx, os.Args) + if err != nil { + log.Crit("Application failed", "message", err) + } +} diff --git a/op-interop-filter/filter/backend.go b/op-interop-filter/filter/backend.go new file mode 100644 index 00000000000..c98a197f8f6 --- /dev/null +++ b/op-interop-filter/filter/backend.go @@ -0,0 +1,56 @@ +package filter + +import ( + "context" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + + "github.com/ethereum-optimism/optimism/op-interop-filter/metrics" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" +) + +// Backend coordinates chain ingesters and handles the failsafe state. +// This is a stub implementation - the actual logic will be added in a follow-up PR. +type Backend struct { + log log.Logger + metrics metrics.Metricer + cfg *Config +} + +// NewBackend creates a new Backend instance +func NewBackend(ctx context.Context, logger log.Logger, m metrics.Metricer, cfg *Config) (*Backend, error) { + b := &Backend{ + log: logger, + metrics: m, + cfg: cfg, + } + logger.Info("Created backend", "chains", len(cfg.L2RPCs)) + return b, nil +} + +// Start starts the backend +func (b *Backend) Start(ctx context.Context) error { + b.log.Info("Starting backend (stub)") + return nil +} + +// Stop stops the backend +func (b *Backend) Stop(ctx context.Context) error { + b.log.Info("Stopping backend (stub)") + return nil +} + +// FailsafeEnabled returns whether failsafe is enabled +func (b *Backend) FailsafeEnabled() bool { + return false +} + +// CheckAccessList validates the given access list entries. +// This is a stub implementation that always returns ErrUninitialized. +func (b *Backend) CheckAccessList(ctx context.Context, inboxEntries []common.Hash, + minSafety types.SafetyLevel, execDescriptor types.ExecutingDescriptor) error { + + b.metrics.RecordCheckAccessList(false) + return types.ErrUninitialized +} diff --git a/op-interop-filter/filter/config.go b/op-interop-filter/filter/config.go new file mode 100644 index 00000000000..2fee5c6283d --- /dev/null +++ b/op-interop-filter/filter/config.go @@ -0,0 +1,62 @@ +package filter + +import ( + "errors" + "fmt" + "time" + + "github.com/urfave/cli/v2" + + "github.com/ethereum-optimism/optimism/op-interop-filter/flags" + oplog "github.com/ethereum-optimism/optimism/op-service/log" + opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics" + "github.com/ethereum-optimism/optimism/op-service/oppprof" + oprpc "github.com/ethereum-optimism/optimism/op-service/rpc" +) + +type Config struct { + L2RPCs []string + DataDir string + BackfillDuration time.Duration + JWTSecretPath string + Version string + + LogConfig oplog.CLIConfig + MetricsConfig opmetrics.CLIConfig + PprofConfig oppprof.CLIConfig + RPC oprpc.CLIConfig +} + +func (c *Config) Check() error { + var result error + if len(c.L2RPCs) == 0 { + result = errors.Join(result, errors.New("at least one L2 RPC is required")) + } + // Admin API requires JWT authentication + if c.RPC.EnableAdmin && c.JWTSecretPath == "" { + result = errors.Join(result, errors.New("admin RPC requires JWT setup, but no JWT path was specified")) + } + result = errors.Join(result, c.MetricsConfig.Check()) + result = errors.Join(result, c.PprofConfig.Check()) + result = errors.Join(result, c.RPC.Check()) + return result +} + +func NewConfig(ctx *cli.Context, version string) (*Config, error) { + backfillDuration, err := time.ParseDuration(ctx.String(flags.BackfillDurationFlag.Name)) + if err != nil { + return nil, fmt.Errorf("invalid backfill-duration: %w", err) + } + + return &Config{ + L2RPCs: ctx.StringSlice(flags.L2RPCsFlag.Name), + DataDir: ctx.String(flags.DataDirFlag.Name), + BackfillDuration: backfillDuration, + JWTSecretPath: ctx.String(flags.JWTSecretFlag.Name), + Version: version, + LogConfig: oplog.ReadCLIConfig(ctx), + MetricsConfig: opmetrics.ReadCLIConfig(ctx), + PprofConfig: oppprof.ReadCLIConfig(ctx), + RPC: oprpc.ReadCLIConfig(ctx), + }, nil +} diff --git a/op-interop-filter/filter/frontend.go b/op-interop-filter/filter/frontend.go new file mode 100644 index 00000000000..7e5a1ee6624 --- /dev/null +++ b/op-interop-filter/filter/frontend.go @@ -0,0 +1,52 @@ +package filter + +import ( + "context" + "errors" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rpc" + + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" +) + +// QueryFrontend handles supervisor query RPC methods +type QueryFrontend struct { + backend *Backend +} + +// CheckAccessList validates interop executing messages +func (f *QueryFrontend) CheckAccessList(ctx context.Context, inboxEntries []common.Hash, + minSafety types.SafetyLevel, executingDescriptor types.ExecutingDescriptor) error { + + err := f.backend.CheckAccessList(ctx, inboxEntries, minSafety, executingDescriptor) + if err != nil { + return &rpc.JsonError{ + Code: types.GetErrorCode(err), + Message: err.Error(), + } + } + return nil +} + +// AdminFrontend handles admin RPC methods +type AdminFrontend struct { + backend *Backend +} + +// GetFailsafeEnabled returns whether failsafe is enabled +func (a *AdminFrontend) GetFailsafeEnabled(ctx context.Context) (bool, error) { + return a.backend.FailsafeEnabled(), nil +} + +// SetFailsafeEnabled enables or disables failsafe mode (TODO: implement) +func (a *AdminFrontend) SetFailsafeEnabled(ctx context.Context, enabled bool) error { + return errors.New("SetFailsafeEnabled not yet implemented") +} + +// Rewind rewinds chain state to a specific block (TODO: implement) +// This can be used to recover from reorg-induced stuck states. +func (a *AdminFrontend) Rewind(ctx context.Context, chain eth.ChainID, block eth.BlockID) error { + return errors.New("Rewind not yet implemented") +} diff --git a/op-interop-filter/filter/service.go b/op-interop-filter/filter/service.go new file mode 100644 index 00000000000..77449649b52 --- /dev/null +++ b/op-interop-filter/filter/service.go @@ -0,0 +1,242 @@ +package filter + +import ( + "context" + "errors" + "fmt" + "sync/atomic" + + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rpc" + "github.com/urfave/cli/v2" + + opservice "github.com/ethereum-optimism/optimism/op-service" + "github.com/ethereum-optimism/optimism/op-service/cliapp" + "github.com/ethereum-optimism/optimism/op-service/httputil" + oplog "github.com/ethereum-optimism/optimism/op-service/log" + opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics" + "github.com/ethereum-optimism/optimism/op-service/oppprof" + oprpc "github.com/ethereum-optimism/optimism/op-service/rpc" + + "github.com/ethereum-optimism/optimism/op-interop-filter/flags" + "github.com/ethereum-optimism/optimism/op-interop-filter/metrics" +) + +// Service is the main op-interop-filter service +type Service struct { + log log.Logger + metrics metrics.Metricer + version string + + pprofService *oppprof.Service + metricsSrv *httputil.HTTPServer + rpcServer *oprpc.Server + + backend *Backend + + stopped atomic.Bool +} + +var _ cliapp.Lifecycle = (*Service)(nil) + +// Main returns the main entrypoint for the service +func Main(version string) cliapp.LifecycleAction { + return func(cliCtx *cli.Context, closeApp context.CancelCauseFunc) (cliapp.Lifecycle, error) { + if err := flags.CheckRequired(cliCtx); err != nil { + return nil, err + } + + cfg, err := NewConfig(cliCtx, version) + if err != nil { + return nil, fmt.Errorf("failed to parse config: %w", err) + } + if err := cfg.Check(); err != nil { + return nil, fmt.Errorf("invalid config: %w", err) + } + + l := oplog.NewLogger(oplog.AppOut(cliCtx), cfg.LogConfig) + oplog.SetGlobalLogHandler(l.Handler()) + opservice.ValidateEnvVars(flags.EnvVarPrefix, flags.Flags, l) + + l.Info("Initializing op-interop-filter", "version", version) + return NewService(cliCtx.Context, cfg, l) + } +} + +// NewService creates a new Service instance +func NewService(ctx context.Context, cfg *Config, logger log.Logger) (*Service, error) { + s := &Service{ + log: logger, + version: cfg.Version, + } + if err := s.init(ctx, cfg); err != nil { + return nil, errors.Join(err, s.Stop(ctx)) + } + return s, nil +} + +func (s *Service) init(ctx context.Context, cfg *Config) error { + s.initMetrics(cfg) + + if err := s.initPProf(cfg); err != nil { + return fmt.Errorf("failed to init pprof: %w", err) + } + if err := s.initMetricsServer(cfg); err != nil { + return fmt.Errorf("failed to init metrics server: %w", err) + } + if err := s.initBackend(ctx, cfg); err != nil { + return fmt.Errorf("failed to init backend: %w", err) + } + if err := s.initRPCServer(cfg); err != nil { + return fmt.Errorf("failed to init RPC server: %w", err) + } + return nil +} + +func (s *Service) initMetrics(cfg *Config) { + if cfg.MetricsConfig.Enabled { + s.metrics = metrics.NewMetrics("default") + s.metrics.RecordInfo(s.version) + } else { + s.metrics = metrics.NoopMetrics + } +} + +func (s *Service) initPProf(cfg *Config) error { + s.pprofService = oppprof.New( + cfg.PprofConfig.ListenEnabled, + cfg.PprofConfig.ListenAddr, + cfg.PprofConfig.ListenPort, + cfg.PprofConfig.ProfileType, + cfg.PprofConfig.ProfileDir, + cfg.PprofConfig.ProfileFilename, + ) + if err := s.pprofService.Start(); err != nil { + return fmt.Errorf("failed to start pprof: %w", err) + } + return nil +} + +func (s *Service) initMetricsServer(cfg *Config) error { + if !cfg.MetricsConfig.Enabled { + s.log.Info("Metrics disabled") + return nil + } + m, ok := s.metrics.(opmetrics.RegistryMetricer) + if !ok { + return fmt.Errorf("metrics do not expose registry") + } + metricsSrv, err := opmetrics.StartServer(m.Registry(), cfg.MetricsConfig.ListenAddr, cfg.MetricsConfig.ListenPort) + if err != nil { + return fmt.Errorf("failed to start metrics server: %w", err) + } + s.log.Info("Started metrics server", "addr", metricsSrv.Addr()) + s.metricsSrv = metricsSrv + return nil +} + +func (s *Service) initBackend(ctx context.Context, cfg *Config) error { + backend, err := NewBackend(ctx, s.log, s.metrics, cfg) + if err != nil { + return err + } + s.backend = backend + return nil +} + +func (s *Service) initRPCServer(cfg *Config) error { + opts := []oprpc.Option{ + oprpc.WithLogger(s.log), + } + + // Load JWT secret if path is provided (generates new secret if file is empty) + if cfg.JWTSecretPath != "" { + secret, err := oprpc.ObtainJWTSecret(s.log, cfg.JWTSecretPath, true) + if err != nil { + return fmt.Errorf("failed to obtain JWT secret: %w", err) + } + opts = append(opts, oprpc.WithJWTSecret(secret[:])) + } + + server := oprpc.NewServer( + cfg.RPC.ListenAddr, + cfg.RPC.ListenPort, + s.version, + opts..., + ) + + // Register supervisor query API + server.AddAPI(rpc.API{ + Namespace: "supervisor", + Service: &QueryFrontend{backend: s.backend}, + Authenticated: false, + }) + + // Register admin API (opt-in) + if cfg.RPC.EnableAdmin { + s.log.Info("Admin RPC enabled") + server.AddAPI(rpc.API{ + Namespace: "admin", + Service: &AdminFrontend{backend: s.backend}, + Authenticated: true, + }) + } + + s.rpcServer = server + return nil +} + +// Start starts the service +func (s *Service) Start(ctx context.Context) error { + s.log.Info("Starting op-interop-filter") + + // Start backend (begins block ingestion) + if err := s.backend.Start(ctx); err != nil { + return fmt.Errorf("failed to start backend: %w", err) + } + + // Start RPC server + if err := s.rpcServer.Start(); err != nil { + return fmt.Errorf("failed to start RPC server: %w", err) + } + s.log.Info("RPC server started", "endpoint", s.rpcServer.Endpoint()) + + s.metrics.RecordUp() + return nil +} + +// Stop stops the service +func (s *Service) Stop(ctx context.Context) error { + if !s.stopped.CompareAndSwap(false, true) { + return nil + } + s.log.Info("Stopping op-interop-filter") + + var result error + if s.rpcServer != nil { + if err := s.rpcServer.Stop(); err != nil { + result = errors.Join(result, fmt.Errorf("failed to stop RPC: %w", err)) + } + } + if s.backend != nil { + if err := s.backend.Stop(ctx); err != nil { + result = errors.Join(result, fmt.Errorf("failed to stop backend: %w", err)) + } + } + if s.pprofService != nil { + if err := s.pprofService.Stop(ctx); err != nil { + result = errors.Join(result, fmt.Errorf("failed to stop pprof: %w", err)) + } + } + if s.metricsSrv != nil { + if err := s.metricsSrv.Stop(ctx); err != nil { + result = errors.Join(result, fmt.Errorf("failed to stop metrics: %w", err)) + } + } + return result +} + +// Stopped returns true if the service has been stopped +func (s *Service) Stopped() bool { + return s.stopped.Load() +} diff --git a/op-interop-filter/flags/flags.go b/op-interop-filter/flags/flags.go new file mode 100644 index 00000000000..465aa3d031d --- /dev/null +++ b/op-interop-filter/flags/flags.go @@ -0,0 +1,79 @@ +package flags + +import ( + "fmt" + + "github.com/urfave/cli/v2" + + opservice "github.com/ethereum-optimism/optimism/op-service" + oplog "github.com/ethereum-optimism/optimism/op-service/log" + opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics" + "github.com/ethereum-optimism/optimism/op-service/oppprof" + oprpc "github.com/ethereum-optimism/optimism/op-service/rpc" +) + +const EnvVarPrefix = "OP_INTEROP_FILTER" + +func prefixEnvVars(name string) []string { + return opservice.PrefixEnvVar(EnvVarPrefix, name) +} + +var ( + L2RPCsFlag = &cli.StringSliceFlag{ + Name: "l2-rpcs", + Usage: "L2 RPC endpoints to connect to (chain ID is queried from each endpoint)", + EnvVars: prefixEnvVars("L2_RPCS"), + } + DataDirFlag = &cli.StringFlag{ + Name: "data-dir", + Usage: "Directory for LogsDB storage. If empty, uses in-memory storage", + EnvVars: prefixEnvVars("DATA_DIR"), + Value: "", + } + BackfillDurationFlag = &cli.StringFlag{ + Name: "backfill-duration", + Usage: "Duration to backfill on startup (e.g., 24h, 30m, 1h30m)", + EnvVars: prefixEnvVars("BACKFILL_DURATION"), + Value: "24h", + } + JWTSecretFlag = &cli.StringFlag{ + Name: "rpc.jwt-secret", + Usage: "Path to JWT secret key for RPC authentication. " + + "Keys are 32 bytes, hex encoded in a file. " + + "A new key will be generated if the file is empty.", + EnvVars: prefixEnvVars("RPC_JWT_SECRET"), + Value: "", + TakesFile: true, + } +) + +var requiredFlags = []cli.Flag{ + L2RPCsFlag, +} + +var optionalFlags = []cli.Flag{ + DataDirFlag, + BackfillDurationFlag, + JWTSecretFlag, +} + +func init() { + optionalFlags = append(optionalFlags, oprpc.CLIFlags(EnvVarPrefix)...) + optionalFlags = append(optionalFlags, oplog.CLIFlags(EnvVarPrefix)...) + optionalFlags = append(optionalFlags, opmetrics.CLIFlags(EnvVarPrefix)...) + optionalFlags = append(optionalFlags, oppprof.CLIFlags(EnvVarPrefix)...) + + Flags = append(requiredFlags, optionalFlags...) +} + +var Flags []cli.Flag + +func CheckRequired(ctx *cli.Context) error { + for _, f := range requiredFlags { + name := f.Names()[0] + if !ctx.IsSet(name) { + return fmt.Errorf("flag %s is required", name) + } + } + return nil +} diff --git a/op-interop-filter/justfile b/op-interop-filter/justfile new file mode 100644 index 00000000000..f8333843443 --- /dev/null +++ b/op-interop-filter/justfile @@ -0,0 +1,20 @@ +import '../justfiles/go.just' + +# Build ldflags string +_LDFLAGSSTRING := "'" + trim( + "-X main.GitCommit=" + GITCOMMIT + " " + \ + "-X main.GitDate=" + GITDATE + " " + \ + "-X main.Version=" + VERSION + " " + \ + "") + "'" + +BINARY := "./bin/op-interop-filter" + +# Build op-interop-filter binary +op-interop-filter: (go_build BINARY "./cmd" "-ldflags" _LDFLAGSSTRING) + +# Clean build artifacts +clean: + rm -f {{BINARY}} + +# Run tests +test: (go_test "./...") diff --git a/op-interop-filter/metrics/metrics.go b/op-interop-filter/metrics/metrics.go new file mode 100644 index 00000000000..c46f28cac59 --- /dev/null +++ b/op-interop-filter/metrics/metrics.go @@ -0,0 +1,127 @@ +package metrics + +import ( + "strconv" + + "github.com/prometheus/client_golang/prometheus" + + opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics" +) + +const Namespace = "op_interop_filter" + +type Metricer interface { + RecordInfo(version string) + RecordUp() + RecordFailsafeEnabled(enabled bool) + RecordChainHead(chainID uint64, blockNum uint64) + RecordCheckAccessList(success bool) +} + +type Metrics struct { + ns string + registry *prometheus.Registry + factory opmetrics.Factory + + info *prometheus.GaugeVec + up prometheus.Gauge + failsafeEnabled prometheus.Gauge + chainHead *prometheus.GaugeVec + checkAccessTotal *prometheus.CounterVec +} + +var _ Metricer = (*Metrics)(nil) +var _ opmetrics.RegistryMetricer = (*Metrics)(nil) + +func NewMetrics(procName string) *Metrics { + if procName == "" { + procName = "default" + } + ns := Namespace + "_" + procName + + registry := opmetrics.NewRegistry() + factory := opmetrics.With(registry) + + return &Metrics{ + ns: ns, + registry: registry, + factory: factory, + + info: factory.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: ns, + Name: "info", + Help: "Service info", + }, []string{"version"}), + + up: factory.NewGauge(prometheus.GaugeOpts{ + Namespace: ns, + Name: "up", + Help: "1 if service is up", + }), + + failsafeEnabled: factory.NewGauge(prometheus.GaugeOpts{ + Namespace: ns, + Name: "failsafe_enabled", + Help: "1 if failsafe is enabled", + }), + + chainHead: factory.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: ns, + Name: "chain_head", + Help: "Latest ingested block number", + }, []string{"chain_id"}), + + checkAccessTotal: factory.NewCounterVec(prometheus.CounterOpts{ + Namespace: ns, + Name: "check_access_list_total", + Help: "Total checkAccessList requests", + }, []string{"success"}), + } +} + +func (m *Metrics) Registry() *prometheus.Registry { + return m.registry +} + +func (m *Metrics) Document() []opmetrics.DocumentedMetric { + return m.factory.Document() +} + +func (m *Metrics) RecordInfo(version string) { + m.info.WithLabelValues(version).Set(1) +} + +func (m *Metrics) RecordUp() { + m.up.Set(1) +} + +func (m *Metrics) RecordFailsafeEnabled(enabled bool) { + if enabled { + m.failsafeEnabled.Set(1) + } else { + m.failsafeEnabled.Set(0) + } +} + +func (m *Metrics) RecordChainHead(chainID uint64, blockNum uint64) { + m.chainHead.WithLabelValues(strconv.FormatUint(chainID, 10)).Set(float64(blockNum)) +} + +func (m *Metrics) RecordCheckAccessList(success bool) { + label := "false" + if success { + label = "true" + } + m.checkAccessTotal.WithLabelValues(label).Inc() +} + +// NoopMetrics is a no-op implementation for testing +var NoopMetrics Metricer = &noopMetrics{} + +type noopMetrics struct{} + +func (n *noopMetrics) RecordInfo(version string) {} +func (n *noopMetrics) RecordUp() {} +func (n *noopMetrics) RecordFailsafeEnabled(enabled bool) {} +func (n *noopMetrics) RecordChainHead(chainID uint64, blockNum uint64) {} +func (n *noopMetrics) RecordCheckAccessList(success bool) {} diff --git a/op-node/config/config.go b/op-node/config/config.go index 4aaa32ccf97..fb53f31f281 100644 --- a/op-node/config/config.go +++ b/op-node/config/config.go @@ -68,6 +68,9 @@ type Config struct { // Optional Tracer tracer.Tracer + // Optional + L2FollowSource L2FollowSourceEndpointSetup + Sync sync.Config // To halt when detecting the node does not support a signaled protocol version diff --git a/op-node/config/follow_source.go b/op-node/config/follow_source.go new file mode 100644 index 00000000000..917c2ced181 --- /dev/null +++ b/op-node/config/follow_source.go @@ -0,0 +1,50 @@ +package config + +import ( + "context" + "errors" + "time" + + "github.com/ethereum-optimism/optimism/op-node/rollup" + "github.com/ethereum-optimism/optimism/op-service/client" + opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics" + "github.com/ethereum-optimism/optimism/op-service/sources" + "github.com/ethereum/go-ethereum/log" +) + +type L2FollowSourceEndpointSetup interface { + // Setup a RPC client to a L2 execution engine to follow. + Setup(ctx context.Context, log log.Logger, rollupCfg *rollup.Config, metrics opmetrics.RPCMetricer) (client.RPC, *sources.L2ClientConfig, error) + Check() error +} + +type L2FollowSourceConfig struct { + L2RPCAddr string + L2RPCCallTimeout time.Duration +} + +var _ L2FollowSourceEndpointSetup = (*L2FollowSourceConfig)(nil) + +func (cfg *L2FollowSourceConfig) Check() error { + if cfg.L2RPCAddr == "" { + return errors.New("empty L2 RPC Address") + } + return nil +} + +func (cfg *L2FollowSourceConfig) Setup(ctx context.Context, log log.Logger, rollupCfg *rollup.Config, metrics opmetrics.RPCMetricer) (client.RPC, *sources.L2ClientConfig, error) { + if err := cfg.Check(); err != nil { + return nil, nil, err + } + opts := []client.RPCOption{ + client.WithDialAttempts(10), + client.WithCallTimeout(cfg.L2RPCCallTimeout), + client.WithRPCRecorder(metrics.NewRecorder("follow-source-api")), + } + l2Node, err := client.NewRPC(ctx, log, cfg.L2RPCAddr, opts...) + if err != nil { + return nil, nil, err + } + + return l2Node, sources.L2ClientDefaultConfig(rollupCfg, true), nil +} diff --git a/op-node/flags/flags.go b/op-node/flags/flags.go index 37d9b421be6..522f4e7fa6b 100644 --- a/op-node/flags/flags.go +++ b/op-node/flags/flags.go @@ -217,20 +217,20 @@ var ( Value: time.Second * 10, Category: RollupCategory, } - L2UnsafeOnly = &cli.BoolFlag{ - Name: "l2.unsafe-only", - Usage: "Disable derivation", - EnvVars: prefixEnvVars("L2_UNSAFE_ONLY"), - Category: RollupCategory, - Required: false, - } L2FollowSource = &cli.StringFlag{ Name: "l2.follow.source", - Usage: "Address of L2 EL RPC HTTP endpoint to fetch safe/finalized blocks", + Usage: "Address of L2 CL RPC HTTP endpoint to follow source", EnvVars: prefixEnvVars("L2_FOLLOW_SOURCE"), Category: RollupCategory, Required: false, } + L2FollowSourceRpcTimeout = &cli.DurationFlag{ + Name: "l2.follow.source.rpc-timeout", + Usage: "L2 follow source client rpc timeout", + EnvVars: prefixEnvVars("L2_FOLLOW_SOURCE_RPC_TIMEOUT"), + Value: time.Second * 10, + Category: RollupCategory, + } VerifierL1Confs = &cli.Uint64Flag{ Name: "verifier.l1-confs", Usage: "Number of L1 blocks to keep distance from the L1 head before deriving L2 data from. Reorgs are supported, but may be slow to perform.", @@ -500,7 +500,6 @@ var optionalFlags = []cli.Flag{ L1ChainConfig, L2EngineKind, L2EngineRpcTimeout, - L2UnsafeOnly, L2FollowSource, InteropRPCAddr, InteropRPCPort, diff --git a/op-node/metrics/metrics.go b/op-node/metrics/metrics.go index 7bc8079c540..bc147643936 100644 --- a/op-node/metrics/metrics.go +++ b/op-node/metrics/metrics.go @@ -84,6 +84,8 @@ type Metrics struct { L1SourceCache *metrics.CacheMetrics L2SourceCache *metrics.CacheMetrics + L2FollowSourceCache *metrics.CacheMetrics + DerivationIdle prometheus.Gauge PipelineResets *metrics.Event @@ -186,6 +188,8 @@ func NewMetrics(procName string) *Metrics { L1SourceCache: metrics.NewCacheMetrics(factory, ns, "l1_source_cache", "L1 Source cache"), L2SourceCache: metrics.NewCacheMetrics(factory, ns, "l2_source_cache", "L2 Source cache"), + L2FollowSourceCache: metrics.NewCacheMetrics(factory, ns, "l2_follow_source_cache", "L2 Follow source cache"), + DerivationIdle: factory.NewGauge(prometheus.GaugeOpts{ Namespace: ns, Name: "derivation_idle", diff --git a/op-node/node/node.go b/op-node/node/node.go index fadc303b727..4ebe271566e 100644 --- a/op-node/node/node.go +++ b/op-node/node/node.go @@ -124,6 +124,8 @@ type OpNode struct { p2pSigner p2p.Signer // p2p gossip application messages will be signed with this signer runCfg *runcfg.RuntimeConfig // runtime configurables + l2FollowSource *sources.FollowClient // (Optional) L2 Follow source when derivation disabled + safeDB closableSafeDB rollupHalt string // when to halt the rollup, disabled if empty @@ -205,17 +207,19 @@ type InitializationOverrides struct { // so order is important to ensure that all resources are available when needed. func (n *OpNode) init(ctx context.Context, cfg *config.Config, overrides InitializationOverrides) error { n.log.Info("Initializing rollup node", "version", n.appVersion) + + var err error + safe := "enabled" - if cfg.Sync.UnsafeOnly { + if cfg.Sync.FollowSourceEnabled() { safe = cfg.Sync.L2FollowSourceEndpoint - if safe == "" { - safe = "disabled" + n.l2FollowSource, err = initFollowSource(ctx, cfg, n) + if err != nil { + return fmt.Errorf("failed to init l2 follow source: %w", err) } } n.log.Info("Safety levels", "unsafe", "enabled", "safe", safe) - var err error - n.eventSys, n.eventDrain, err = initEventSystem(n) if err != nil { return fmt.Errorf("failed to init event system: %w", err) @@ -597,7 +601,12 @@ func initL2(ctx context.Context, cfg *config.Config, node *OpNode) (*sources.Eng return nil, nil, nil, nil, fmt.Errorf("cfg.Rollup.ChainOpConfig is nil. Please see https://github.com/ethereum-optimism/optimism/releases/tag/op-node/v1.11.0: %w", err) } - l2Driver := driver.NewDriver(node.eventSys, node.eventDrain, &cfg.Driver, &cfg.Rollup, cfg.L1ChainConfig, cfg.DependencySet, l2Source, node.l1Source, + var upstreamFollowSource driver.UpstreamFollowSource + if node.cfg.Sync.FollowSourceEnabled() { + upstreamFollowSource = driver.NewL2FollowSource(node.l2FollowSource, node.l1Source) + } + + l2Driver := driver.NewDriver(node.eventSys, node.eventDrain, &cfg.Driver, &cfg.Rollup, cfg.L1ChainConfig, cfg.DependencySet, l2Source, node.l1Source, upstreamFollowSource, node.beacon, node, node, node.log, node.metrics, cfg.ConfigPersistence, safeDB, &cfg.Sync, sequencerConductor, altDA, indexingMode) // Wire up IndexingMode to engine controller for direct procedure call @@ -610,6 +619,18 @@ func initL2(ctx context.Context, cfg *config.Config, node *OpNode) (*sources.Eng return l2Source, sys, l2Driver, safeDB, nil } +func initFollowSource(ctx context.Context, cfg *config.Config, node *OpNode) (*sources.FollowClient, error) { + rpcClient, _, err := cfg.L2FollowSource.Setup(ctx, node.log, &node.cfg.Rollup, node.metrics) + if err != nil { + return nil, fmt.Errorf("failed to setup L2 follow source RPC client: %w", err) + } + l2FollowSource, err := sources.NewFollowClient(rpcClient) + if err != nil { + return nil, fmt.Errorf("failed to create follow client: %w", err) + } + return l2FollowSource, nil +} + func initRPCServer(cfg *config.Config, node *OpNode) (*oprpc.Server, error) { server := newRPCServer(&cfg.RPC, &cfg.Rollup, cfg.DependencySet, node.l2Source.L2Client, node.l2Driver, node.safeDB, diff --git a/op-node/rollup/derive/system_config.go b/op-node/rollup/derive/system_config.go index 0d03dd056d3..773a1c6f517 100644 --- a/op-node/rollup/derive/system_config.go +++ b/op-node/rollup/derive/system_config.go @@ -32,6 +32,11 @@ var ( ConfigUpdateEventVersion0 = common.Hash{} ) +var ( + ErrUnknownEventVersion = errors.New("unknown SystemConfig event version") + ErrUnknownEventType = errors.New("unknown SystemConfig event type") +) + // UpdateSystemConfigWithL1Receipts filters all L1 receipts to find config updates and applies the config updates to the given sysCfg // Updates are applied individually, and any malformed or invalid updates are ignored. // Any errors encountered during the update process are returned as a multierror. @@ -79,7 +84,7 @@ func ProcessSystemConfigUpdateLogEvent(destSysCfg *eth.SystemConfig, ev *types.L // indexed 0 version := ev.Topics[1] if version != ConfigUpdateEventVersion0 { - return fmt.Errorf("unrecognized SystemConfig update event version: %s", version) + return fmt.Errorf("%w: %s", ErrUnknownEventVersion, version) } // indexed 1 updateType := ev.Topics[2] @@ -150,11 +155,11 @@ func ProcessSystemConfigUpdateLogEvent(destSysCfg *eth.SystemConfig, ev *types.L destSysCfg.DAFootprintGasScalar = daFootprintGasScalar return nil default: - return fmt.Errorf("unrecognized L1 sysCfg update type: %s", updateType) + return fmt.Errorf("%w: %s", ErrUnknownEventType, updateType) } } -var ErrParsingSystemConfig = NewCriticalError(errors.New("error parsing system config")) +var ErrParsingSystemConfig = errors.New("error parsing system config") func parseSystemConfigUpdateBatcher(data []byte) (common.Address, error) { reader := bytes.NewReader(data) diff --git a/op-node/rollup/derive/system_config_test.go b/op-node/rollup/derive/system_config_test.go index 87a3bc1e291..8317587543a 100644 --- a/op-node/rollup/derive/system_config_test.go +++ b/op-node/rollup/derive/system_config_test.go @@ -387,6 +387,25 @@ func TestUpdateSystemConfigWithL1Receipts_Atomicity(t *testing.T) { }, Data: []byte{0x00}, // insufficient bytes for pointer/length -> parse error } + // Future / unknown event type + futureLogType := &types.Log{ + Address: l1Addr, + Topics: []common.Hash{ + ConfigUpdateEventABIHash, + ConfigUpdateEventVersion0, + common.Hash{0: 'a', 31: 7}, // test assumes this is not a known event type + }, + } + // Future / unknown event version + futureLogVersion := &types.Log{ + Address: l1Addr, + Topics: []common.Hash{ + ConfigUpdateEventABIHash, + common.Hash{31: 1}, // test assumes this is not a known event version + SystemConfigUpdateBatcher, + }, + Data: batcherData, + } receipts := []*types.Receipt{ { Status: types.ReceiptStatusSuccessful, @@ -396,6 +415,14 @@ func TestUpdateSystemConfigWithL1Receipts_Atomicity(t *testing.T) { Status: types.ReceiptStatusSuccessful, Logs: []*types.Log{malformedGasLog}, }, + { + Status: types.ReceiptStatusSuccessful, + Logs: []*types.Log{futureLogType}, + }, + { + Status: types.ReceiptStatusSuccessful, + Logs: []*types.Log{futureLogVersion}, + }, } err = UpdateSystemConfigWithL1Receipts(&sysCfg, receipts, &cfg, 0) // Error should be returned due to malformed update, but valid updates should apply @@ -404,6 +431,11 @@ func TestUpdateSystemConfigWithL1Receipts_Atomicity(t *testing.T) { require.Equal(t, newBatcher, sysCfg.BatcherAddr) // Confirm invalid update did not apply; GasLimit remains unchanged require.Equal(t, initial.GasLimit, sysCfg.GasLimit) + // Confirm error contains expected messages + require.ErrorContains(t, err, "invalid pointer field") + require.ErrorIs(t, err, ErrParsingSystemConfig) + require.ErrorIs(t, err, ErrUnknownEventType) + require.ErrorIs(t, err, ErrUnknownEventVersion) }) t.Run("applies multiple updates within a single receipt", func(t *testing.T) { diff --git a/op-node/rollup/driver/driver.go b/op-node/rollup/driver/driver.go index 63f552080ec..9d24c340b0e 100644 --- a/op-node/rollup/driver/driver.go +++ b/op-node/rollup/driver/driver.go @@ -37,6 +37,7 @@ func NewDriver( depSet derive.DependencySet, l2 L2Chain, l1 L1Chain, + upstreamFollowSource UpstreamFollowSource, l1Blobs derive.L1BlobsFetcher, altSync AltSync, network Network, @@ -127,22 +128,23 @@ func NewDriver( driverEmitter := sys.Register("driver", nil) driver := &Driver{ - StatusTracker: statusTracker, - Finalizer: finalizer, - SyncDeriver: syncDeriver, - sched: schedDeriv, - emitter: driverEmitter, - drain: drain, - stateReq: make(chan chan struct{}), - forceReset: make(chan chan struct{}, 10), - driverConfig: driverCfg, - syncConfig: syncCfg, - driverCtx: driverCtx, - driverCancel: driverCancel, - log: log, - sequencer: sequencer, - metrics: metrics, - altSync: altSync, + StatusTracker: statusTracker, + Finalizer: finalizer, + SyncDeriver: syncDeriver, + sched: schedDeriv, + emitter: driverEmitter, + drain: drain, + stateReq: make(chan chan struct{}), + forceReset: make(chan chan struct{}, 10), + driverConfig: driverCfg, + syncConfig: syncCfg, + driverCtx: driverCtx, + driverCancel: driverCancel, + log: log, + sequencer: sequencer, + metrics: metrics, + altSync: altSync, + upstreamFollowSource: upstreamFollowSource, } return driver @@ -184,6 +186,8 @@ type Driver struct { driverCtx context.Context driverCancel context.CancelFunc + + upstreamFollowSource UpstreamFollowSource } // Start starts up the state loop. @@ -272,7 +276,7 @@ func (s *Driver) eventLoop() { lastUnsafeL2 := s.SyncDeriver.Engine.UnsafeL2Head() - unsafeOnly := s.SyncDeriver.SyncCfg.UnsafeOnly + followSource := s.SyncDeriver.SyncCfg.FollowSourceEnabled() resetAltSync := func(newHead eth.L2BlockRef, derivationReady bool) { s.log.Debug( @@ -280,12 +284,27 @@ func (s *Driver) eventLoop() { "head", newHead, "lastUnsafeL2", lastUnsafeL2, "derivationReady", derivationReady, - "unsafeOnly", unsafeOnly, + "followSource", followSource, ) lastUnsafeL2 = newHead altSyncTicker.Reset(syncCheckInterval) } + // upstreamSyncTickerC drives the upstreamSyncTicker, which periodically reconciles + // the state against upstream sources when derivation is disabled (unsafeOnly). + // + // In this mode, the node does not derive from L1; instead, it uses L1 as a mandatory + // upstream anchor for its unsafe head, and imports safe/finalized state + // from an external source. Since the normal derivation pipeline is inactive, reorg + // detection must be performed here instead. + var upstreamSyncTickerC <-chan time.Time + if followSource { + upstreamSyncTickerCheckInterval := time.Duration(s.SyncDeriver.Config.BlockTime) * time.Second * 2 + upstreamSyncTicker := time.NewTicker(upstreamSyncTickerCheckInterval) + upstreamSyncTickerC = upstreamSyncTicker.C + defer upstreamSyncTicker.Stop() + } + for { if s.driverCtx.Err() != nil { // don't try to schedule/handle more work when we are closing. return @@ -299,7 +318,7 @@ func (s *Driver) eventLoop() { if lastUnsafeL2 != head { // Unsafe head changed: reset alt-sync to avoid redundant L2 requests while syncing. resetAltSync(head, derivationReady) - } else if !unsafeOnly && !derivationReady { + } else if !followSource && !derivationReady { // Derivation enabled but not yet ready: reset alt-sync while it catches up. resetAltSync(head, derivationReady) } @@ -315,6 +334,8 @@ func (s *Driver) eventLoop() { if err != nil { s.log.Warn("failed to check for unsafe L2 blocks to sync", "err", err) } + case <-upstreamSyncTickerC: + s.followUpstream() case <-s.sched.NextDelayedStep(): s.sched.AttemptStep(s.driverCtx) case <-s.sched.NextStep(): @@ -445,3 +466,85 @@ func (s *Driver) checkForGapInUnsafeQueue(ctx context.Context) error { func (s *Driver) OnUnsafeL2Payload(ctx context.Context, payload *eth.ExecutionPayloadEnvelope) { s.SyncDeriver.OnUnsafeL2Payload(ctx, payload) } + +// followUpstream reconciles the local engine state with upstream sources when +// derivation is disabled (UnsafeOnly). +// +// In this mode, the driver does not derive L2 from L1. Instead, it: +// Uses the followTracker to fetch external safe / finalized / CurrentL1, +// validates that the external state is sane (e.g. finalized is not ahead +// of safe), and then updates the engine via FollowSource. +// +// This function is intended to be called periodically by a ticker and is a +// no-op while derivation is enabled or the EL is still performing its initial +// sync. +func (s *Driver) followUpstream() { + if !s.syncConfig.FollowSourceEnabled() { + return + } + if s.SyncDeriver.Engine.IsEngineInitialELSyncing() { + // Do not interfere with initial EL Sync and wait until it is done + return + } + status, err := s.upstreamFollowSource.GetFollowStatus(s.driverCtx) + if err != nil { + s.log.Warn("Follow Upstream: Failed to fetch status", "err", err) + return + } + s.log.Info("Follow Upstream", "eSafe", status.SafeL2, "eFinalized", status.FinalizedL2, "eCurrentL1", status.CurrentL1) + if status.FinalizedL2.Number > status.SafeL2.Number { + s.log.Warn("Follow Upstream: Invalid external state, finalized is ahead of safe", "safe", status.SafeL2.Number, "finalized", status.FinalizedL2.Number) + return + } + + eSafeL1Origin, err := s.upstreamFollowSource.L1BlockRefByNumber(s.driverCtx, status.SafeL2.L1Origin.Number) + if err != nil { + s.log.Warn("Follow Upstream: Failed to look up L1 origin of external safe head", "err", err) + return + } + if eSafeL1Origin.Hash != status.SafeL2.L1Origin.Hash { + s.log.Warn( + "Follow Upstream: Invalid external safe: L1 origin of external safe head mismatch", + "actual", eSafeL1Origin, + "expected", status.SafeL2.L1Origin, + ) + return + } + + eFinalizedL1Origin, err := s.upstreamFollowSource.L1BlockRefByNumber(s.driverCtx, status.FinalizedL2.L1Origin.Number) + if err != nil { + s.log.Warn("Follow Upstream: Failed to look up L1 origin of external finalized head", "err", err) + return + } + if eFinalizedL1Origin.Hash != status.FinalizedL2.L1Origin.Hash { + s.log.Warn( + "Follow Upstream: Invalid external finalized: L1 origin of external finalized head mismatch", + "actual", eFinalizedL1Origin, + "expected", status.FinalizedL2.L1Origin, + ) + return + } + + if (status.CurrentL1 == eth.L1BlockRef{}) { + s.log.Debug("Follow Upstream: CurrentL1 not available") + } else { + eCurrentL1, err := s.upstreamFollowSource.L1BlockRefByNumber(s.driverCtx, status.CurrentL1.Number) + if err != nil { + s.log.Warn("Follow Upstream: Failed to look up external currentL1", "err", err) + return + } + if eCurrentL1.Hash != status.CurrentL1.Hash { + s.log.Warn( + "Follow Upstream: Invalid external CurrentL1: L1 head mismatch", + "actual", eCurrentL1, + "expected", status.CurrentL1, + ) + return + } + + s.log.Debug("Follow Upstream: Inject L1 Info", "currentL1", status.CurrentL1) + s.emitter.Emit(s.driverCtx, derive.DeriverL1StatusEvent{Origin: status.CurrentL1}) + } + // Only reach this point if all L1 checks passed + s.SyncDeriver.Engine.FollowSource(status.SafeL2, status.FinalizedL2) +} diff --git a/op-node/rollup/driver/follow_source.go b/op-node/rollup/driver/follow_source.go new file mode 100644 index 00000000000..d6343eef35b --- /dev/null +++ b/op-node/rollup/driver/follow_source.go @@ -0,0 +1,42 @@ +package driver + +import ( + "context" + + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/sources" +) + +// L1FollowSource provides access to L1 block references for upstream following. +type L1FollowSource interface { + L1BlockRefByNumber(ctx context.Context, num uint64) (eth.L1BlockRef, error) +} + +// UpstreamFollowSource combines L1 and L2 follow sources. +// L2 following may be optionally disabled. +type UpstreamFollowSource interface { + L1FollowSource + GetFollowStatus(ctx context.Context) (*sources.FollowStatus, error) +} + +type L2FollowSource struct { + l2Source *sources.FollowClient + l1Source L1FollowSource +} + +var _ UpstreamFollowSource = (*L2FollowSource)(nil) + +func NewL2FollowSource(client *sources.FollowClient, l1Source L1FollowSource) *L2FollowSource { + if l1Source == nil || client == nil { + panic("NewL2FollowSource: sources must not be nil") + } + return &L2FollowSource{l2Source: client, l1Source: l1Source} +} + +func (fs *L2FollowSource) GetFollowStatus(ctx context.Context) (*sources.FollowStatus, error) { + return fs.l2Source.GetFollowStatus(ctx) +} + +func (fs *L2FollowSource) L1BlockRefByNumber(ctx context.Context, num uint64) (eth.L1BlockRef, error) { + return fs.l1Source.L1BlockRefByNumber(ctx, num) +} diff --git a/op-node/rollup/driver/sync_deriver.go b/op-node/rollup/driver/sync_deriver.go index bd5f29b0044..e8e38757a5e 100644 --- a/op-node/rollup/driver/sync_deriver.go +++ b/op-node/rollup/driver/sync_deriver.go @@ -237,7 +237,7 @@ func (s *SyncDeriver) SyncStep() { return } - if s.SyncCfg.UnsafeOnly { + if s.SyncCfg.FollowSourceEnabled() { if s.SyncCfg.NeedInitialResetEngine { // May need a single reset to trigger sequencer block building s.Engine.TryInitialResetEngineForSequencer(s.Ctx) diff --git a/op-node/rollup/engine/engine_controller.go b/op-node/rollup/engine/engine_controller.go index f97ba854f11..f6eaa8956ce 100644 --- a/op-node/rollup/engine/engine_controller.go +++ b/op-node/rollup/engine/engine_controller.go @@ -62,6 +62,7 @@ type ExecEngine interface { NewPayload(ctx context.Context, payload *eth.ExecutionPayload, parentBeaconBlockRoot *common.Hash) (*eth.PayloadStatusV1, error) L2BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L2BlockRef, error) L2BlockRefByHash(ctx context.Context, hash common.Hash) (eth.L2BlockRef, error) + L2BlockRefByNumber(ctx context.Context, num uint64) (eth.L2BlockRef, error) } // Metrics interface for CLSync functionality @@ -1122,3 +1123,65 @@ func (e *EngineController) startPayload(ctx context.Context, fc eth.ForkchoiceSt return eth.PayloadID{}, BlockInsertTemporaryErr, eth.ForkchoiceUpdateErr(fcRes.PayloadStatus) } } + +func (e *EngineController) FollowSource(eSafeBlockRef, eFinalizedRef eth.L2BlockRef) { + e.mu.Lock() + defer e.mu.Unlock() + + followExternalRefs := func(updateUnsafe bool) { + // Assume the sanity of external safe and finalized are checked + if updateUnsafe { + // May interrupt ongoing EL Sync to update the target, or trigger EL Sync + e.tryUpdateUnsafe(e.ctx, eSafeBlockRef) + } + e.tryUpdateLocalSafe(e.ctx, eSafeBlockRef, true, eth.L1BlockRef{}) + // Directly update the Engine Controller state, bypassing finalizer + if e.finalizedHead.Number <= eFinalizedRef.Number { + e.promoteFinalized(e.ctx, eFinalizedRef) + } + } + + logger := e.log.With( + "currentUnsafe", e.unsafeHead, + "currentSafe", e.safeHead, + "externalSafe", eSafeBlockRef, + "externalFinalized", eFinalizedRef, + ) + + logger.Info("Follow Source: Process external refs") + + if e.unsafeHead.Number < eSafeBlockRef.Number { + // EL Sync target may be updated + logger.Debug("Follow Source: EL Sync: External safe ahead of current unsafe") + followExternalRefs(true) + return + } + + fetchedSafe, err := e.engine.L2BlockRefByNumber(e.ctx, eSafeBlockRef.Number) + if errors.Is(err, ethereum.NotFound) { + // We queried a block before the EngineController unsafe head number, + // but it is not found. This indicates the underlying EL is still syncing. + // We do not know if the current EL sync is targeting a chain that will + // eventually reorg out this target. So we do not interrupt EL sync; + // we only update the local safe head. + logger.Debug("Follow Source: EL Sync in progress") + followExternalRefs(false) + return + } + if err != nil { + logger.Debug("Follow Source: Failed to fetch external safe from local EL", "err", err) + return + } + + if fetchedSafe == eSafeBlockRef { + // External safe is found locally and matches. + logger.Debug("Follow Source: Consolidation") + followExternalRefs(false) + return + } + + // External safe is found locally but they differ so trigger reorg. + // Reorging may trigger EL Sync, or updating the EL Sync target. + logger.Warn("Follow Source: Reorg. May Trigger EL sync") + followExternalRefs(true) +} diff --git a/op-node/rollup/sync/config.go b/op-node/rollup/sync/config.go index 79b46a223a0..2822be0420a 100644 --- a/op-node/rollup/sync/config.go +++ b/op-node/rollup/sync/config.go @@ -75,7 +75,10 @@ type Config struct { SupportsPostFinalizationELSync bool `json:"supports_post_finalization_elsync"` - UnsafeOnly bool `json:"unsafe_only"` L2FollowSourceEndpoint string `json:"l2_follow_source_endpoint"` NeedInitialResetEngine bool `json:"need_initial_reset_engine"` } + +func (c *Config) FollowSourceEnabled() bool { + return c.L2FollowSourceEndpoint != "" +} diff --git a/op-node/service.go b/op-node/service.go index d590750628e..1ac43ad80f4 100644 --- a/op-node/service.go +++ b/op-node/service.go @@ -117,6 +117,7 @@ func NewConfig(ctx cliiface.Context, log log.Logger) (*config.Config, error) { ConfigPersistence: configPersistence, SafeDBPath: ctx.String(flags.SafeDBPath.Name), Sync: *syncConfig, + L2FollowSource: NewL2FollowSourceConfig(ctx), RollupHalt: haltOption, ConductorEnabled: ctx.Bool(flags.ConductorEnabledFlag.Name), @@ -194,6 +195,15 @@ func NewL2EndpointConfig(ctx cliiface.Context, logger log.Logger) (*config.L2End }, nil } +func NewL2FollowSourceConfig(ctx cliiface.Context) *config.L2FollowSourceConfig { + l2Addr := ctx.String(flags.L2FollowSource.Name) + l2RpcTimeout := ctx.Duration(flags.L2FollowSourceRpcTimeout.Name) + return &config.L2FollowSourceConfig{ + L2RPCAddr: l2Addr, + L2RPCCallTimeout: l2RpcTimeout, + } +} + func NewConfigPersistence(ctx cliiface.Context) config.ConfigPersistence { stateFile := ctx.String(flags.RPCAdminPersistence.Name) if stateFile == "" { @@ -340,12 +350,7 @@ func NewSyncConfig(ctx cliiface.Context, log log.Logger) (*sync.Config, error) { } else if ctx.IsSet(flags.L2EngineSyncEnabled.Name) { log.Error("l2.engine-sync is deprecated and will be removed in a future release. Use --syncmode=execution-layer instead.") } - unsafeOnly := ctx.Bool(flags.L2UnsafeOnly.Name) l2FollowSourceEndpoint := ctx.String(flags.L2FollowSource.Name) - if !unsafeOnly && l2FollowSourceEndpoint != "" { - return nil, errors.New("cannot follow external safe/finalized with derivation enabled (--l2.unsafe-only=false): " + - "Either remove --l2.follow.source or set --l2.unsafe-only=true to disable derivation") - } rrSyncEnabled := ctx.Bool(flags.SyncModeReqRespFlag.Name) // p2p.sync.req-resp=false && syncmode.req-resp=true is not allowed if !ctx.Bool(flags.SyncReqRespName) && rrSyncEnabled { @@ -355,32 +360,15 @@ func NewSyncConfig(ctx cliiface.Context, log log.Logger) (*sync.Config, error) { if err != nil { return nil, err } - isSequencer := ctx.Bool(flags.SequencerEnabledFlag.Name) - if unsafeOnly && !isSequencer { - // The verifier node initially gains payloads from the sequencer via CLP2P. - // To sync to the chain tip, the verifier must close the gap between its current - // unsafe view and the sequencer's latest unsafe payloads. - // With derivation disabled, the node can only rely on RR Sync or EL Sync to close this gap. - if rrSyncEnabled { - // Allowing RR Sync technically works, but it is impractical for a verifier to - // rely solely on RR Syncing - bootstrapping would take too long. - // Since RR Sync is also being deprecated, fail early for clarity. - return nil, errors.New("derivation disabled (--l2.unsafe-only=true) and RR sync enabled (--syncmode.req-resp=true): " + - "reaching the unsafe tip would rely solely on RR sync, " + - "which is infeasible for bootstrap. Disable RR sync or enable derivation") - } - // If RR Sync is not used, EL Sync will fill in the unsafe gap. - // This path is much faster and more practical for closing the gap. - } engineKind := engine.Kind(ctx.String(flags.L2EngineKind.Name)) cfg := &sync.Config{ SyncMode: mode, SyncModeReqResp: ctx.Bool(flags.SyncModeReqRespFlag.Name), SkipSyncStartCheck: ctx.Bool(flags.SkipSyncStartCheck.Name), SupportsPostFinalizationELSync: engineKind.SupportsPostFinalizationELSync(), - UnsafeOnly: unsafeOnly, L2FollowSourceEndpoint: l2FollowSourceEndpoint, - NeedInitialResetEngine: isSequencer && unsafeOnly, + // Sequencer needs a manual initial reset when follow source + NeedInitialResetEngine: ctx.Bool(flags.SequencerEnabledFlag.Name) && l2FollowSourceEndpoint != "", } if ctx.Bool(flags.L2EngineSyncEnabled.Name) { cfg.SyncMode = sync.ELSync diff --git a/op-node/service_test.go b/op-node/service_test.go index ac24aa86165..7bcd0171cae 100644 --- a/op-node/service_test.go +++ b/op-node/service_test.go @@ -1,7 +1,6 @@ package opnode import ( - "fmt" "testing" "github.com/ethereum-optimism/optimism/op-node/flags" @@ -12,7 +11,6 @@ import ( func syncConfigCliApp() *cli.App { syncConfigFlags := append([]cli.Flag{ - flags.L2UnsafeOnly, flags.SequencerEnabledFlag, flags.L2EngineSyncEnabled, flags.SyncModeFlag, @@ -38,38 +36,3 @@ func run(args []string) error { func TestNewSyncConfigDefault(t *testing.T) { require.NoError(t, run(nil)) } - -func TestNewSyncConfig_DerivationDisabled_NoRRSync(t *testing.T) { - err := run([]string{ - fmt.Sprintf("--%s=true", flags.L2UnsafeOnly.Name), - // No follow source with no derivation allowed - fmt.Sprintf("--%s=false", flags.SyncModeReqRespFlag.Name), - }) - require.NoError(t, err) -} - -func TestNewSyncConfig_FollowSourceWithDerivationDisabled(t *testing.T) { - err := run([]string{ - fmt.Sprintf("--%s=true", flags.L2UnsafeOnly.Name), - fmt.Sprintf("--%s=http://example", flags.L2FollowSource.Name), - fmt.Sprintf("--%s=false", flags.SyncModeReqRespFlag.Name), - }) - require.NoError(t, err) -} - -func TestNewSyncConfig_FollowSourceWithDerivationEnabled(t *testing.T) { - err := run([]string{ - // unsafe-only defaults in false - fmt.Sprintf("--%s=http://example", flags.L2FollowSource.Name), - }) - require.ErrorContains(t, err, "cannot follow external safe/finalized with derivation enabled") -} - -func TestNewSyncConfig_VerifierUnsafeOnlyWithRRSyncEnabled(t *testing.T) { - err := run([]string{ - // verifier mode is default - fmt.Sprintf("--%s=true", flags.L2UnsafeOnly.Name), - fmt.Sprintf("--%s=true", flags.SyncModeReqRespFlag.Name), - }) - require.ErrorContains(t, err, "reaching the unsafe tip would rely solely on RR sync") -} diff --git a/op-rbuilder b/op-rbuilder new file mode 160000 index 00000000000..272d462d980 --- /dev/null +++ b/op-rbuilder @@ -0,0 +1 @@ +Subproject commit 272d462d980a43e7caf568df0fbbc0c2e0066207 diff --git a/op-service/github/release/provider.go b/op-service/github/release/provider.go index 9a17ec08512..ec1ff433a39 100644 --- a/op-service/github/release/provider.go +++ b/op-service/github/release/provider.go @@ -437,7 +437,7 @@ func (d *GithubReleaseDownloader) download(ctx context.Context, version string, // Move the extracted name to the destination path and ensure it is // executable by clearing/setting appropriate file mode bits. - if err := os.Rename(sourcePath, destinationPath); err != nil { + if err := ioutil.SafeRename(sourcePath, destinationPath); err != nil { return "", fmt.Errorf("failed to move name from %s to %s: %w", sourcePath, destinationPath, err) } if err := os.Chmod(destinationPath, 0o755); err != nil { diff --git a/op-service/ioutil/rename.go b/op-service/ioutil/rename.go new file mode 100644 index 00000000000..a7f421613d1 --- /dev/null +++ b/op-service/ioutil/rename.go @@ -0,0 +1,75 @@ +package ioutil + +import ( + "fmt" + "io" + "os" + "syscall" + + "errors" +) + +// SafeRename attempts to rename a file from source to destination. +// If the rename fails due to cross-device link error, it falls back to copying the file and deleting the source. +func SafeRename(source, destination string) error { + // First see if we can just rename the file normally + err := os.Rename(source, destination) + + // If we get an "invalid cross-device link" error, we need to do a copy and delete + if err != nil && errors.Is(err, syscall.EXDEV) { + return renameCrossDevice(source, destination) + } + + return err +} + +func renameCrossDevice(source, destination string) error { + // Open the source file + src, err := os.Open(source) + if err != nil { + return fmt.Errorf("rename: failed to open source file %s: %w", source, err) + } + + // Create the destination file + dst, err := os.Create(destination) + if err != nil { + // Make sure to close the source file before returning + src.Close() + + return fmt.Errorf("rename: failed to create destination file %s: %w", destination, err) + } + + // Copy the contents over + _, err = io.Copy(dst, src) + + // Close both files + src.Close() + dst.Close() + + if err != nil { + return fmt.Errorf("rename: failed to copy source %s to destination %s: %w", source, destination, err) + } + + // Get source file permissions + fileInfo, err := os.Stat(source) + if err != nil { + // Remove the destination file if we fail to stat the source + os.Remove(destination) + + return fmt.Errorf("rename: failed to stat source %s: %w", source, err) + } + + // Apply source file permissions to destination + err = os.Chmod(destination, fileInfo.Mode()) + if err != nil { + // Remove the destination file if we fail to apply the permissions + os.Remove(destination) + + return fmt.Errorf("rename: failed to apply file permissions to destination %s: %w", destination, err) + } + + // Delete the source file + os.Remove(source) + + return nil +} diff --git a/op-service/sources/follow_client.go b/op-service/sources/follow_client.go new file mode 100644 index 00000000000..0793ad5736a --- /dev/null +++ b/op-service/sources/follow_client.go @@ -0,0 +1,36 @@ +package sources + +import ( + "context" + "fmt" + + "github.com/ethereum-optimism/optimism/op-service/client" + "github.com/ethereum-optimism/optimism/op-service/eth" +) + +type FollowClient struct { + rollupClient *RollupClient +} + +type FollowStatus struct { + SafeL2 eth.L2BlockRef + FinalizedL2 eth.L2BlockRef + CurrentL1 eth.L1BlockRef +} + +func NewFollowClient(client client.RPC) (*FollowClient, error) { + rollupClient := NewRollupClient(client) + return &FollowClient{rollupClient: rollupClient}, nil +} + +func (s *FollowClient) GetFollowStatus(ctx context.Context) (*FollowStatus, error) { + status, err := s.rollupClient.SyncStatus(ctx) + if err != nil { + return nil, fmt.Errorf("failed to fetch external syncStatus: %w", err) + } + return &FollowStatus{ + FinalizedL2: status.FinalizedL2, + SafeL2: status.SafeL2, + CurrentL1: status.CurrentL1, + }, nil +} diff --git a/op-validator/pkg/validations/addresses.go b/op-validator/pkg/validations/addresses.go index 96c0fe41490..3e5de4e5dd2 100644 --- a/op-validator/pkg/validations/addresses.go +++ b/op-validator/pkg/validations/addresses.go @@ -20,7 +20,7 @@ var addresses = map[uint64]map[string]common.Address{ standard.ContractsV400Tag: common.HexToAddress("0x3dfc5e44043DC5998928E0b8280136b7352d3F70"), // Bootstrapped on 10/02/2025 using OP Deployer. standard.ContractsV410Tag: common.HexToAddress("0x845FEF377Fa9C678B3eBe33B024678538f1215dD"), - // Bootstrapped on 10/27/2025 using OP Deployer (v5.0.0-rc.2). + // Bootstrapped on 10/27/2025 using OP Deployer. standard.ContractsV500Tag: common.HexToAddress("0xDCE1A51A25dD5BF02ccB4264D039EDdF11A95b43"), }, 11155111: { @@ -34,7 +34,7 @@ var addresses = map[uint64]map[string]common.Address{ standard.ContractsV400Tag: common.HexToAddress("0xA8a1529547306FEC7A32a001705160f2110451aE"), // Bootstrapped on 10/02/2025 using OP Deployer. standard.ContractsV410Tag: common.HexToAddress("0x7B4d2a02d5fa6C7C98D835d819956EBB876Ff439"), - // Bootstrapped on 10/27/2025 using OP Deployer (v5.0.0-rc.2). + // Bootstrapped on 10/27/2025 using OP Deployer. standard.ContractsV500Tag: common.HexToAddress("0x757bFA3AAABcE60112Cee3239DCD05b5F6EFaE3A"), }, } diff --git a/op-validator/pkg/validations/addresses_test.go b/op-validator/pkg/validations/addresses_test.go index d4f7e46b132..edea32cea37 100644 --- a/op-validator/pkg/validations/addresses_test.go +++ b/op-validator/pkg/validations/addresses_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "strings" "testing" "time" @@ -111,8 +112,7 @@ func testStandardVersionNetwork(t *testing.T, network string) { standard.ContractsV300Tag, standard.ContractsV400Tag, standard.ContractsV410Tag, - // Enable whenever we upgrade the superchain registry - //standard.ContractsV500Tag, + standard.ContractsV500Tag, } for _, semver := range contractVersions { @@ -138,10 +138,12 @@ func testStandardVersion(t *testing.T, address common.Address, rpcClient *rpc.Cl w3c := w3.NewClient(rpcClient) - ver, err := semver.NewVersion(version.SystemConfig.Version) + // Semver tags from the registry include the "op-contracts/" prefix. + cleanTag := strings.TrimPrefix(strings.TrimPrefix(semverTag, "op-contracts/"), "v") + releaseVer, err := semver.NewVersion(cleanTag) require.NoError(t, err) - if ver.Major() >= 5 { + if releaseVer.Major() >= 5 { // For v5.0.0+ type implFieldDef struct { implGetter string diff --git a/ops/ai-eng/contracts-test-maintenance/VERSION b/ops/ai-eng/contracts-test-maintenance/VERSION index f0bb29e7638..88c5fb891dc 100644 --- a/ops/ai-eng/contracts-test-maintenance/VERSION +++ b/ops/ai-eng/contracts-test-maintenance/VERSION @@ -1 +1 @@ -1.3.0 +1.4.0 diff --git a/ops/ai-eng/contracts-test-maintenance/components/devin-api/devin_client.py b/ops/ai-eng/contracts-test-maintenance/components/devin-api/devin_client.py index bad0ff0e3de..cb80541ff09 100644 --- a/ops/ai-eng/contracts-test-maintenance/components/devin-api/devin_client.py +++ b/ops/ai-eng/contracts-test-maintenance/components/devin-api/devin_client.py @@ -4,7 +4,7 @@ then monitors the session until completion while logging the results. """ -from datetime import datetime +from datetime import datetime, timezone import glob import json import os @@ -45,6 +45,7 @@ def write_log(session_id, status, session_data): ranking_file = f"../tests_ranker/output/{run_id}_ranking.json" with open(ranking_file, "r") as f: + data = json.load(f) selected_files = { "test_path": data["entries"][0]["test_path"], @@ -73,8 +74,8 @@ def write_log(session_id, status, session_data): "status": status, } - # Only add PR link if status is finished - if status == "finished" and session_data: + # Add PR link if status is finished or no_changes_needed (both create PRs) + if status in ["finished", "no_changes_needed"] and session_data: pr_data = session_data.get("pull_request") or {} pr_url = pr_data.get("url") if pr_url: @@ -136,28 +137,38 @@ def create_session(prompt): headers = _create_headers(api_key, "application/json") data = json.dumps({"prompt": prompt}).encode("utf-8") - response_data = _make_request(f"{base_url}/sessions", headers, data, "POST") - session_id = response_data["session_id"] + retry_delay = 60 + while True: + response_data = _make_request(f"{base_url}/sessions", headers, data, "POST") + + if response_data is None: + print(f"Session creation timed out, retrying in {retry_delay}s...") + time.sleep(retry_delay) + retry_delay = min(retry_delay * 2, 480) + continue - print(f"Created session: {session_id}") - return session_id + session_id = response_data["session_id"] + print(f"Created session: {session_id}") + return session_id def monitor_session(session_id): """Monitor session status until completion.""" api_key, base_url = _validate_environment() headers = _create_headers(api_key) - last_status = None + last_status_enum = None retry_delay = 60 # Start with 1 minute setup_printed = False timeout_count = 0 + blocked_start_time = None # Track when we first entered blocked state + blocked_timeout = 300 # 5 minutes timeout for blocked state without outcome while True: try: - status = _make_request(f"{base_url}/sessions/{session_id}", headers) + api_response = _make_request(f"{base_url}/sessions/{session_id}", headers) # Handle server timeout (no response) - retry with backoff - if status is None: + if api_response is None: timeout_count += 1 # Only print after 3rd consecutive timeout to reduce noise if timeout_count >= 3: @@ -171,10 +182,10 @@ def monitor_session(session_id): if timeout_count > 0: timeout_count = 0 - current_status = status.get("status_enum") + status_enum = api_response.get("status_enum") # Handle Devin setup phase (status_enum is None but we got a response) - if current_status is None: + if status_enum is None: if not setup_printed: print("Devin is setting up...") setup_printed = True @@ -182,49 +193,86 @@ def monitor_session(session_id): continue # Print setup completion message once - if setup_printed and current_status: + if setup_printed and status_enum: print("Devin finished setup") setup_printed = False # Only print when status changes and is meaningful - if current_status and current_status != last_status: - print(f"Status: {current_status}") - last_status = current_status + if status_enum and status_enum != last_status_enum: + print(f"Status: {status_enum}") + last_status_enum = status_enum # Stop monitoring for terminal statuses (only if we have valid status data) - if status and current_status in ["blocked", "expired", "suspend_requested", "suspend_requested_frontend"]: + if api_response and status_enum in ["blocked", "finished", "expired", "suspend_requested", "suspend_requested_frontend"]: # Handle user stopping the session - if current_status in ["suspend_requested", "suspend_requested_frontend"]: + if status_enum in ["suspend_requested", "suspend_requested_frontend"]: print("Session stopped by user") return - # Blocked = PR created or analysis completed without changes - if current_status == "blocked": - structured_output = status.get("structured_output") or {} - pr_data = status.get("pull_request") or {} - - # Check if analysis completed without changes - if structured_output.get("analysis_complete") and not structured_output.get("changes_needed"): - reason = structured_output.get("reason", "no changes needed") - print(f"Session completed - {reason}") - write_log(session_id, "finished_no_changes", status) + # Blocked or finished - check for outcome + if status_enum in ["blocked", "finished"]: + # Ensure we have valid status data before accessing nested fields + if api_response is None: + print("Warning: Terminal status reached but no status data available, retrying...") + time.sleep(retry_delay) + continue + + # Check structured output and PR (both should be populated when blocked) + # Note: Devin API nests structured_output twice: {structured_output: {structured_output: {...}}} + # The outer structured_output can be null, so we use `or {}` to handle that case + structured = (api_response.get("structured_output") or {}).get("structured_output") or {} + analysis_complete = structured.get("analysis_complete", False) + changes_needed = structured.get("changes_needed") + + pr_data = api_response.get("pull_request") or {} + pr_url = pr_data.get("url") + + # Case 1: Structured output indicates no changes needed + if analysis_complete and changes_needed is False: + reason = structured.get("reason", "Not provided") + print(f"Session completed - no changes needed") + print(f"Reason: {reason}") + if pr_url: + print(f"PR created for TOML tracking: {pr_url}") + + write_log(session_id, "no_changes_needed", api_response) return - # Check if PR was created - if pr_data.get("url"): - print("Session completed successfully - PR created") - write_log(session_id, "finished", status) + # Case 2: PR created with test improvements (only if no structured output yet) + # We need to wait for structured_output to determine if this is a no-changes case + if pr_url and analysis_complete: + # We have both PR and completed analysis, and changes_needed != False + # This means actual test improvements were made + print(f"Session completed successfully - PR created: {pr_url}") + write_log(session_id, "finished", api_response) return - # Blocked without completion signal - print(f"Session blocked without PR - check Devin web interface") - # Don't write log.json so artifact won't be stored for failed sessions - sys.exit(1) # Exit with error code to mark job as failed + # If blocked without complete data, keep waiting briefly + # Devin may still be populating the data + if status_enum == "blocked": + if blocked_start_time is None: + blocked_start_time = time.time() + print("Devin is blocked - waiting for complete outcome data...") + + elapsed = time.time() - blocked_start_time + if elapsed > blocked_timeout: + print(f"Timeout: Devin blocked for {int(elapsed)}s without outcome - check Devin web interface") + sys.exit(1) + + time.sleep(5) + continue + + # Reset blocked timer if we move out of blocked state + blocked_start_time = None + + # Finished without PR and no structured output = error + print(f"Session finished without PR or clear outcome - check Devin web interface") + sys.exit(1) # Expired = session timed out - if current_status == "expired": + if status_enum == "expired": print(f"Session expired") - write_log(session_id, "expired", status) + write_log(session_id, "expired", api_response) return time.sleep(5) diff --git a/ops/ai-eng/contracts-test-maintenance/components/prompt-renderer/render.py b/ops/ai-eng/contracts-test-maintenance/components/prompt-renderer/render.py index 62639cbca21..bff63500124 100644 --- a/ops/ai-eng/contracts-test-maintenance/components/prompt-renderer/render.py +++ b/ops/ai-eng/contracts-test-maintenance/components/prompt-renderer/render.py @@ -8,7 +8,7 @@ def load_ranking_data(): - """Load the ranking JSON file and return the first entry and run_id.""" + """Load the ranking JSON file and return the first entry, stale entries, and run_id.""" ranking_dir = Path(__file__).parent / "../tests_ranker" / "output" # Get the ranking file @@ -23,7 +23,31 @@ def load_ranking_data(): if not data.get("entries"): raise ValueError(f"No entries found in {ranking_file.name}") - return data["entries"][0], run_id + stale_toml_entries = data.get("stale_toml_entries", []) + + return data["entries"][0], stale_toml_entries, run_id + + +def format_stale_entries(stale_entries): + """Format stale TOML entries as markdown list. + + Args: + stale_entries: List of dicts with test_path, contract_path, old_hash, new_hash + + Returns: + Formatted markdown string with bullet list of stale entries, or "(none)" if empty + """ + if not stale_entries: + return "(none)" + + lines = [] + for entry in stale_entries: + test_path = entry.get("test_path", "unknown") + old_hash = entry.get("old_hash", "unknown")[:7] + new_hash = entry.get("new_hash", "unknown")[:7] + lines.append(f"- `{test_path}` (contract changed: {old_hash} → {new_hash})") + + return "\n".join(lines) def load_prompt_template(): @@ -34,10 +58,22 @@ def load_prompt_template(): return f.read() -def render_prompt(template, test_path, contract_path): - """Replace the placeholders in the template with actual paths.""" - return template.replace("{TEST_PATH}", test_path).replace( - "{CONTRACT_PATH}", contract_path +def render_prompt(template, test_path, contract_path, stale_entries_list): + """Replace the placeholders in the template with actual paths and stale entries. + + Args: + template: The prompt template string + test_path: Path to the test file + contract_path: Path to the contract file + stale_entries_list: Formatted markdown list of stale entries + + Returns: + Rendered prompt with all placeholders replaced + """ + return ( + template.replace("{TEST_PATH}", test_path) + .replace("{CONTRACT_PATH}", contract_path) + .replace("{{STALE_ENTRIES_LIST}}", stale_entries_list) ) @@ -63,7 +99,7 @@ def main(): """Main function to render and save the prompt instance.""" try: # Load ranking data and get run_id - first_entry, run_id = load_ranking_data() + first_entry, stale_toml_entries, run_id = load_ranking_data() test_path = first_entry["test_path"] contract_path = first_entry["contract_path"] @@ -71,11 +107,16 @@ def main(): print(f" Test path: {test_path}") print(f" Contract path: {contract_path}") + # Format stale entries for injection + stale_entries_list = format_stale_entries(stale_toml_entries) + if stale_toml_entries: + print(f" Stale TOML entries: {len(stale_toml_entries)}") + # Load prompt template template = load_prompt_template() - # Render the prompt with actual paths - rendered_prompt = render_prompt(template, test_path, contract_path) + # Render the prompt with actual paths and stale entries + rendered_prompt = render_prompt(template, test_path, contract_path, stale_entries_list) # Save the rendered prompt output_file = save_prompt_instance(rendered_prompt, run_id) diff --git a/ops/ai-eng/contracts-test-maintenance/components/slack-notifier/build_notification.sh b/ops/ai-eng/contracts-test-maintenance/components/slack-notifier/build_notification.sh new file mode 100755 index 00000000000..db7e03bb086 --- /dev/null +++ b/ops/ai-eng/contracts-test-maintenance/components/slack-notifier/build_notification.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +set -eo pipefail + +LOG_FILE="$1" + +if [ -z "$LOG_FILE" ]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +STATUS=$(jq -r '.status // empty' "$LOG_FILE") +PR_URL=$(jq -r '.pull_request_url // empty' "$LOG_FILE") +TEST_FILE=$(jq -r '.selected_files.test_path | split("/") | .[-1]' "$LOG_FILE") + +if [ "$STATUS" = "no_changes_needed" ] && [ -n "$PR_URL" ]; then + # No changes needed but PR opened to add TOML tracking entry + MESSAGE=$' AI Contracts Test Maintenance System analyzed '"${TEST_FILE}"$' - no changes needed (test coverage already comprehensive)\n<'"${PR_URL}"$'|View PR to add no-changes tracking>' + SLACK_JSON=$(jq -n --arg msg "$MESSAGE" '{"text": $msg}') + echo "$SLACK_JSON" +elif [ -n "$PR_URL" ]; then + # Normal case: PR with test improvements + MESSAGE=$' AI Contracts Test Maintenance System created a PR for '"${TEST_FILE}"$'\n<'"${PR_URL}"$'|View PR> | ' + SLACK_JSON=$(jq -n --arg msg "$MESSAGE" '{"text": $msg}') + echo "$SLACK_JSON" +elif [ "$STATUS" = "no_changes_needed" ]; then + # Edge case: no changes and no PR (shouldn't happen with new workflow) + MESSAGE=$' AI Contracts Test Maintenance System analyzed '"${TEST_FILE}"$' - no changes needed (test coverage already comprehensive)' + SLACK_JSON=$(jq -n --arg msg "$MESSAGE" '{"text": $msg}') + echo "$SLACK_JSON" +else + echo "No notification needed (status: $STATUS)" >&2 + echo '{}' +fi diff --git a/ops/ai-eng/contracts-test-maintenance/components/slack-notifier/prepare_notification.sh b/ops/ai-eng/contracts-test-maintenance/components/slack-notifier/prepare_notification.sh deleted file mode 100755 index 1c366ad2962..00000000000 --- a/ops/ai-eng/contracts-test-maintenance/components/slack-notifier/prepare_notification.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Prepare Slack notification for AI Contracts Test Maintenance System results -# Outputs a JSON message to be used with CircleCI Slack orb - -LOG_FILE="../../log.json" - -# Extract data from log file -PR_URL=$(jq -r '.pull_request_url // empty' "$LOG_FILE") -TEST_FILE=$(jq -r '.selected_files.test_path | split("/") | .[-1]' "$LOG_FILE") -STATUS=$(jq -r '.status // empty' "$LOG_FILE") - -# Prepare message based on outcome -if [ -n "$PR_URL" ]; then - # PR was created - notify team for review - MESSAGE=$' AI Contracts Test Maintenance System created a PR for '"${TEST_FILE}"$'\n<'"${PR_URL}"$'|View PR> | ' - SLACK_JSON=$(jq -n --arg msg "$MESSAGE" '{"text": $msg}') - echo "export AI_PR_SLACK_TEMPLATE='${SLACK_JSON}'" -elif [ "$STATUS" = "finished_no_changes" ]; then - # Analysis complete but no changes needed - informational only - MESSAGE=$'AI Contracts Test Maintenance System analyzed '"${TEST_FILE}"$' - no changes needed (test coverage is already comprehensive)' - SLACK_JSON=$(jq -n --arg msg "$MESSAGE" '{"text": $msg}') - echo "export AI_PR_SLACK_TEMPLATE='${SLACK_JSON}'" -else - # No notification needed - echo "No PR created, skipping notification" - echo "export AI_PR_SLACK_TEMPLATE=''" -fi diff --git a/ops/ai-eng/contracts-test-maintenance/components/tests_ranker/test_ranker.py b/ops/ai-eng/contracts-test-maintenance/components/tests_ranker/test_ranker.py index 49de537d94c..d1881ea9bf2 100644 --- a/ops/ai-eng/contracts-test-maintenance/components/tests_ranker/test_ranker.py +++ b/ops/ai-eng/contracts-test-maintenance/components/tests_ranker/test_ranker.py @@ -20,6 +20,36 @@ # === Git Utilities === +def get_file_commit_hash(file_path: Path, repo_root: Path) -> Optional[str]: + """Get the commit hash of the last commit that modified a file. + + Args: + file_path: Path to the file. + repo_root: Path to the git repository root. + + Returns: + Full SHA-1 hash of the last commit, or None if unable to determine. + """ + try: + relative_path = file_path.relative_to(repo_root) + + result = subprocess.run( + ["git", "log", "-1", "--format=%H", "--", str(relative_path)], + cwd=repo_root, + capture_output=True, + text=True, + check=True, + ) + + if result.stdout.strip(): + return result.stdout.strip() + + except (subprocess.CalledProcessError, ValueError, OSError): + pass + + return None + + def get_file_commit_timestamp(file_path: Path, repo_root: Path) -> Optional[int]: """Get the timestamp of the last commit that modified a file. @@ -306,14 +336,97 @@ def fetch_last_processed_from_circleci() -> list[Path]: return [] -def load_exclusions(contracts_bedrock: Path) -> tuple[list[Path], set[Path]]: +def fetch_no_changes_needed_exclusions(contracts_bedrock: Path, repo_root: Path) -> tuple[set[Path], list[dict]]: + """Load 'no changes needed' tests from TOML file, detecting stale entries. + + Reads tracking file from no-need-changes.toml, checks each entry's contract hash. + Returns both tests to exclude (unchanged contracts) and stale entries (changed contracts). + + Args: + contracts_bedrock: Path to the contracts-bedrock directory. + repo_root: Path to the git repository root. + + Returns: + Tuple of (excluded_tests, stale_entries): + - excluded_tests: Set of test paths to exclude (contracts haven't changed) + - stale_entries: List of dicts with stale entry info for Devin to clean up + """ + try: + print("Checking no-need-changes.toml for tracked tests...") + + toml_file = Path(__file__).parent.parent.parent / "no-need-changes.toml" + + if not toml_file.exists(): + print("No tracking file found") + return set(), [] + + with toml_file.open("rb") as f: + tracking_data = tomllib.load(f) + + tests = tracking_data.get("tests", []) + if not tests: + print("No tracked tests found") + return set(), [] + + print(f"Loaded {len(tests)} tracked test(s)") + + excluded_tests = set() + stale_entries = [] + + for entry in tests: + test_path = entry.get("test_path") + contract_path = entry.get("contract_path") + recorded_hash = entry.get("contract_hash") + + if not test_path or not contract_path or not recorded_hash: + continue + + # Get current contract hash + full_contract_path = contracts_bedrock / contract_path + current_hash = get_file_commit_hash(full_contract_path, repo_root) + + if not current_hash: + # Can't get hash - skip this entry + continue + + if current_hash == recorded_hash: + # Contract unchanged - exclude from ranking + excluded_tests.add(Path(test_path)) + print(f" Excluding: {test_path} (contract unchanged)") + else: + # Contract changed - entry is stale (Devin should remove it) + print(f" Stale entry: {test_path} (contract changed: {recorded_hash[:7]} → {current_hash[:7]})") + stale_entries.append({ + "test_path": test_path, + "contract_path": contract_path, + "old_hash": recorded_hash, + "new_hash": current_hash + }) + + if excluded_tests: + print(f"✓ Excluding {len(excluded_tests)} test(s) with unchanged contracts") + + if stale_entries: + print(f"⚠ Found {len(stale_entries)} stale entry(ies) - Devin will clean these up") + + return excluded_tests, stale_entries + + except Exception as e: + print(f"Could not fetch tracking file: {e}") + return set(), [] + + +def load_exclusions(contracts_bedrock: Path) -> tuple[list[Path], set[Path], list[dict]]: """Load and normalize exclusion paths from TOML configuration. Args: contracts_bedrock: Path to the contracts-bedrock directory. Returns: - Tuple of (excluded_dirs, excluded_files) as normalized Path objects. + Tuple of (excluded_dirs, excluded_files, stale_toml_entries): + - excluded_dirs: List of excluded directory paths + - excluded_files: Set of excluded file paths + - stale_toml_entries: List of stale entries from no-need-changes.toml Raises: FileNotFoundError: If exclusions.toml file is not found. @@ -347,7 +460,12 @@ def load_exclusions(contracts_bedrock: Path) -> tuple[list[Path], set[Path]]: for test_file in last_processed_files: excluded_files.add(test_file) - return excluded_dirs, excluded_files + # Add TOML-based "no changes needed" exclusions (permanent until contract changes) + repo_root = get_base_paths()[0] + no_changes_exclusions, stale_toml_entries = fetch_no_changes_needed_exclusions(contracts_bedrock, repo_root) + excluded_files.update(no_changes_exclusions) + + return excluded_dirs, excluded_files, stale_toml_entries def is_path_excluded( @@ -412,7 +530,10 @@ def filter_excluded_files( def generate_ranking_json( - entries: list[dict[str, str | int | float | None]], output_dir: Path, run_id: str + entries: list[dict[str, str | int | float | None]], + output_dir: Path, + run_id: str, + stale_toml_entries: list[dict] = None ) -> Path: """Generate the ranking JSON file. @@ -420,10 +541,13 @@ def generate_ranking_json( entries: List of test-to-contract mappings with scores. output_dir: Directory to write the output file. run_id: Timestamp-based run identifier. + stale_toml_entries: List of stale entries from no-need-changes.toml (optional). Returns: Path to the generated JSON file. """ + if stale_toml_entries is None: + stale_toml_entries = [] # Ensure output directory exists output_dir.mkdir(parents=True, exist_ok=True) @@ -441,6 +565,7 @@ def generate_ranking_json( "run_id": run_id, "generated_at": datetime.now(timezone.utc).isoformat(), "entries": sorted_entries, + "stale_toml_entries": stale_toml_entries, } # Write to output file with run_id @@ -539,16 +664,16 @@ def main() -> None: # Get base paths repo_root, contracts_bedrock, output_dir = get_base_paths() - # Load exclusions - excluded_dirs, excluded_files = load_exclusions(contracts_bedrock) + # Load exclusions (now also returns stale TOML entries) + excluded_dirs, excluded_files, stale_toml_entries = load_exclusions(contracts_bedrock) # Collect test entries entries = collect_test_entries( contracts_bedrock, excluded_dirs, excluded_files, repo_root ) - # Generate ranking JSON with run_id - output_file = generate_ranking_json(entries, output_dir, run_id) + # Generate ranking JSON with run_id and stale entries + output_file = generate_ranking_json(entries, output_dir, run_id, stale_toml_entries) print(f"Generated {output_file} with {len(entries)} entries") print(f"Run ID: {run_id}") diff --git a/ops/ai-eng/contracts-test-maintenance/docs/runbook.md b/ops/ai-eng/contracts-test-maintenance/docs/runbook.md index 582d321c2f2..5da50e030cd 100644 --- a/ops/ai-eng/contracts-test-maintenance/docs/runbook.md +++ b/ops/ai-eng/contracts-test-maintenance/docs/runbook.md @@ -15,6 +15,8 @@ The system uses a **two-branch scoring algorithm**: - **Automated CI Integration**: Runs twice weekly on schedule (Monday/Thursday) or on-demand - **Smart Prioritization**: Focuses on tests that are most out of sync with their contracts - **Duplicate Prevention**: Automatically excludes recently processed files (last 2 weeks) +- **No-Changes Tracking**: Tests with comprehensive coverage tracked in TOML file to avoid redundant work +- **Stale Entry Detection**: Automatically identifies when tracked tests need re-analysis due to contract changes - **Resilient Monitoring**: Handles long-running Devin sessions with retry logic - **Full Audit Trail**: All runs logged with complete traceability @@ -26,7 +28,8 @@ The system uses a **two-branch scoring algorithm**: contracts-test-maintenance/ ├── VERSION # System version ├── exclusion.toml # Static exclusions configuration -├── log.json # Latest execution log +├── no-need-changes.toml # Tests with comprehensive coverage (auto-managed) +├── log.jsonl # Execution history and results ├── prompt/ │ └── prompt.md # AI instruction template (~2000 lines) ├── components/ @@ -36,10 +39,8 @@ contracts-test-maintenance/ │ ├── prompt-renderer/ # Stage 2: Prompt generation │ │ ├── render.py │ │ └── output/{run_id}_prompt.md -│ ├── devin-api/ # Stage 3: AI execution -│ │ └── devin_client.py -│ └── slack-notifier/ # Slack notification preparation -│ └── prepare_notification.sh +│ └── devin-api/ # Stage 3: AI execution +│ └── devin_client.py └── docs/ └── runbook.md # This document ``` @@ -83,7 +84,9 @@ ai-contracts-test: **Slack Notifications**: - Automatic notification posted to #evm-safety Slack channel when PR is created -- Includes PR URL, test file information, and link to reviewer guide +- Two notification types: + - **Test improvements**: Includes PR URL, test file info, and reviewer guide link + - **No changes needed**: Notifies team that test has comprehensive coverage with PR for TOML tracking update - Helps expedite review process by alerting reviewers immediately **In CircleCI**: @@ -166,6 +169,14 @@ The `just rank` command generates `components/tests_ranker/output/{run_id}_ranki "staleness_days": -98.21, "score": 135.84 } + ], + "stale_toml_entries": [ + { + "test_path": "test/L1/SystemConfig.t.sol", + "contract_path": "src/L1/SystemConfig.sol", + "old_hash": "abc1234", + "new_hash": "def5678" + } ] } ``` @@ -180,10 +191,14 @@ The `just rank` command generates `components/tests_ranker/output/{run_id}_ranki - `contract_commit_ts` - Unix timestamp of contract file's last commit - `staleness_days` - Calculated staleness (positive = contract newer) - `score` - Priority score (higher = more urgent) +- `stale_toml_entries` - Array of no-need-changes.toml entries that need removal (contract hash changed) ### Prompt Renderer Output -The `just render` command generates a markdown file in `components/prompt-renderer/output/` with the name format `{run_id}_prompt.md`. This file contains the AI prompt template with the highest-priority test and contract paths filled in, ready to be used for test maintenance analysis. +The `just render` command generates a markdown file in `components/prompt-renderer/output/` with the name format `{run_id}_prompt.md`. This file contains the AI prompt template with: +- The highest-priority test and contract paths filled in +- List of stale no-need-changes.toml entries (if any) that Devin should remove before starting analysis +- Instructions for adding new entries when no changes are needed For example, a run with ID `20250922_143052` will generate `20250922_143052_prompt.md`. The system automatically links prompts to their corresponding ranking runs through the shared run ID. @@ -239,17 +254,26 @@ All Devin sessions are automatically logged to `log.json` with: - `run_time` - Human-readable timestamp of the run - `devin_session_id` - Unique Devin session identifier - `selected_files` - The test-contract pair that was worked on -- `status` - Final session status ("finished", "finished_no_changes", "blocked", "expired") -- `pull_request_url` - GitHub PR URL (only present if status is "finished") +- `status` - Final session status ("finished", "no_changes_needed", "blocked", "expired", "failed") +- `pull_request_url` - GitHub PR URL (present for both "finished" and "no_changes_needed" statuses) #### Duplicate Prevention -The ranking system automatically excludes files processed in the **last 2 weeks** to prevent duplicate work: +The ranking system uses two complementary strategies to prevent duplicate work: + +**1. No-Changes Tracking (with automatic reintegration)**: +- Tests with comprehensive coverage tracked in `no-need-changes.toml` +- Each entry includes contract git hash for validation +- Automatically excluded from ranking while contract remains unchanged +- **Automatically reintegrated** when contract changes (hash mismatch detected) +- Stale entries flagged in ranking output for Devin to remove + +**2. Recent Processing Exclusion (2-week cooldown)**: - Queries CircleCI API for recent successful runs - Extracts test paths from stored `log.json` artifacts - Temporarily excludes these files from ranking - Files become available again after 2 weeks -- This prevents immediate re-ranking of files still under review +- Prevents immediate re-ranking of files still under review ## Configuration @@ -320,8 +344,12 @@ else: 3. Get git commit timestamps using `git log -1 --format=%ct` 4. Calculate staleness: `contract_commit_ts - test_commit_ts` 5. Calculate priority score using two-branch algorithm -6. Apply exclusions (static from `exclusion.toml` + dynamic from CircleCI) -7. Sort by score (descending) and output to JSON +6. Apply exclusions: + - Static exclusions from `exclusion.toml` + - Dynamic exclusions from CircleCI artifacts (2-week window) + - **No-changes tracking from `no-need-changes.toml`** (excluded while contract hash matches) +7. Detect stale TOML entries (contract hash changed since entry was added) +8. Sort by score (descending) and output to JSON with stale entries **Output Fields**: - `test_path`: Relative path from `contracts-bedrock/` @@ -330,20 +358,25 @@ else: - `contract_commit_ts`: Unix timestamp (seconds since epoch) - `staleness_days`: Float, positive = contract is newer - `score`: Float, higher = more urgent attention needed +- `stale_toml_entries`: Array of entries that need removal (contract changed) ### Stage 2: Prompt Rendering **Process**: 1. Load ranking JSON from Stage 1 output -2. Extract first entry (highest priority test) -3. Load prompt template from `prompt/prompt.md` -4. Replace placeholders: +2. Extract first entry (highest priority test) and stale TOML entries +3. Format stale entries as markdown bullet list +4. Load prompt template from `prompt/prompt.md` +5. Replace placeholders: - `{TEST_PATH}` → test file path - `{CONTRACT_PATH}` → contract file path -5. Save rendered prompt to `output/` with same `run_id` + - `{{STALE_ENTRIES_LIST}}` → formatted list of stale entries (or "(none)") +6. Save rendered prompt to `output/` with same `run_id` **The Prompt Template** contains: - Role definition and task instructions +- **Stale entries cleanup instructions** (if any entries flagged) +- **No-changes tracking instructions** (how to add entries when no improvements needed) - Comprehensive testing methodology (4 phases) - Naming conventions for test contracts and functions - Fuzz testing decision trees @@ -356,24 +389,31 @@ else: **Process**: 1. Find latest prompt file from Stage 2 -2. Create Devin session via POST to `/sessions` endpoint +2. Create Devin session via POST to `/sessions` endpoint with session creation retry logic 3. Monitor session with polling: - Poll every 30 seconds for status updates + - Check for blocked state with 5-minute timeout + - Parse `structured_output` for completion signal - Implement exponential backoff for server errors (502/504) - Continue monitoring until terminal state reached -4. Log results to `log.json` with full session details +4. Determine final status based on Devin state and structured output +5. Log results to `log.json` with full session details **Devin API Terminal States**: -- `blocked`: Devin reached a blocking state (e.g., needs approval, PR created) +- `blocked`: Devin reached a blocking state (e.g., needs approval, PR created, or waiting) - `expired`: Session timeout reached - `suspend_requested` / `suspend_requested_frontend`: User manually stopped session -**Logged Status Values** (what gets written to `log.json`): -- `finished`: Devin status was "blocked" AND PR was successfully created -- `finished_no_changes`: Devin completed analysis and determined no changes needed (uses structured output) -- `blocked`: Devin status was "blocked" without PR URL or completion signal -- `expired`: Session timed out -- Note: User-stopped sessions are not logged +**Status Detection Logic**: +The client distinguishes between different outcomes by examining both Devin API status and structured output: + +1. **`finished`**: Devin blocked + PR created for test improvements +2. **`no_changes_needed`**: Devin blocked + structured_output indicates comprehensive coverage + PR created for TOML entry +3. **`blocked`**: Devin blocked but no PR or unclear state +4. **`expired`**: Session timeout +5. User-stopped sessions are not logged + +**Note**: The `status` field in `log.json` represents our interpretation of what happened, not the raw Devin API status. **Error Handling**: - 30-second timeout per HTTP request diff --git a/ops/ai-eng/contracts-test-maintenance/exclusion.toml b/ops/ai-eng/contracts-test-maintenance/exclusion.toml index 76a04639b1b..d049863db28 100644 --- a/ops/ai-eng/contracts-test-maintenance/exclusion.toml +++ b/ops/ai-eng/contracts-test-maintenance/exclusion.toml @@ -22,6 +22,5 @@ files = [ "test/universal/BenchmarkTest.t.sol", "test/universal/ExtendedPause.t.sol", "test/vendor/Initializable.t.sol", - "test/vendor/InitializableOZv5.t.sol", - "test/universal/ReinitializableBase.t.sol" + "test/vendor/InitializableOZv5.t.sol" ] diff --git a/ops/ai-eng/contracts-test-maintenance/no-need-changes.toml b/ops/ai-eng/contracts-test-maintenance/no-need-changes.toml new file mode 100644 index 00000000000..9b1330b9c16 --- /dev/null +++ b/ops/ai-eng/contracts-test-maintenance/no-need-changes.toml @@ -0,0 +1,32 @@ +# Files that don't need changes +# When a test file is analyzed and determined to not need changes, +# add an entry here with the test path, contract path, and contract hash. +# Entries are automatically removed when the contract hash changes. + +# Example entry: +# [[tests]] +# test_path = "test/L1/SystemConfig.t.sol" +# contract_path = "src/L1/SystemConfig.sol" +# contract_hash = "abc123..." +# recorded_at = "2025-12-11T20:00:00Z" +# devin_session_id = "devin-abc123" +# run_id = "20251211_200000" +# reason = "Test coverage is comprehensive" + +[[tests]] +test_path = "test/universal/ReinitializableBase.t.sol" +contract_path = "src/universal/ReinitializableBase.sol" +contract_hash = "cbe992160b4978a49dd92949106531f7bd6e2029" +recorded_at = "2025-12-12T17:18:04Z" +devin_session_id = "677a871d381f44848ff297549125765c" +run_id = "20251212_171800" +reason = "Test coverage is already comprehensive with all functions and code paths tested. The constructor has both focused (zero version revert) and fuzz tests (valid versions 1-255), and the initVersion() getter is verified within the constructor test." + +[[tests]] +test_path = "test/universal/SafeSend.t.sol" +contract_path = "src/universal/SafeSend.sol" +contract_hash = "35f7553422a7b81ad998d04ed1f67e1fa56d6b8d" +recorded_at = "2025-12-12T17:32:49Z" +devin_session_id = "50e7ca21d9264fe6a62c7ec944da7e43" +run_id = "20251212_173200" +reason = "Test coverage is already comprehensive with all functions and code paths tested. The constructor has three fuzz tests covering EOA, contract, and zero address recipients with thorough assertions." diff --git a/ops/ai-eng/contracts-test-maintenance/prompt/prompt.md b/ops/ai-eng/contracts-test-maintenance/prompt/prompt.md index 38e350fe4c5..7ec2c73501f 100644 --- a/ops/ai-eng/contracts-test-maintenance/prompt/prompt.md +++ b/ops/ai-eng/contracts-test-maintenance/prompt/prompt.md @@ -28,10 +28,44 @@ Only make changes you're confident about - analyze code behavior before testing. Don't guess or assume - if unsure, examine the source contract carefully. + +1. NO creating NEW tests for inherited functions - only test functions declared in target contract +2. NO creating test contracts for constructor parameters - use Constructor_Test instead +3. NO failing tests kept - all must pass or task fails +4. NO removing ANY existing tests - even if they test inherited functions (enhance/modify instead) + + + +- Enhancement First: Always improve existing tests before adding new ones +- Function-First Organization: Every function gets its own test contract; Uncategorized_Test is reserved for true multi-function integration scenarios +- Preserve Behavior: Modify tests only to improve coverage/naming while keeping original functionality +- Contract Reality: Test what the contract DOES, not what you think it SHOULD do +- Target Contract Only: Never test inherited functions - only test functions declared in the contract under test +- Test Valid Scenarios: Focus on legitimate use cases and edge cases, not artificial failure modes from broken setup +- Test Intent vs Side Effects: Verify tests fail for their intended reason, not technical side effects +- Test Uniqueness: Each test must verify distinct logic - different values alone don't justify separate tests + + + +{TEST_PATH} +{CONTRACT_PATH} + + + +Enhance the provided Solidity test file following these objectives: +1. Convert regular tests to fuzz tests where appropriate +2. Add tests for uncovered code paths (if statements, branches, reverts) +3. Ensure every public/external function has at least one test + +Focus on mechanical improvements that increase coverage and quality. + + -You MUST maintain a `structured_output` field to communicate task completion status. Please update the structured output immediately after completing your analysis (Phases 1-2) to indicate whether changes are needed. +**IMPORTANT: You MUST maintain a `structured_output` field to communicate task status.** + +Please update the structured output **immediately after completing Phases 1-2 (Enhancement Analysis & Coverage Gap Analysis)** to indicate whether changes are needed. -**Required Format:** +**Required JSON Structure:** ```json { "analysis_complete": boolean, @@ -40,12 +74,13 @@ You MUST maintain a `structured_output` field to communicate task completion sta } ``` +**Field Definitions:** +- `analysis_complete`: Set to `true` as soon as you determine the outcome after Phases 1-2 +- `changes_needed`: Set to `true` if you will create or modify tests; `false` if no changes needed +- `reason`: Brief explanation (1-2 sentences) of why changes are/aren't needed + **When to Update:** -- Update immediately after completing Phases 1-2 (Enhancement Analysis & Coverage Gap Analysis) -- Set `analysis_complete: true` as soon as you determine the outcome -- Set `changes_needed: true` if you will create or modify tests -- Set `changes_needed: false` if no changes are needed after thorough analysis -- Set `reason` with a brief explanation of the outcome +Update the structured output immediately after completing your analysis (Phases 1-2), before starting implementation work. This allows the system to detect when no changes are required. **Examples:** @@ -54,7 +89,7 @@ No changes needed: { "analysis_complete": true, "changes_needed": false, - "reason": "Test coverage is already comprehensive with all functions and code paths tested" + "reason": "Test coverage is already comprehensive with all functions and code paths tested. The constructor has both focused and fuzz tests covering all valid inputs." } ``` @@ -63,42 +98,63 @@ Changes needed: { "analysis_complete": true, "changes_needed": true, - "reason": "Converting 3 tests to fuzz tests and adding coverage for 2 untested functions" + "reason": "Converting 3 tests to fuzz tests and adding coverage for 2 untested error conditions in the validate() function." } ``` + +**Critical:** Set this output BEFORE proceeding to Phase 3. If `changes_needed: false`, you can stop after Phase 2. - -1. NO creating NEW tests for inherited functions - only test functions declared in target contract -2. NO creating test contracts for constructor parameters - use Constructor_Test instead -3. NO failing tests kept - all must pass or task fails -4. NO removing ANY existing tests - even if they test inherited functions (enhance/modify instead) - + +**IMPORTANT: When no changes are needed (`changes_needed: false`), you MUST update the tracking file.** - -- Enhancement First: Always improve existing tests before adding new ones -- Function-First Organization: Every function gets its own test contract; Uncategorized_Test is reserved for true multi-function integration scenarios -- Preserve Behavior: Modify tests only to improve coverage/naming while keeping original functionality -- Contract Reality: Test what the contract DOES, not what you think it SHOULD do -- Target Contract Only: Never test inherited functions - only test functions declared in the contract under test -- Test Valid Scenarios: Focus on legitimate use cases and edge cases, not artificial failure modes from broken setup -- Test Intent vs Side Effects: Verify tests fail for their intended reason, not technical side effects -- Test Uniqueness: Each test must verify distinct logic - different values alone don't justify separate tests - +If your analysis determines that no changes are needed, add an entry to `ops/ai-eng/contracts-test-maintenance/no-need-changes.toml` with the following information: - -{TEST_PATH} -{CONTRACT_PATH} - +```toml +[[tests]] +test_path = "{TEST_PATH}" +contract_path = "{CONTRACT_PATH}" +contract_hash = "" +recorded_at = "" +devin_session_id = "" +run_id = "" +reason = "" +``` - -Enhance the provided Solidity test file following these objectives: -1. Convert regular tests to fuzz tests where appropriate -2. Add tests for uncovered code paths (if statements, branches, reverts) -3. Ensure every public/external function has at least one test +**How to get the contract hash:** +Run this command: `git log -1 --format=%H -- packages/contracts-bedrock/{CONTRACT_PATH}` + +**Example entry:** +```toml +[[tests]] +test_path = "test/L1/SystemConfig.t.sol" +contract_path = "src/L1/SystemConfig.sol" +contract_hash = "abc123def456..." +recorded_at = "2025-12-11T20:30:00Z" +devin_session_id = "devin-abc123" +run_id = "20251211_203000" +reason = "Test coverage is already comprehensive with all functions and code paths tested." +``` -Focus on mechanical improvements that increase coverage and quality. - +**Critical Steps:** +1. Get the contract hash using the git command above +2. Add the entry to the TOML file +3. Commit this change with message: `chore(ai-test): skip {TEST_PATH} - already has comprehensive coverage` +4. **Create a pull request** with the default template to record this decision + +This tracking allows the system to automatically exclude well-tested files from future runs until the contract changes. + + + +**IMPORTANT: Clean up stale TOML entries before starting your analysis.** + +The following entries in `ops/ai-eng/contracts-test-maintenance/no-need-changes.toml` have outdated contract hashes and must be removed: + +{{STALE_ENTRIES_LIST}} + +Remove these entries from the TOML file and commit with message: +`chore(ai-test): remove stale entries from no-need-changes.toml` + **Structured Enhancement Methodology** diff --git a/ops/ai-eng/justfile b/ops/ai-eng/justfile index 33cdf858fbb..4af89c07777 100644 --- a/ops/ai-eng/justfile +++ b/ops/ai-eng/justfile @@ -30,6 +30,8 @@ ai-contracts-test: # Step 3: Send to Devin just devin -# Prepare Slack notification based on log.json results -prepare-slack-notification: - cd contracts-test-maintenance/components/slack-notifier && ./prepare_notification.sh +# Build Slack notification JSON from log.json results +build-slack-notification: + #!/usr/bin/env bash + SLACK_JSON=$(bash contracts-test-maintenance/components/slack-notifier/build_notification.sh contracts-test-maintenance/log.json) + echo "export AI_PR_SLACK_TEMPLATE='${SLACK_JSON}'" diff --git a/ops/docker/op-stack-go/Dockerfile b/ops/docker/op-stack-go/Dockerfile index 57052e7101b..f07beb6b4d8 100644 --- a/ops/docker/op-stack-go/Dockerfile +++ b/ops/docker/op-stack-go/Dockerfile @@ -170,6 +170,11 @@ ARG OP_SUPERNODE_VERSION=v0.0.0 RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build cd op-supernode && make op-supernode \ GOOS=$TARGETOS GOARCH=$TARGETARCH GITCOMMIT=$GIT_COMMIT GITDATE=$GIT_DATE VERSION="$OP_SUPERNODE_VERSION" +FROM --platform=$BUILDPLATFORM builder AS op-interop-filter-builder +ARG OP_INTEROP_FILTER_VERSION=v0.0.0 +RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build cd op-interop-filter && make op-interop-filter \ + GOOS=$TARGETOS GOARCH=$TARGETARCH GITCOMMIT=$GIT_COMMIT GITDATE=$GIT_DATE VERSION="$OP_INTEROP_FILTER_VERSION" + FROM --platform=$BUILDPLATFORM builder AS op-test-sequencer-builder ARG OP_TEST_SEQUENCER_VERSION=v0.0.0 RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build cd op-test-sequencer && make op-test-sequencer \ @@ -262,6 +267,10 @@ FROM $TARGET_BASE_IMAGE AS op-supernode-target COPY --from=op-supernode-builder /app/op-supernode/bin/op-supernode /usr/local/bin/ CMD ["op-supernode"] +FROM $TARGET_BASE_IMAGE AS op-interop-filter-target +COPY --from=op-interop-filter-builder /app/op-interop-filter/bin/op-interop-filter /usr/local/bin/ +CMD ["op-interop-filter"] + FROM $TARGET_BASE_IMAGE AS op-test-sequencer-target COPY --from=op-test-sequencer-builder /app/op-test-sequencer/bin/op-test-sequencer /usr/local/bin/ CMD ["op-test-sequencer"] diff --git a/ops/docker/op-stack-go/Dockerfile.dockerignore b/ops/docker/op-stack-go/Dockerfile.dockerignore index 280b1603456..db630bdb08b 100644 --- a/ops/docker/op-stack-go/Dockerfile.dockerignore +++ b/ops/docker/op-stack-go/Dockerfile.dockerignore @@ -20,6 +20,7 @@ !/op-service !/op-supervisor !/op-supernode +!/op-interop-filter !/op-test-sequencer !/op-wheel !/op-alt-da diff --git a/packages/contracts-bedrock/book/src/contributing/style-guide.md b/packages/contracts-bedrock/book/src/contributing/style-guide.md index 4a5981b2499..65057018ae0 100644 --- a/packages/contracts-bedrock/book/src/contributing/style-guide.md +++ b/packages/contracts-bedrock/book/src/contributing/style-guide.md @@ -3,29 +3,31 @@ -- [Standards and Conventions](#standards-and-conventions) - - [Style](#style) - - [Comments](#comments) - - [Errors](#errors) - - [Function Parameters](#function-parameters) - - [Function Return Arguments](#function-return-arguments) - - [Event Parameters](#event-parameters) - - [Immutable variables](#immutable-variables) - - [Spacers](#spacers) - - [Proxy by Default](#proxy-by-default) - - [Versioning](#versioning) - - [Exceptions](#exceptions) - - [Dependencies](#dependencies) - - [Source Code](#source-code) - - [Tests](#tests) - - [Expect Revert with Low Level Calls](#expect-revert-with-low-level-calls) - - [Organizing Principles](#organizing-principles) - - [Test function naming convention](#test-function-naming-convention) - - [Detailed Naming Rules](#detailed-naming-rules) - - [Contract Naming Conventions](#contract-naming-conventions) - - [Test File Organization](#test-file-organization) - - [Test Naming Exceptions](#test-naming-exceptions) -- [Withdrawing From Fee Vaults](#withdrawing-from-fee-vaults) +- [Smart Contract Style Guide](#smart-contract-style-guide) + - [Standards and Conventions](#standards-and-conventions) + - [Style](#style) + - [Comments](#comments) + - [Errors](#errors) + - [Function Parameters](#function-parameters) + - [Function Return Arguments](#function-return-arguments) + - [Event Parameters](#event-parameters) + - [Immutable variables](#immutable-variables) + - [Struct typed storage variables](#struct-typed-storage-variables) + - [Spacers](#spacers) + - [Proxy by Default](#proxy-by-default) + - [Versioning](#versioning) + - [Exceptions](#exceptions) + - [Dependencies](#dependencies) + - [Interface Inheritance](#interface-inheritance) + - [Source Code](#source-code) + - [Tests](#tests) + - [Expect Revert with Low Level Calls](#expect-revert-with-low-level-calls) + - [Organizing Principles](#organizing-principles) + - [Test function naming convention](#test-function-naming-convention) + - [Detailed Naming Rules](#detailed-naming-rules) + - [Contract Naming Conventions](#contract-naming-conventions) + - [Test File Organization](#test-file-organization) + - [Test Naming Exceptions](#test-naming-exceptions) @@ -181,6 +183,48 @@ contract ExampleWithImmutable { } ``` +#### Struct typed storage variables + +Struct typed storage variables: + +- should be `internal` with a `_` prefix on the variable name +- should have a hand written getter function that returns the struct type +- the getter should be named after the variable, minus the `_` prefix. +- if necessary to avoid a naming collision, the getter function can be prefixed with `get`. + +When a struct typed storage variable is declared as `public`, Solidity's auto-generated getter returns +the struct fields as a tuple rather than as the struct type itself. This makes the contract +interface less ergonomic for external consumers. By using `internal` visibility and writing a +manual getter, we can return the proper struct type. + +Example: + +```solidity +contract ExampleWithStruct { + struct Config { + address owner; + uint256 timeout; + bool enabled; + } + + // ❌ Incorrect - public struct variable returns tuple + Config public config; + // The auto-generated getter: function config() returns (address, uint256, bool) + + // ✅ Correct - internal variable with handwritten getter returns struct + Config internal _config; + + function config() public view returns (Config memory) { + return _config; + } + + // Also acceptable if necessary to prevent naming collisions + function getConfig() public view returns (Config memory) { + return _config; + } +} +``` + #### Spacers We use spacer variables to account for old storage slots that are no longer being used. @@ -389,4 +433,4 @@ Certain types of tests are excluded from standard naming conventions: - **Script tests** (`test/scripts/`): Test deployment and utility scripts - **Library tests** (`test/libraries/`): May have different artifact structures - **Formal verification** (`test/kontrol/`): Use specialized tooling conventions -- **Vendor tests** (`test/vendor/`): Test external code with different patterns \ No newline at end of file +- **Vendor tests** (`test/vendor/`): Test external code with different patterns diff --git a/packages/contracts-bedrock/checks.yaml b/packages/contracts-bedrock/checks.yaml new file mode 100644 index 00000000000..15284eb9562 --- /dev/null +++ b/packages/contracts-bedrock/checks.yaml @@ -0,0 +1,92 @@ +# Check Runner Configuration +# +# Phases run sequentially, top to bottom. +# Within each phase, checks run in parallel (unless parallel: false). +# Dependencies within a phase are respected. +# +# Phase fields: +# - name: Phase identifier (shown in output) +# - build: (optional) Build command to run before checks +# - parallel: (optional) Whether checks run in parallel (default: true) +# - checks: List of checks to run +# +# Check fields: +# - name: Unique identifier +# - description: Human-readable description +# - command: Shell command to run +# - depends: (optional) Checks that must complete first (within same phase) +# - retry-clean: (optional) If true, retry with clean build on failure + +phases: + # Phase 1: Setup - runs sequentially + - name: setup + parallel: false + checks: + - name: lint-fix + description: Fix code formatting + command: forge fmt || true + + # Phase 2: Pre-build checks - no compilation needed + - name: pre-build + checks: + - name: lint + description: Check code formatting + command: forge fmt --check + - name: semgrep + description: Run semgrep security linter + command: cd ../../ && semgrep scan --config .semgrep/rules/ ./packages/contracts-bedrock + - name: semgrep-test-validity + description: Check semgrep tests are valid + command: forge fmt ../../.semgrep/tests/sol-rules.t.sol --check + - name: deploy-configs + description: Validate deploy configurations + command: ./scripts/checks/check-deploy-configs.sh + - name: kontrol-summaries + description: Check kontrol summaries unchanged + command: ./scripts/checks/check-kontrol-summaries-unchanged.sh + + # Phase 3: Source build checks - production contracts only + - name: source + build: just build-source + checks: + - name: snapshots + description: Check snapshots are up to date + command: go run ./scripts/autogen/generate-snapshots . && go run scripts/autogen/generate-semver-lock/main.go + - name: semver-diff + description: Check semver changes match lock changes + command: ./scripts/checks/check-semver-diff.sh + depends: + - snapshots + - name: unused-imports + description: Check for unused imports + command: go run ./scripts/checks/unused-imports + - name: strict-pragma + description: Check strict pragma versions + command: go run ./scripts/checks/strict-pragma + - name: valid-semver + description: Check valid semver versions + command: go run ./scripts/checks/valid-semver-check/main.go + - name: spacers + description: Validate storage spacers + command: go run ./scripts/checks/spacers + - name: reinitializers + description: Check reinitializer modifiers + command: go run ./scripts/checks/reinitializer + - name: interfaces + description: Check interface correctness + command: go run ./scripts/checks/interfaces + retry-clean: true + - name: size-check + description: Check contract sizes + command: forge build --sizes --skip "/**/test/**" --skip "/**/scripts/**" + - name: opcm-upgrade-checks + description: Check OPCM upgrade methods + command: go run ./scripts/checks/opcm-upgrade-checks/ + + # Phase 4: Dev build checks - needs test artifacts + - name: dev + build: just forge-build-dev + checks: + - name: forge-test-names + description: Validate forge test conventions + command: go run ./scripts/checks/test-validation diff --git a/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol b/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol index f042cffa579..48c7ab9c6b5 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOPContractsManager.sol @@ -192,10 +192,6 @@ interface IOPContractsManager { address proxyAdmin; address l1ChugSplashProxy; address resolvedDelegateProxy; - address permissionedDisputeGame1; - address permissionedDisputeGame2; - address permissionlessDisputeGame1; - address permissionlessDisputeGame2; } /// @notice The latest implementation contracts for the OP Stack. diff --git a/packages/contracts-bedrock/interfaces/L1/IOPContractsManagerStandardValidator.sol b/packages/contracts-bedrock/interfaces/L1/IOPContractsManagerStandardValidator.sol index 71f54bc5213..0606ea93ef2 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOPContractsManagerStandardValidator.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOPContractsManagerStandardValidator.sol @@ -19,6 +19,8 @@ interface IOPContractsManagerStandardValidator { address anchorStateRegistryImpl; address delayedWETHImpl; address mipsImpl; + address faultDisputeGameImpl; + address permissionedDisputeGameImpl; } struct ValidationInput { @@ -54,11 +56,12 @@ interface IOPContractsManagerStandardValidator { function l1PAOMultisig() external view returns (address); function l1StandardBridgeImpl() external view returns (address); function mipsImpl() external view returns (address); + function faultDisputeGameImpl() external view returns (address); + function permissionedDisputeGameImpl() external view returns (address); function optimismMintableERC20FactoryImpl() external view returns (address); function optimismPortalImpl() external view returns (address); function optimismPortalInteropImpl() external view returns (address); function ethLockboxImpl() external view returns (address); - function permissionedDisputeGameVersion() external pure returns (string memory); function preimageOracleVersion() external pure returns (string memory); function superchainConfig() external view returns (ISuperchainConfig); function systemConfigImpl() external view returns (address); diff --git a/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerContainer.sol b/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerContainer.sol index fb1351e1ff1..e75f3ba8c2b 100644 --- a/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerContainer.sol +++ b/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerContainer.sol @@ -8,10 +8,6 @@ interface IOPContractsManagerContainer { address proxyAdmin; address l1ChugSplashProxy; address resolvedDelegateProxy; - address permissionedDisputeGame1; - address permissionedDisputeGame2; - address permissionlessDisputeGame1; - address permissionlessDisputeGame2; } struct Implementations { diff --git a/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerUtils.sol b/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerUtils.sol index 02f1b506d61..3c11d04240f 100644 --- a/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerUtils.sol +++ b/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerUtils.sol @@ -46,6 +46,14 @@ interface IOPContractsManagerUtils { pure returns (bytes32); + function isMatchingInstructionByKey( + ExtraInstruction memory _instruction, + string memory _key + ) + external + pure + returns (bool); + function isMatchingInstruction( ExtraInstruction memory _instruction, string memory _key, diff --git a/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerV2.sol b/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerV2.sol index 9d35fe23920..b156c84b6bf 100644 --- a/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerV2.sol +++ b/packages/contracts-bedrock/interfaces/L1/opcm/IOPContractsManagerV2.sol @@ -81,6 +81,12 @@ interface IOPContractsManagerV2 { uint256 l2ChainId; IResourceMetering.ResourceConfig resourceConfig; DisputeGameConfig[] disputeGameConfigs; + bool useCustomGasToken; + } + + struct ExtraInstruction { + string key; + bytes data; } struct UpgradeInput { @@ -99,6 +105,7 @@ interface IOPContractsManagerV2 { error OPContractsManagerV2_SuperchainConfigNeedsUpgrade(); error OPContractsManagerV2_UnsupportedGameType(); error OPContractsManagerV2_InvalidUpgradeInstruction(string _key); + error OPContractsManagerV2_CannotUpgradeToCustomGasToken(); error IdentityPrecompileCallFailed(); error ReservedBitsSet(); error BytesArrayTooLong(); @@ -131,9 +138,7 @@ interface IOPContractsManagerV2 { function version() external view returns (string memory); /// @notice Upgrades Superchain-wide contracts. - function upgradeSuperchain(SuperchainUpgradeInput memory _inp) - external - returns (SuperchainContracts memory); + function upgradeSuperchain(SuperchainUpgradeInput memory _inp) external returns (SuperchainContracts memory); /// @notice Deploys and wires a complete OP Chain per the provided configuration. function deploy(FullConfig memory _cfg) external returns (ChainContracts memory); diff --git a/packages/contracts-bedrock/justfile b/packages/contracts-bedrock/justfile index d91ba5a502b..cff45827452 100644 --- a/packages/contracts-bedrock/justfile +++ b/packages/contracts-bedrock/justfile @@ -433,3 +433,27 @@ lint: lint-fix lint-check # Generates a table of contents for the POLICY.md file. toc: md_toc -p github meta/POLICY.md + + +######################################################## +# PARALLEL CHECK RUNNER # +######################################################## + +# Runs checks in parallel with smart build caching. +# Handles builds automatically and caches results for faster subsequent runs. +# Use -run to run specific checks, -list to see available checks. +# Examples: +# just check-fast # Run all checks with caching +# just check-fast -run lint # Run only lint check +# just check-fast -no-build # Skip checks that require builds +# just check-fast -list # List available checks +# just check-fast -verbose # Show output for all checks +# just check-fast -no-cache # Disable build caching +# just check-fast -clean # Clean artifacts before each build +# just check-fast -config path/to/file # Use custom checks.yaml +check-fast *ARGS: + go run ./scripts/check-runner -config checks.yaml {{ARGS}} + +# Alias for check-fast. +pr *ARGS: + just check-fast {{ARGS}} diff --git a/packages/contracts-bedrock/scripts/check-runner/main.go b/packages/contracts-bedrock/scripts/check-runner/main.go new file mode 100644 index 00000000000..e662c500c9e --- /dev/null +++ b/packages/contracts-bedrock/scripts/check-runner/main.go @@ -0,0 +1,1422 @@ +// check-runner is a parallel check execution tool for contracts-bedrock. +// +// It reads a checks.yaml configuration file that defines phases of checks to run. +// Each phase can optionally require a build step, and checks within a phase run +// in parallel by default (with dependency support for ordering). +// +// Key features: +// - Parallel execution of independent checks within each phase +// - Build caching based on source file hashes (SHA256 of all .sol files) +// - Automatic artifact preservation and restoration +// - Graceful shutdown on Ctrl+C (restores artifacts before exit) +// - Dependency-based ordering within phases +// - Pretty terminal output with spinners and colors +// +// Usage: +// +// go run ./scripts/check-runner -config checks.yaml +// go run ./scripts/check-runner -run lint,snapshots +// go run ./scripts/check-runner -list +// go run ./scripts/check-runner -no-build +// go run ./scripts/check-runner -clean +package main + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "flag" + "fmt" + "io" + "os" + "os/exec" + "os/signal" + "path/filepath" + "sort" + "strings" + "sync" + "sync/atomic" + "syscall" + "time" + + "github.com/chelnak/ysmrr" + "github.com/chelnak/ysmrr/pkg/colors" + "gopkg.in/yaml.v3" +) + +// ============================================================================= +// ANSI Color Codes +// ============================================================================= + +// Terminal color codes for pretty output. +const ( + Reset = "\033[0m" + Bold = "\033[1m" + Dim = "\033[2m" + Red = "\033[31m" + Green = "\033[32m" + Yellow = "\033[33m" + Blue = "\033[34m" + Cyan = "\033[36m" + BoldGreen = "\033[1;32m" + BoldRed = "\033[1;31m" + BoldCyan = "\033[1;36m" +) + +// ============================================================================= +// Cache Configuration +// ============================================================================= + +const ( + // MaxCacheCount is the maximum number of cached builds to keep per phase. + // Older caches are evicted using LRU. + MaxCacheCount = 5 + + // Baseline times for the old check script (in seconds). + // Used to calculate time saved. + BaselineCacheHit = 90.0 // 1.5 minutes when cache hit + BaselineNoCacheHit = 180.0 // 3.0 minutes when no cache hit +) + +// Paths are computed at runtime to use user's home directory. +var ( + // CacheDir is where build artifacts are cached between runs. + // Keyed by phase name and source hash. + CacheDir string + + // StatsFile is where cumulative time savings are stored. + StatsFile string +) + +func init() { + home, err := os.UserHomeDir() + if err != nil { + home = "/tmp" + } + CacheDir = filepath.Join(home, ".cache", "check-runner", "builds") + StatsFile = filepath.Join(home, ".cache", "check-runner", "stats.json") +} + +// artifactDirs are the directories containing build artifacts. +// These are saved/restored for caching and preserved across check runs. +var artifactDirs = []string{"artifacts", "forge-artifacts", "cache"} + +// ============================================================================= +// Stats Tracking +// ============================================================================= + +// Stats tracks cumulative time savings across runs. +type Stats struct { + TotalRuns int `json:"total_runs"` + TotalTimeSaved float64 `json:"total_time_saved"` // in seconds +} + +// loadStats reads the stats file from disk. +func loadStats() Stats { + data, err := os.ReadFile(StatsFile) + if err != nil { + return Stats{} + } + var stats Stats + if err := json.Unmarshal(data, &stats); err != nil { + return Stats{} + } + return stats +} + +// saveStats writes the stats file to disk. +func saveStats(stats Stats) { + data, err := json.Marshal(stats) + if err != nil { + return + } + // Ensure directory exists + if err := os.MkdirAll(filepath.Dir(StatsFile), 0755); err != nil { + return + } + _ = os.WriteFile(StatsFile, data, 0644) +} + +// formatDuration formats seconds into a human-readable string. +func formatDuration(seconds float64) string { + secs := int(seconds + 0.5) // Round to nearest second + if secs < 60 { + return fmt.Sprintf("%d seconds", secs) + } + minutes := secs / 60 + secs = secs % 60 + if minutes < 60 { + if secs == 0 { + return fmt.Sprintf("%d minutes", minutes) + } + return fmt.Sprintf("%d minutes %d seconds", minutes, secs) + } + hours := minutes / 60 + mins := minutes % 60 + if mins == 0 { + return fmt.Sprintf("%d hours", hours) + } + return fmt.Sprintf("%d hours %d minutes", hours, mins) +} + +// formatDurationHoursMinutes formats seconds into compact hours and minutes (e.g., "2h15m"). +func formatDurationHoursMinutes(seconds float64) string { + minutes := int(seconds) / 60 + if minutes < 60 { + return fmt.Sprintf("%dm", minutes) + } + hours := minutes / 60 + mins := minutes % 60 + if mins == 0 { + return fmt.Sprintf("%dh", hours) + } + return fmt.Sprintf("%dh%dm", hours, mins) +} + +// cacheRestoreDirs are the directories restored from cache before builds. +// We restore all artifact directories for incremental builds, then clean stale artifacts. +var cacheRestoreDirs = []string{"artifacts", "forge-artifacts", "cache"} + +// ============================================================================= +// Configuration Types +// ============================================================================= + +// Check represents a single check to run. +type Check struct { + Name string `yaml:"name"` // Unique identifier for the check + Description string `yaml:"description"` // Human-readable description + Command string `yaml:"command"` // Shell command to execute + Depends []string `yaml:"depends"` // Names of checks that must pass first + RetryClean bool `yaml:"retry-clean"` // If true, retry with clean build on failure +} + +// Phase represents a group of related checks. +// Checks within a phase run in parallel by default. +type Phase struct { + Name string `yaml:"name"` // Phase identifier (e.g., "setup", "source", "dev") + Build string `yaml:"build"` // Optional build command to run before checks + Parallel *bool `yaml:"parallel"` // Whether to run checks in parallel (default: true) + Checks []Check `yaml:"checks"` // Checks to run in this phase +} + +// Config is the top-level configuration loaded from checks.yaml. +type Config struct { + Phases []Phase `yaml:"phases"` +} + +// ============================================================================= +// Execution State Types +// ============================================================================= + +// CheckResult holds the outcome of running a single check. +type CheckResult struct { + Name string + Success bool + Output string // Combined stdout/stderr + Duration time.Duration +} + +// checkState tracks the execution state of a check during parallel runs. +type checkState struct { + status string // "pending", "queued", "running", "pass", "fail", "skipped" + spinner *ysmrr.Spinner +} + +// Runner orchestrates the execution of all checks. +type Runner struct { + config *Config + results map[string]*CheckResult + states map[string]*checkState + mu sync.Mutex + + // Configuration flags + noBuild bool // Skip phases that require builds + verbose bool // Show output for all checks, not just failures + noCache bool // Disable build caching + clean bool // Clean artifacts before each build + + // Build state + tempDir string // Temp directory for preserving working artifacts + sourceHash string // SHA256 hash of source files for cache key + + // Results tracking + totalPassed int + totalFailed int + totalSkipped int + failedChecks []string + buildError string // Build failure output to display at end + + // Retry-clean tracking + retryCleanChecks []string // Checks that failed and have retry-clean enabled + + // Graceful shutdown support + interrupted atomic.Bool // Set to true on first Ctrl+C + sigCount atomic.Int32 // Number of times Ctrl+C pressed + cancelFunc context.CancelFunc + + // Timing and stats + startTime time.Time + hadCacheHit bool // Whether any phase had a cache hit + isFullRun bool // Whether this is a full run (no -run filter) +} + +// ============================================================================= +// Entry Point +// ============================================================================= + +func main() { + var ( + configPath string + listChecks bool + runChecks string + noBuild bool + verbose bool + noCache bool + clean bool + ) + + flag.StringVar(&configPath, "config", "", "Path to checks.yaml config file") + flag.BoolVar(&listChecks, "list", false, "List available checks") + flag.StringVar(&runChecks, "run", "", "Run specific check(s), comma-separated") + flag.BoolVar(&noBuild, "no-build", false, "Skip phases that have builds") + flag.BoolVar(&verbose, "verbose", false, "Show output for all checks, not just failures") + flag.BoolVar(&noCache, "no-cache", false, "Disable build caching") + flag.BoolVar(&clean, "clean", false, "Clean build artifacts before each build (forces fresh compilation)") + flag.Parse() + + // Find config file + if configPath == "" { + // Default to checks.yaml in current directory + if _, err := os.Stat("checks.yaml"); err == nil { + configPath = "checks.yaml" + } else { + fmt.Fprintf(os.Stderr, "Error: could not find checks.yaml (use -config to specify path)\n") + os.Exit(1) + } + } + + config, err := loadConfig(configPath) + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err) + os.Exit(1) + } + + if listChecks { + printCheckList(config) + return + } + + runner := &Runner{ + config: config, + results: make(map[string]*CheckResult), + states: make(map[string]*checkState), + noBuild: noBuild, + verbose: verbose, + noCache: noCache, + clean: clean, + failedChecks: []string{}, + } + + // Parse which checks to run (if specified) + var selectedChecks map[string]bool + if runChecks != "" { + selectedChecks = make(map[string]bool) + for _, name := range strings.Split(runChecks, ",") { + name = strings.TrimSpace(name) + if !runner.checkExists(name) { + fmt.Fprintf(os.Stderr, "Error: unknown check '%s'\n", name) + fmt.Fprintf(os.Stderr, "Run with -list to see available checks\n") + os.Exit(1) + } + selectedChecks[name] = true + } + } + + success := runner.Run(selectedChecks) + if !success { + os.Exit(1) + } +} + +// ============================================================================= +// Configuration Loading +// ============================================================================= + +// loadConfig reads and parses the checks.yaml configuration file. +func loadConfig(path string) (*Config, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read config: %w", err) + } + + var config Config + if err := yaml.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("failed to parse config: %w", err) + } + + return &config, nil +} + +// printCheckList displays all available checks grouped by phase. +func printCheckList(config *Config) { + fmt.Println("Available checks:") + fmt.Println() + + for _, phase := range config.Phases { + fmt.Printf("%s%s%s", BoldCyan, phase.Name, Reset) + if phase.Build != "" { + fmt.Printf(" %s(builds)%s", Dim, Reset) + } + if phase.Parallel != nil && !*phase.Parallel { + fmt.Printf(" %s(sequential)%s", Dim, Reset) + } + fmt.Println() + + maxNameLen := 0 + for _, c := range phase.Checks { + if len(c.Name) > maxNameLen { + maxNameLen = len(c.Name) + } + } + + for _, c := range phase.Checks { + info := "" + if len(c.Depends) > 0 { + info = fmt.Sprintf(" %s→ %s%s", Dim, strings.Join(c.Depends, ", "), Reset) + } + fmt.Printf(" %-*s %s%s%s%s\n", maxNameLen, c.Name, Dim, c.Description, Reset, info) + } + fmt.Println() + } +} + +// checkExists returns true if a check with the given name exists. +func (r *Runner) checkExists(name string) bool { + for _, phase := range r.config.Phases { + for _, c := range phase.Checks { + if c.Name == name { + return true + } + } + } + return false +} + +// getCheck returns the Check with the given name, or nil if not found. +func (r *Runner) getCheck(name string) *Check { + for _, phase := range r.config.Phases { + for i := range phase.Checks { + if phase.Checks[i].Name == name { + return &phase.Checks[i] + } + } + } + return nil +} + +// ============================================================================= +// Build Caching +// ============================================================================= + +// computeSourceHash calculates a SHA256 hash of all Solidity source files +// and foundry.toml. This hash is used as the cache key for build artifacts. +func computeSourceHash() (string, error) { + h := sha256.New() + + var files []string + + // Walk src/ and interfaces/ directories for .sol files + sourceDirs := []string{"src", "interfaces"} + for _, dir := range sourceDirs { + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + if os.IsNotExist(err) { + return nil // Directory doesn't exist, skip + } + return err + } + if info.IsDir() { + return nil + } + if strings.HasSuffix(path, ".sol") { + files = append(files, path) + } + return nil + }) + if err != nil && !os.IsNotExist(err) { + return "", err + } + } + + // Include foundry.toml as it affects compilation + if _, err := os.Stat("foundry.toml"); err == nil { + files = append(files, "foundry.toml") + } + + // Sort for deterministic hashing + sort.Strings(files) + + // Hash each file's path and contents + for _, path := range files { + h.Write([]byte(path)) + data, err := os.ReadFile(path) + if err != nil { + return "", err + } + h.Write(data) + } + + // Return first 16 hex chars (64 bits) - enough for cache uniqueness + return hex.EncodeToString(h.Sum(nil))[:16], nil +} + +// getCachePath returns the filesystem path for a cached build. +func getCachePath(phaseName, hash string) string { + return filepath.Join(CacheDir, phaseName, hash) +} + +// cacheExists checks if a cached build exists for the given phase and hash. +func cacheExists(phaseName, hash string) bool { + path := getCachePath(phaseName, hash) + _, err := os.Stat(path) + return err == nil +} + +// getLatestCachePath returns the path to the "latest" symlink for a phase. +func getLatestCachePath(phaseName string) string { + return filepath.Join(CacheDir, phaseName, "latest") +} + +// getLatestCache returns the hash of the most recent cached build for a phase. +func getLatestCache(phaseName string) string { + latestPath := getLatestCachePath(phaseName) + target, err := os.Readlink(latestPath) + if err != nil { + return "" + } + return filepath.Base(target) +} + +// restoreFromCache restores build artifacts from a cached build. +func restoreFromCache(phaseName, hash string) error { + cachePath := getCachePath(phaseName, hash) + if _, err := os.Stat(cachePath); err != nil { + return fmt.Errorf("cache not found: %s", cachePath) + } + + for _, dir := range cacheRestoreDirs { + src := filepath.Join(cachePath, dir) + if _, err := os.Stat(src); err == nil { + os.RemoveAll(dir) + if err := copyDir(src, dir); err != nil { + return fmt.Errorf("failed to restore %s: %w", dir, err) + } + } + } + + return nil +} + +// saveToCache saves current build artifacts to the cache. +func saveToCache(phaseName, hash string) error { + cachePath := getCachePath(phaseName, hash) + + if err := os.MkdirAll(cachePath, 0755); err != nil { + return err + } + + for _, dir := range artifactDirs { + if _, err := os.Stat(dir); err == nil { + dst := filepath.Join(cachePath, dir) + // Remove existing cache directory first to ensure clean overwrite + os.RemoveAll(dst) + if err := copyDir(dir, dst); err != nil { + return fmt.Errorf("failed to cache %s: %w", dir, err) + } + } + } + + // Update "latest" symlink to point to this cache + latestPath := getLatestCachePath(phaseName) + os.Remove(latestPath) + if err := os.Symlink(hash, latestPath); err != nil { + fmt.Fprintf(os.Stderr, "Warning: could not update latest symlink: %v\n", err) + } + + // Clean up old caches to stay under MaxCacheCount + evictOldCaches(phaseName) + + return nil +} + +// evictOldCaches removes old cached builds, keeping only the MaxCacheCount most recent. +func evictOldCaches(phaseName string) { + cacheTypeDir := filepath.Join(CacheDir, phaseName) + entries, err := os.ReadDir(cacheTypeDir) + if err != nil { + return + } + + type cacheEntry struct { + name string + modTime time.Time + } + var caches []cacheEntry + + for _, entry := range entries { + if entry.Name() == "latest" { + continue // Skip the symlink + } + info, err := entry.Info() + if err != nil { + continue + } + caches = append(caches, cacheEntry{ + name: entry.Name(), + modTime: info.ModTime(), + }) + } + + // Sort by modification time, newest first + sort.Slice(caches, func(i, j int) bool { + return caches[i].modTime.After(caches[j].modTime) + }) + + // Remove caches beyond the limit + for i := MaxCacheCount; i < len(caches); i++ { + path := filepath.Join(cacheTypeDir, caches[i].name) + os.RemoveAll(path) + } +} + +// ============================================================================= +// Main Execution +// ============================================================================= + +// Run executes all configured checks, optionally filtering to selectedChecks. +// Returns true if all checks passed, false otherwise. +func (r *Runner) Run(selectedChecks map[string]bool) bool { + r.startTime = time.Now() + r.isFullRun = selectedChecks == nil // Full run if no filter specified + + // Check if any phase has a build step + hasBuilds := false + for _, phase := range r.config.Phases { + if phase.Build != "" { + hasBuilds = true + break + } + } + + // Save current working artifacts so we can restore them after checks complete. + // This ensures the user's build state is preserved even if checks modify artifacts. + if hasBuilds && !r.noBuild { + if err := r.saveWorkingArtifacts(); err != nil { + fmt.Fprintf(os.Stderr, "Warning: could not save artifacts: %v\n", err) + } + defer r.restoreWorkingArtifacts() + } + + // Set up signal handling for graceful shutdown. + // First Ctrl+C sets interrupted flag and restores artifacts. + // Third Ctrl+C force exits immediately. + ctx, cancel := context.WithCancel(context.Background()) + r.cancelFunc = cancel + defer cancel() + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + + go func() { + for sig := range sigChan { + count := r.sigCount.Add(1) + if count == 1 { + fmt.Printf("\n%s⚠ Interrupted%s - restoring artifacts...%s\n", Yellow, Dim, Reset) + r.interrupted.Store(true) + cancel() + } else if count >= 3 { + fmt.Printf("\n%s✗ Force exit%s\n", BoldRed, Reset) + os.Exit(130) // Standard exit code for SIGINT + } else { + fmt.Printf("\n%sPress Ctrl+C %d more time(s) to force exit%s\n", Dim, 3-count, Reset) + } + _ = sig // acknowledge + } + }() + defer signal.Stop(sigChan) + + hashComputed := false + _ = ctx // Used by signal handler via cancel() + + // Execute each phase in order + for _, phase := range r.config.Phases { + // Check for interruption at start of each phase + if r.interrupted.Load() { + break + } + + // Filter checks for this phase if specific checks were requested + var checksToRun []Check + for _, c := range phase.Checks { + if selectedChecks == nil || selectedChecks[c.Name] { + checksToRun = append(checksToRun, c) + } + } + + if len(checksToRun) == 0 { + continue + } + + // Skip phases with builds if -no-build flag was passed + if phase.Build != "" && r.noBuild { + fmt.Printf("%s⊘ %s%s %s(skipped - no build)%s\n", Yellow, phase.Name, Reset, Dim, Reset) + continue + } + + // Print phase header + fmt.Printf("\n%s→ %s%s\n", BoldCyan, phase.Name, Reset) + + // Compute source hash before first build phase (for cache key) + if phase.Build != "" && !hashComputed && !r.noCache { + hash, err := computeSourceHash() + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: could not compute source hash: %v\n", err) + } else { + r.sourceHash = hash + } + hashComputed = true + } + + // Run build step if this phase requires it + if phase.Build != "" { + if err := r.doBuildWithCache(phase.Name, phase.Build); err != nil { + if r.interrupted.Load() { + break // Don't print build failed if interrupted + } + fmt.Fprintf(os.Stderr, "%s✗ Build failed%s\n", BoldRed, Reset) + r.printFinalSummary() + return false + } + } + + // Check for interruption after build + if r.interrupted.Load() { + break + } + + // Run the checks (parallel by default) + parallel := phase.Parallel == nil || *phase.Parallel + r.runPhaseChecks(checksToRun, parallel) + } + + // Check if any failed checks have retry-clean enabled + if !r.interrupted.Load() && len(r.retryCleanChecks) > 0 { + r.runRetryClean() + } + + r.printFinalSummary() + return r.totalFailed == 0 && !r.interrupted.Load() +} + +// runRetryClean re-runs failed checks that have retry-clean enabled after a clean build. +func (r *Runner) runRetryClean() { + // Group retry checks by their phase's build command + checksByBuild := make(map[string][]string) + for _, checkName := range r.retryCleanChecks { + check := r.getCheck(checkName) + if check == nil { + continue + } + // Find the phase this check belongs to + for _, phase := range r.config.Phases { + for _, c := range phase.Checks { + if c.Name == checkName { + checksByBuild[phase.Build] = append(checksByBuild[phase.Build], checkName) + break + } + } + } + } + + fmt.Printf("\n%s→ retry-clean%s\n", BoldCyan, Reset) + fmt.Printf("%s⟳ Retrying %d check(s) with clean build...%s\n", Dim, len(r.retryCleanChecks), Reset) + + // Clean all artifacts + for _, dir := range artifactDirs { + os.RemoveAll(dir) + } + + // Re-run builds and checks for each build type that has retry checks + for buildCmd, checkNames := range checksByBuild { + if r.interrupted.Load() { + return + } + + // Find phase name for this build + phaseName := "" + for _, phase := range r.config.Phases { + if phase.Build == buildCmd { + phaseName = phase.Name + break + } + } + + // Run the build (clean, no cache) + if buildCmd != "" { + if err := r.doBuildClean(phaseName, buildCmd); err != nil { + fmt.Fprintf(os.Stderr, "%s✗ Clean build failed%s\n", BoldRed, Reset) + return + } + } + + // Re-run the failed checks + for _, checkName := range checkNames { + if r.interrupted.Load() { + return + } + r.runRetryCheck(checkName) + } + } +} + +// doBuildClean runs a build without using cache (for retry-clean). +func (r *Runner) doBuildClean(phaseName, buildCmd string) error { + sm := ysmrr.NewSpinnerManager( + ysmrr.WithSpinnerColor(colors.FgHiBlue), + ) + spinner := sm.AddSpinner(fmt.Sprintf("Clean building (%s)", buildCmd)) + sm.Start() + + startTime := time.Now() + cmd := exec.Command("sh", "-c", buildCmd) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + duration := time.Since(startTime) + + if err != nil { + spinner.UpdateMessage(fmt.Sprintf("%sClean build failed%s (%s) %s%.1fs%s", Red, Reset, buildCmd, Dim, duration.Seconds(), Reset)) + spinner.Error() + sm.Stop() + output := stdout.String() + stderr.String() + if output != "" { + r.buildError = output + } + return err + } + + spinner.UpdateMessage(fmt.Sprintf("%sClean built%s (%s) %s%.1fs%s", Green, Reset, buildCmd, Dim, duration.Seconds(), Reset)) + spinner.Complete() + sm.Stop() + + // Save clean build to cache + if r.sourceHash != "" && !r.noCache { + fmt.Printf("%s⟳ Saving clean build to cache %s...%s\n", Dim, r.sourceHash[:8], Reset) + if err := saveToCache(phaseName, r.sourceHash); err != nil { + fmt.Fprintf(os.Stderr, "Warning: could not save to cache: %v\n", err) + } + } + + return nil +} + +// runRetryCheck re-runs a single check and updates the results. +func (r *Runner) runRetryCheck(name string) { + check := r.getCheck(name) + if check == nil { + return + } + + sm := ysmrr.NewSpinnerManager( + ysmrr.WithSpinnerColor(colors.FgHiBlue), + ) + spinner := sm.AddSpinner(fmt.Sprintf("%s (retry)", name)) + sm.Start() + + startTime := time.Now() + cmd := exec.Command("sh", "-c", check.Command) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + duration := time.Since(startTime) + + output := stdout.String() + if stderr.Len() > 0 { + if output != "" { + output += "\n" + } + output += stderr.String() + } + + timeStr := fmt.Sprintf("%s%.1fs%s", Dim, duration.Seconds(), Reset) + + if err == nil { + // Retry succeeded - update results + spinner.UpdateMessage(fmt.Sprintf("%s (retry) %s", name, timeStr)) + spinner.Complete() + sm.Stop() + + // Update totals: was failed, now passed + r.totalFailed-- + r.totalPassed++ + + // Remove from failed checks list + newFailed := []string{} + for _, n := range r.failedChecks { + if n != name { + newFailed = append(newFailed, n) + } + } + r.failedChecks = newFailed + + // Update result + r.results[name] = &CheckResult{ + Name: name, + Success: true, + Output: output, + Duration: duration, + } + } else { + // Retry also failed + spinner.UpdateMessage(fmt.Sprintf("%s (retry) %s", name, timeStr)) + spinner.Error() + sm.Stop() + + // Update result with new output + r.results[name] = &CheckResult{ + Name: name, + Success: false, + Output: output, + Duration: duration, + } + } +} + +// printFinalSummary displays the final results including any failures. +func (r *Runner) printFinalSummary() { + fmt.Println() + + // If interrupted, just confirm artifacts were restored + if r.interrupted.Load() { + fmt.Printf("%s✓ Artifacts restored%s\n", Green, Reset) + return + } + + // Print build error with box drawing characters + if r.buildError != "" { + fmt.Printf("%s┌─ build%s\n", Red, Reset) + lines := strings.Split(strings.TrimSpace(r.buildError), "\n") + for _, line := range lines { + fmt.Printf("%s│%s %s\n", Red, Reset, line) + } + fmt.Printf("%s└%s\n", Red, Reset) + fmt.Println() + } + + // Print failed check details with box drawing characters + if len(r.failedChecks) > 0 { + for _, name := range r.failedChecks { + result := r.results[name] + if result != nil && result.Output != "" { + fmt.Printf("%s┌─ %s%s\n", Red, name, Reset) + lines := strings.Split(strings.TrimSpace(result.Output), "\n") + for _, line := range lines { + fmt.Printf("%s│%s %s\n", Red, Reset, line) + } + fmt.Printf("%s└%s\n", Red, Reset) + fmt.Println() + } + } + } + + // Print final status line + total := r.totalPassed + r.totalFailed + r.totalSkipped + if r.buildError != "" { + fmt.Printf("%s✗ Build failed%s\n", BoldRed, Reset) + } else if r.totalFailed == 0 { + fmt.Printf("%s✓ All checks passed%s", BoldGreen, Reset) + fmt.Printf(" %s(%d/%d)%s\n", Dim, r.totalPassed, total, Reset) + } else { + fmt.Printf("%s✗ %d check(s) failed%s", BoldRed, r.totalFailed, Reset) + fmt.Printf(" %s(%d passed, %d failed)%s\n", Dim, r.totalPassed, r.totalFailed, Reset) + } + + // Print timing stats + r.printTimingStats() +} + +// printTimingStats displays execution time and cumulative time saved. +func (r *Runner) printTimingStats() { + duration := time.Since(r.startTime).Seconds() + + // Print this run's time + fmt.Printf("\n%sThis run took %s%s\n", Dim, formatDuration(duration), Reset) + + // Only track stats for full runs + if !r.isFullRun { + return + } + + // Select baseline based on cache hit + baseline := BaselineNoCacheHit + if r.hadCacheHit { + baseline = BaselineCacheHit + } + + // Calculate time saved (baseline - actual) + timeSaved := baseline - duration + if timeSaved < 0 { + timeSaved = 0 + } + + // Load and update stats + stats := loadStats() + stats.TotalRuns++ + stats.TotalTimeSaved += timeSaved + saveStats(stats) + + // Print cumulative time saved (only if at least 1 minute saved) + if stats.TotalTimeSaved >= 60 { + fmt.Printf("%sYou've saved ~%s using check-fast%s\n", Green, formatDurationHoursMinutes(stats.TotalTimeSaved), Reset) + } +} + +// ============================================================================= +// Build Execution +// ============================================================================= + +// doBuildWithCache runs a build command, using caching when possible. +// It handles cache restoration, incremental builds, and cache saving. +func (r *Runner) doBuildWithCache(phaseName, buildCmd string) error { + hash := r.sourceHash + cacheHit := false + + if r.clean { + // Clean build: remove all artifacts first + fmt.Printf("%s⟳ Cleaning artifacts...%s\n", Dim, Reset) + for _, dir := range artifactDirs { + os.RemoveAll(dir) + } + } else { + // Try to restore from exact cache match + if hash != "" && !r.noCache && cacheExists(phaseName, hash) { + fmt.Printf("%s⟳ Restoring from cache %s...%s\n", Dim, hash[:8], Reset) + if err := restoreFromCache(phaseName, hash); err != nil { + fmt.Fprintf(os.Stderr, "Warning: cache restore failed: %v\n", err) + } else { + cacheHit = true + r.hadCacheHit = true + } + } + + // If no exact match, try to restore latest cache for incremental build + if !cacheHit && hash != "" && !r.noCache { + latest := getLatestCache(phaseName) + if latest != "" && latest != hash { + fmt.Printf("%s⟳ Restoring latest cache for incremental build...%s\n", Dim, Reset) + if err := restoreFromCache(phaseName, latest); err != nil { + fmt.Fprintf(os.Stderr, "Warning: could not restore latest cache: %v\n", err) + } + } + } + } + + // Run the build with a spinner + sm := ysmrr.NewSpinnerManager( + ysmrr.WithSpinnerColor(colors.FgHiBlue), + ) + spinner := sm.AddSpinner(fmt.Sprintf("Building (%s)", buildCmd)) + sm.Start() + + startTime := time.Now() + cmd := exec.Command("sh", "-c", buildCmd) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + duration := time.Since(startTime) + + if err != nil { + spinner.UpdateMessage(fmt.Sprintf("%sBuild failed%s (%s) %s%.1fs%s", Red, Reset, buildCmd, Dim, duration.Seconds(), Reset)) + spinner.Error() + sm.Stop() + // Store build output for display in final summary + output := stdout.String() + stderr.String() + if output != "" { + r.buildError = output + } + return err + } + + spinner.UpdateMessage(fmt.Sprintf("%sBuilt%s (%s) %s%.1fs%s", Green, Reset, buildCmd, Dim, duration.Seconds(), Reset)) + spinner.Complete() + sm.Stop() + + // Always save to cache after successful build. + // Even on cache hit, tests/scripts may have changed and we want the latest artifacts cached. + if hash != "" && !r.noCache { + fmt.Printf("%s⟳ Saving to cache %s...%s\n", Dim, hash[:8], Reset) + if err := saveToCache(phaseName, hash); err != nil { + fmt.Fprintf(os.Stderr, "Warning: could not save to cache: %v\n", err) + } + } + + return nil +} + +// ============================================================================= +// Artifact Preservation +// ============================================================================= + +// saveWorkingArtifacts copies current build artifacts to a temp directory. +// This preserves the user's working state before checks potentially modify artifacts. +func (r *Runner) saveWorkingArtifacts() error { + tempDir, err := os.MkdirTemp("", "check-runner-working-") + if err != nil { + return err + } + r.tempDir = tempDir + + for _, dir := range artifactDirs { + if _, err := os.Stat(dir); err == nil { + dst := filepath.Join(tempDir, dir) + if err := copyDir(dir, dst); err != nil { + fmt.Fprintf(os.Stderr, "Warning: could not save %s: %v\n", dir, err) + } + } + } + return nil +} + +// restoreWorkingArtifacts restores the user's original build artifacts. +// Called after checks complete or on interrupt. +func (r *Runner) restoreWorkingArtifacts() { + if r.tempDir == "" { + return + } + defer os.RemoveAll(r.tempDir) + + for _, dir := range artifactDirs { + src := filepath.Join(r.tempDir, dir) + if _, err := os.Stat(src); err == nil { + os.RemoveAll(dir) + if err := copyDir(src, dir); err != nil { + fmt.Fprintf(os.Stderr, "Warning: could not restore %s: %v\n", dir, err) + } + } + } +} + +// copyDir copies a directory recursively using cp -r. +func copyDir(src, dst string) error { + cmd := exec.Command("cp", "-r", src, dst) + return cmd.Run() +} + +// ============================================================================= +// Check Execution +// ============================================================================= + +// runPhaseChecks runs all checks for a phase, either in parallel or sequentially. +func (r *Runner) runPhaseChecks(checks []Check, parallel bool) { + if len(checks) == 0 { + return + } + + if parallel { + r.runChecksParallel(checks) + } else { + r.runChecksSequential(checks) + } +} + +// runChecksSequential runs checks one at a time in order. +func (r *Runner) runChecksSequential(checks []Check) { + for _, check := range checks { + // Check for interruption before each check + if r.interrupted.Load() { + return + } + + startTime := time.Now() + cmd := exec.Command("sh", "-c", check.Command) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + duration := time.Since(startTime) + + // Check for interruption after command completes + if r.interrupted.Load() { + return + } + + output := stdout.String() + if stderr.Len() > 0 { + if output != "" { + output += "\n" + } + output += stderr.String() + } + + result := &CheckResult{ + Name: check.Name, + Success: err == nil, + Output: output, + Duration: duration, + } + r.results[check.Name] = result + + if result.Success { + fmt.Printf("%s✓%s %s %s%.1fs%s\n", Green, Reset, check.Name, Dim, duration.Seconds(), Reset) + r.totalPassed++ + } else if check.RetryClean { + // Show retry indicator for checks that will retry with clean build + fmt.Printf("%s↻%s %s %s%.1fs%s %s(will retry with clean build)%s\n", Yellow, Reset, check.Name, Dim, duration.Seconds(), Reset, Yellow, Reset) + r.totalFailed++ + r.failedChecks = append(r.failedChecks, check.Name) + r.retryCleanChecks = append(r.retryCleanChecks, check.Name) + } else { + fmt.Printf("%s✗%s %s %s%.1fs%s\n", Red, Reset, check.Name, Dim, duration.Seconds(), Reset) + r.totalFailed++ + r.failedChecks = append(r.failedChecks, check.Name) + } + } +} + +// runChecksParallel runs checks concurrently with dependency ordering. +// Uses a worker pool and respects check dependencies. +func (r *Runner) runChecksParallel(checks []Check) { + sm := ysmrr.NewSpinnerManager( + ysmrr.WithSpinnerColor(colors.FgHiBlue), + ) + + // Initialize state for each check with a spinner + checkNames := make([]string, len(checks)) + for i, c := range checks { + checkNames[i] = c.Name + spinner := sm.AddSpinner(c.Name) + r.states[c.Name] = &checkState{status: "pending", spinner: spinner} + } + + sm.Start() + + // Channel for checks ready to run (dependencies satisfied) + ready := make(chan string, len(checks)) + var wg sync.WaitGroup + + // Queue checks with no dependencies + r.mu.Lock() + for _, name := range checkNames { + if r.depsReady(name, checkNames) { + r.states[name].status = "queued" + ready <- name + } + } + r.mu.Unlock() + + // Start worker pool + numWorkers := 8 + for i := 0; i < numWorkers; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for name := range ready { + // Check for interruption before starting each check + if r.interrupted.Load() { + return + } + r.runCheckParallel(name) + } + }() + } + + // Monitor loop: queue checks as their dependencies complete + go func() { + for { + // Check for interruption + if r.interrupted.Load() { + close(ready) + return + } + + r.mu.Lock() + allDone := true + for _, name := range checkNames { + state := r.states[name] + if state.status == "pending" { + // Skip checks whose dependencies failed + if r.depsFailed(name, checkNames) { + state.status = "skipped" + state.spinner.UpdateMessage(fmt.Sprintf(" %s %s(skipped)%s", name, Dim, Reset)) + state.spinner.Error() + r.totalSkipped++ + continue + } + // Queue checks whose dependencies passed + if r.depsReady(name, checkNames) { + state.status = "queued" + ready <- name + } else { + allDone = false + } + } else if state.status == "running" || state.status == "queued" { + allDone = false + } + } + r.mu.Unlock() + + if allDone { + close(ready) + return + } + time.Sleep(50 * time.Millisecond) + } + }() + + wg.Wait() + sm.Stop() + + // Tally results + r.mu.Lock() + for _, name := range checkNames { + state := r.states[name] + if state == nil { + continue + } + switch state.status { + case "pass": + r.totalPassed++ + case "fail": + r.totalFailed++ + r.failedChecks = append(r.failedChecks, name) + // Track checks with retry-clean enabled + check := r.getCheck(name) + if check != nil && check.RetryClean { + r.retryCleanChecks = append(r.retryCleanChecks, name) + } + } + } + r.mu.Unlock() +} + +// depsReady returns true if all dependencies of a check have passed. +func (r *Runner) depsReady(name string, checkNames []string) bool { + check := r.getCheck(name) + if check == nil || len(check.Depends) == 0 { + return true + } + + // Build set of checks in this phase + inPhase := make(map[string]bool) + for _, n := range checkNames { + inPhase[n] = true + } + + // Check each dependency + for _, dep := range check.Depends { + if !inPhase[dep] { + continue // Dependency not in this phase, ignore + } + state := r.states[dep] + if state == nil || state.status != "pass" { + return false + } + } + return true +} + +// depsFailed returns true if any dependency of a check has failed. +func (r *Runner) depsFailed(name string, checkNames []string) bool { + check := r.getCheck(name) + if check == nil || len(check.Depends) == 0 { + return false + } + + // Build set of checks in this phase + inPhase := make(map[string]bool) + for _, n := range checkNames { + inPhase[n] = true + } + + // Check each dependency + for _, dep := range check.Depends { + if !inPhase[dep] { + continue // Dependency not in this phase, ignore + } + state := r.states[dep] + if state != nil && state.status == "fail" { + return true + } + } + return false +} + +// runCheckParallel executes a single check and updates its spinner. +func (r *Runner) runCheckParallel(name string) { + check := r.getCheck(name) + if check == nil { + return + } + + r.mu.Lock() + state := r.states[name] + state.status = "running" + spinner := state.spinner + r.mu.Unlock() + + startTime := time.Now() + cmd := exec.Command("sh", "-c", check.Command) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + duration := time.Since(startTime) + + output := stdout.String() + if stderr.Len() > 0 { + if output != "" { + output += "\n" + } + output += stderr.String() + } + + result := &CheckResult{ + Name: name, + Success: err == nil, + Output: output, + Duration: duration, + } + + r.mu.Lock() + r.results[name] = result + if result.Success { + state.status = "pass" + } else { + state.status = "fail" + } + r.mu.Unlock() + + // Update spinner with result + // Note: spinner.Complete() and spinner.Error() add ✓/✗ prefix automatically + timeStr := fmt.Sprintf("%s%.1fs%s", Dim, duration.Seconds(), Reset) + if result.Success { + spinner.UpdateMessage(fmt.Sprintf("%s %s", name, timeStr)) + spinner.Complete() + } else if check.RetryClean { + // Show retry indicator for checks that will retry with clean build + spinner.CompleteCharacter(fmt.Sprintf("%s↻%s", Yellow, Reset)) + spinner.UpdateMessage(fmt.Sprintf("%s %s %s(will retry with clean build)%s", name, timeStr, Yellow, Reset)) + spinner.Complete() + } else { + spinner.UpdateMessage(fmt.Sprintf("%s %s", name, timeStr)) + spinner.Error() + } +} + +// Ensure io package is used (for potential future extensions) +var _ io.Reader diff --git a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol index 3661c7602ff..0ac35465ff8 100644 --- a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol +++ b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol @@ -16,7 +16,6 @@ import { Types } from "scripts/libraries/Types.sol"; import { Blueprint } from "src/libraries/Blueprint.sol"; import { GameTypes } from "src/dispute/lib/Types.sol"; import { Hash } from "src/dispute/lib/Types.sol"; -import { DevFeatures } from "src/libraries/DevFeatures.sol"; // Interfaces import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; @@ -426,26 +425,6 @@ library ChainAssertions { Blueprint.Preamble memory rdProxyPreamble = Blueprint.parseBlueprintPreamble(address(blueprints.resolvedDelegateProxy).code); require(keccak256(rdProxyPreamble.initcode) == keccak256(vm.getCode("ResolvedDelegateProxy")), "CHECK-OPCM-200"); - - if (!_opcm.isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - Blueprint.Preamble memory pdg1Preamble = - Blueprint.parseBlueprintPreamble(address(blueprints.permissionedDisputeGame1).code); - Blueprint.Preamble memory pdg2Preamble = - Blueprint.parseBlueprintPreamble(address(blueprints.permissionedDisputeGame2).code); - // combine pdg1 and pdg2 initcodes - bytes memory fullPermissionedDisputeGameInitcode = - abi.encodePacked(pdg1Preamble.initcode, pdg2Preamble.initcode); - require( - keccak256(fullPermissionedDisputeGameInitcode) == keccak256(vm.getCode("PermissionedDisputeGame")), - "CHECK-OPCM-210" - ); - } else { - // Should not deploy V1 blueprints when using V2 dispute games - require(address(blueprints.permissionedDisputeGame1).code.length == 0, "CHECK-OPCM-220"); - require(address(blueprints.permissionedDisputeGame2).code.length == 0, "CHECK-OPCM-230"); - require(address(blueprints.permissionlessDisputeGame1).code.length == 0, "CHECK-OPCM-240"); - require(address(blueprints.permissionlessDisputeGame2).code.length == 0, "CHECK-OPCM-250"); - } } function checkAnchorStateRegistryProxy(IAnchorStateRegistry _anchorStateRegistryProxy, bool _isProxy) internal { diff --git a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol index 87c76b7855d..3467c24dfff 100644 --- a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol @@ -22,9 +22,9 @@ import { StandardConstants } from "scripts/deploy/StandardConstants.sol"; // Libraries import { Types } from "scripts/libraries/Types.sol"; import { Duration } from "src/dispute/lib/LibUDT.sol"; -import { DevFeatures } from "src/libraries/DevFeatures.sol"; import { GameType, Claim, GameTypes, Proposal, Hash } from "src/dispute/lib/Types.sol"; import { Constants } from "src/libraries/Constants.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; // Interfaces import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; @@ -308,9 +308,7 @@ contract Deploy is Deployer { } artifacts.save("DelayedWETHImpl", address(dio.delayedWETHImpl)); artifacts.save("PreimageOracle", address(dio.preimageOracleSingleton)); - if (DevFeatures.isDevFeatureEnabled(dio.opcm.devFeatureBitmap(), DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - artifacts.save("PermissionedDisputeGame", address(dio.permissionedDisputeGameV2Impl)); - } + artifacts.save("PermissionedDisputeGame", address(dio.permissionedDisputeGameV2Impl)); // Get a contract set from the implementation addresses which were just deployed. Types.ContractSet memory impls = ChainAssertions.dioToContractSet(dio); @@ -378,9 +376,6 @@ contract Deploy is Deployer { artifacts.save("AnchorStateRegistryProxy", address(deployOutput.anchorStateRegistryProxy)); artifacts.save("OptimismPortalProxy", address(deployOutput.optimismPortalProxy)); artifacts.save("OptimismPortal2Proxy", address(deployOutput.optimismPortalProxy)); - if (!DevFeatures.isDevFeatureEnabled(opcm.devFeatureBitmap(), DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - artifacts.save("PermissionedDisputeGame", address(deployOutput.permissionedDisputeGame)); - } // Check if the permissionless game implementation is already set IDisputeGameFactory factory = IDisputeGameFactory(artifacts.mustGetAddress("DisputeGameFactoryProxy")); @@ -547,7 +542,8 @@ contract Deploy is Deployer { gasLimit: uint64(cfg.l2GenesisBlockGasLimit()), l2ChainId: cfg.l2ChainID(), resourceConfig: Constants.DEFAULT_RESOURCE_CONFIG(), - disputeGameConfigs: disputeGameConfigs + disputeGameConfigs: disputeGameConfigs, + useCustomGasToken: cfg.useCustomGasToken() }); } } diff --git a/packages/contracts-bedrock/scripts/deploy/DeployDisputeGame.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployDisputeGame.s.sol index 7ea2eb4cbce..5096d78f3a7 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployDisputeGame.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployDisputeGame.s.sol @@ -26,7 +26,6 @@ contract DeployDisputeGame is Script { struct Input { // Common inputs. string release; - bool useV2; // Specify which game kind is being deployed here. string gameKind; // All inputs required to deploy FaultDisputeGame. @@ -52,11 +51,7 @@ contract DeployDisputeGame is Script { function run(Input memory _input) public returns (Output memory output_) { assertValidInput(_input); - if (_input.useV2) { - deployDisputeGameImplV2(_input, output_); - } else { - deployDisputeGameImplV1(_input, output_); - } + deployDisputeGameImplV2(_input, output_); assertValidOutput(_input, output_); } @@ -171,24 +166,9 @@ contract DeployDisputeGame is Script { DeployUtils.assertValidContractAddress(address(game)); - if (!_input.useV2) { - require(GameType.unwrap(game.gameType()) == GameType.unwrap(_input.gameType), "DG-10"); - } require(game.maxGameDepth() == _input.maxGameDepth, "DG-20"); require(game.splitDepth() == _input.splitDepth, "DG-30"); require(game.clockExtension().raw() == uint64(_input.clockExtension), "DG-40"); require(game.maxClockDuration().raw() == uint64(_input.maxClockDuration), "DG-50"); - - if (!_input.useV2) { - require(game.vm() == _input.vmAddress, "DG-60"); - require(game.weth() == _input.delayedWethProxy, "DG-70"); - require(game.anchorStateRegistry() == _input.anchorStateRegistryProxy, "DG-80"); - require(game.l2ChainId() == _input.l2ChainId, "DG-90"); - - if (LibString.eq(_input.gameKind, "PermissionedDisputeGame")) { - require(game.proposer() == _input.proposer, "DG-100"); - require(game.challenger() == _input.challenger, "DG-110"); - } - } } } diff --git a/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol index 8634e721898..d6d660ff64b 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol @@ -132,10 +132,8 @@ contract DeployImplementations is Script { deployMipsSingleton(_input, output_); deployDisputeGameFactoryImpl(output_); deployAnchorStateRegistryImpl(_input, output_); - if (DevFeatures.isDevFeatureEnabled(_input.devFeatureBitmap, DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - deployFaultDisputeGameV2Impl(_input, output_); - deployPermissionedDisputeGameV2Impl(_input, output_); - } + deployFaultDisputeGameV2Impl(_input, output_); + deployPermissionedDisputeGameV2Impl(_input, output_); if (DevFeatures.isDevFeatureEnabled(_input.devFeatureBitmap, DevFeatures.OPTIMISM_PORTAL_INTEROP)) { deploySuperFaultDisputeGameImpl(_input, output_); deploySuperPermissionedDisputeGameImpl(_input, output_); @@ -210,11 +208,7 @@ contract DeployImplementations is Script { proxy: _blueprints.proxy, proxyAdmin: _blueprints.proxyAdmin, l1ChugSplashProxy: _blueprints.l1ChugSplashProxy, - resolvedDelegateProxy: _blueprints.resolvedDelegateProxy, - permissionedDisputeGame1: _blueprints.permissionedDisputeGame1, - permissionedDisputeGame2: _blueprints.permissionedDisputeGame2, - permissionlessDisputeGame1: _blueprints.permissionlessDisputeGame1, - permissionlessDisputeGame2: _blueprints.permissionlessDisputeGame2 + resolvedDelegateProxy: _blueprints.resolvedDelegateProxy }); deployOPCMBPImplsContainer(_input, _output, _blueprints, implementations); @@ -286,12 +280,6 @@ contract DeployImplementations is Script { require(checkAddress == address(0), "OPCM-40"); (blueprints.resolvedDelegateProxy, checkAddress) = DeployUtils.createDeterministicBlueprint(vm.getCode("ResolvedDelegateProxy"), _salt); require(checkAddress == address(0), "OPCM-50"); - // The max initcode/runtimecode size is 48KB/24KB. - // But for Blueprint, the initcode is stored as runtime code, that's why it's necessary to split into 2 parts. - if (!DevFeatures.isDevFeatureEnabled(_input.devFeatureBitmap, DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - (blueprints.permissionedDisputeGame1, blueprints.permissionedDisputeGame2) = DeployUtils.createDeterministicBlueprint(vm.getCode("PermissionedDisputeGame"), _salt); - (blueprints.permissionlessDisputeGame1, blueprints.permissionlessDisputeGame2) = DeployUtils.createDeterministicBlueprint(vm.getCode("FaultDisputeGame"), _salt); - } // forgefmt: disable-end vm.stopBroadcast(); @@ -741,6 +729,8 @@ contract DeployImplementations is Script { opcmImplementations.anchorStateRegistryImpl = _implementations.anchorStateRegistryImpl; opcmImplementations.delayedWETHImpl = _implementations.delayedWETHImpl; opcmImplementations.mipsImpl = _implementations.mipsImpl; + opcmImplementations.faultDisputeGameImpl = _implementations.faultDisputeGameV2Impl; + opcmImplementations.permissionedDisputeGameImpl = _implementations.permissionedDisputeGameV2Impl; IOPContractsManagerStandardValidator impl = IOPContractsManagerStandardValidator( DeployUtils.createDeterministic({ @@ -809,35 +799,31 @@ contract DeployImplementations is Script { } function assertValidInput(Input memory _input) private pure { - if (DevFeatures.isDevFeatureEnabled(_input.devFeatureBitmap, DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - // Validate V2 game depth parameters are sensible - require( - _input.faultGameV2MaxGameDepth > 0 && _input.faultGameV2MaxGameDepth <= 125, - "DeployImplementations: faultGameV2MaxGameDepth out of valid range (1-125)" - ); - // V2 contract requires splitDepth >= 2 and splitDepth + 1 < maxGameDepth - require( - _input.faultGameV2SplitDepth >= 2 && _input.faultGameV2SplitDepth + 1 < _input.faultGameV2MaxGameDepth, - "DeployImplementations: faultGameV2SplitDepth must be >= 2 and splitDepth + 1 < maxGameDepth" - ); + // Validate V2 game depth parameters are sensible + require( + _input.faultGameV2MaxGameDepth > 0 && _input.faultGameV2MaxGameDepth <= 125, + "DeployImplementations: faultGameV2MaxGameDepth out of valid range (1-125)" + ); + // V2 contract requires splitDepth >= 2 and splitDepth + 1 < maxGameDepth + require( + _input.faultGameV2SplitDepth >= 2 && _input.faultGameV2SplitDepth + 1 < _input.faultGameV2MaxGameDepth, + "DeployImplementations: faultGameV2SplitDepth must be >= 2 and splitDepth + 1 < maxGameDepth" + ); - // Validate V2 clock parameters fit in uint64 before deployment - require( - _input.faultGameV2ClockExtension <= type(uint64).max, - "DeployImplementations: faultGameV2ClockExtension too large for uint64" - ); - require( - _input.faultGameV2MaxClockDuration <= type(uint64).max, - "DeployImplementations: faultGameV2MaxClockDuration too large for uint64" - ); - require( - _input.faultGameV2MaxClockDuration >= _input.faultGameV2ClockExtension, - "DeployImplementations: maxClockDuration must be >= clockExtension" - ); - require( - _input.faultGameV2ClockExtension > 0, "DeployImplementations: faultGameV2ClockExtension must be > 0" - ); - } + // Validate V2 clock parameters fit in uint64 before deployment + require( + _input.faultGameV2ClockExtension <= type(uint64).max, + "DeployImplementations: faultGameV2ClockExtension too large for uint64" + ); + require( + _input.faultGameV2MaxClockDuration <= type(uint64).max, + "DeployImplementations: faultGameV2MaxClockDuration too large for uint64" + ); + require( + _input.faultGameV2MaxClockDuration >= _input.faultGameV2ClockExtension, + "DeployImplementations: maxClockDuration must be >= clockExtension" + ); + require(_input.faultGameV2ClockExtension > 0, "DeployImplementations: faultGameV2ClockExtension must be > 0"); require(_input.withdrawalDelaySeconds != 0, "DeployImplementations: withdrawalDelaySeconds not set"); require(_input.minProposalSizeBytes != 0, "DeployImplementations: minProposalSizeBytes not set"); require(_input.challengePeriodSeconds != 0, "DeployImplementations: challengePeriodSeconds not set"); @@ -883,16 +869,11 @@ contract DeployImplementations is Script { address(_output.optimismMintableERC20FactoryImpl), address(_output.disputeGameFactoryImpl), address(_output.anchorStateRegistryImpl), - address(_output.ethLockboxImpl) + address(_output.ethLockboxImpl), + address(_output.faultDisputeGameV2Impl), + address(_output.permissionedDisputeGameV2Impl) ); - // Only include V2 contracts in validation if they were deployed - if (DevFeatures.isDevFeatureEnabled(_input.devFeatureBitmap, DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - address[] memory v2Addrs = Solarray.addresses( - address(_output.faultDisputeGameV2Impl), address(_output.permissionedDisputeGameV2Impl) - ); - addrs2 = Solarray.extend(addrs2, v2Addrs); - } if (DevFeatures.isDevFeatureEnabled(_input.devFeatureBitmap, DevFeatures.OPTIMISM_PORTAL_INTEROP)) { address[] memory superGameAddrs = Solarray.addresses( address(_output.superFaultDisputeGameImpl), address(_output.superPermissionedDisputeGameImpl) @@ -902,17 +883,6 @@ contract DeployImplementations is Script { DeployUtils.assertValidContractAddresses(Solarray.extend(addrs1, addrs2)); - // Validate V2 contracts not deployed when flag is disabled - if (!DevFeatures.isDevFeatureEnabled(_input.devFeatureBitmap, DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - require( - address(_output.faultDisputeGameV2Impl) == address(0), - "DeployImplementations: V2 flag disabled but FaultDisputeGameV2 was deployed" - ); - require( - address(_output.permissionedDisputeGameV2Impl) == address(0), - "DeployImplementations: V2 flag disabled but PermissionedDisputeGameV2 was deployed" - ); - } if (!DevFeatures.isDevFeatureEnabled(_input.devFeatureBitmap, DevFeatures.OPTIMISM_PORTAL_INTEROP)) { require( address(_output.superFaultDisputeGameImpl) == address(0), diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index 07683d3fca5..2f5ff24e752 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -3,7 +3,6 @@ pragma solidity 0.8.15; import { Script } from "forge-std/Script.sol"; -import { DevFeatures } from "src/libraries/DevFeatures.sol"; import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; import { Solarray } from "scripts/libraries/Solarray.sol"; import { ChainAssertions } from "scripts/deploy/ChainAssertions.sol"; @@ -123,13 +122,6 @@ contract DeployOPChain is Script { checkOutput(_input, output_); } - // -------- Features -------- - - function isDevFeatureV2DisputeGamesEnabled(address _opcmAddr) internal view returns (bool) { - IOPContractsManager opcm = IOPContractsManager(_opcmAddr); - return DevFeatures.isDevFeatureEnabled(opcm.devFeatureBitmap(), DevFeatures.DEPLOY_V2_DISPUTE_GAMES); - } - // -------- Validations -------- function checkInput(Types.DeployOPChainInput memory _i) public view { @@ -176,16 +168,6 @@ contract DeployOPChain is Script { address(_o.ethLockboxProxy) ); - if (!isDevFeatureV2DisputeGamesEnabled(_i.opcm)) { - // Only check dispute game contracts if v2 dispute games are not enabled. - // When v2 contracts are enabled, we no longer deploy dispute games per chain - addrs2 = Solarray.extend(addrs2, Solarray.addresses(address(_o.permissionedDisputeGame))); - - // TODO: Eventually switch from Permissioned to Permissionless. Add these addresses back in. - // address(_o.delayedWETHPermissionlessGameProxy) - // address(_o.faultDisputeGame()), - } - DeployUtils.assertValidContractAddresses(Solarray.extend(addrs1, addrs2)); _assertValidDeploy(_i, _o); } @@ -209,11 +191,8 @@ contract DeployOPChain is Script { }); // Check dispute games - address expectedPDGImpl = address(_o.permissionedDisputeGame); - if (isDevFeatureV2DisputeGamesEnabled(_i.opcm)) { - // With v2 game contracts enabled, we use the predeployed pdg implementation - expectedPDGImpl = IOPContractsManager(_i.opcm).implementations().permissionedDisputeGameV2Impl; - } + // With v2 game contracts enabled, we use the predeployed pdg implementation + address expectedPDGImpl = IOPContractsManager(_i.opcm).implementations().permissionedDisputeGameV2Impl; ChainAssertions.checkDisputeGameFactory( _o.disputeGameFactoryProxy, _i.opChainProxyAdminOwner, expectedPDGImpl, true ); diff --git a/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol b/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol index cf0cf807f40..ea8374a9519 100644 --- a/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol @@ -415,29 +415,6 @@ contract VerifyOPCM is Script { string memory artifactPath = _buildArtifactPath(_target.name); console.log(string.concat(" Expected Runtime Artifact: ", artifactPath)); - // Check if this is a V1 dispute game that should be skipped - if (_isV1DisputeGameImplementation(_target.name) && _target.blueprint) { - if (_isV2DisputeGamesEnabled(_opcm)) { - console.log("[SKIP] Dispute game blueprint not deployed (dispute game v2 feature enabled)"); - return true; // Consider this "verified" when feature is on - } else if (_target.addr == address(0)) { - console.log("[FAIL] Dispute game blueprint not deployed (dispute game v2 feature disabled)"); - success = false; - } - } - // Check if this is a V2 dispute game that should be skipped - if (_isV2DisputeGameImplementation(_target.name)) { - if (!_isV2DisputeGamesEnabled(_opcm)) { - if (_target.addr == address(0)) { - console.log("[SKIP] V2 dispute game not deployed (feature disabled)"); - return true; // Consider this "verified" when feature is off - } else { - console.log("[FAIL] ERROR: V2 dispute game deployed but feature disabled"); - success = false; - } - } - // If feature is enabled, continue with normal verification - } // Check if this is a Super dispute game that should be skipped if (_isSuperDisputeGameImplementation(_target.name)) { if (!_isSuperDisputeGamesEnabled(_opcm)) { @@ -570,14 +547,6 @@ contract VerifyOPCM is Script { return bytes(Process.bash(cmd)); } - /// @notice Checks if V2 dispute games feature is enabled in the dev feature bitmap. - /// @param _opcm The OPContractsManager to check. - /// @return True if V2 dispute games are enabled. - function _isV2DisputeGamesEnabled(IOPContractsManager _opcm) internal view returns (bool) { - bytes32 bitmap = _opcm.devFeatureBitmap(); - return DevFeatures.isDevFeatureEnabled(bitmap, DevFeatures.DEPLOY_V2_DISPUTE_GAMES); - } - /// @notice Checks if super dispute games feature is enabled in the dev feature bitmap. /// @param _opcm The OPContractsManager to check. /// @return True if super dispute games are enabled. diff --git a/packages/contracts-bedrock/scripts/libraries/Config.sol b/packages/contracts-bedrock/scripts/libraries/Config.sol index 4b5a2ed6339..70988a582cf 100644 --- a/packages/contracts-bedrock/scripts/libraries/Config.sol +++ b/packages/contracts-bedrock/scripts/libraries/Config.sol @@ -276,16 +276,6 @@ library Config { return vm.envOr("DEV_FEATURE__OPTIMISM_PORTAL_INTEROP", false); } - /// @notice Returns true if the development feature cannon_kona is enabled. - function devFeatureCannonKona() internal view returns (bool) { - return vm.envOr("DEV_FEATURE__CANNON_KONA", false); - } - - /// @notice Returns true if the development feature deploy_v2_dispute_games is enabled. - function devFeatureDeployV2DisputeGames() internal view returns (bool) { - return vm.envOr("DEV_FEATURE__DEPLOY_V2_DISPUTE_GAMES", false); - } - /// @notice Returns true if the development feature opcm_v2 is enabled. function devFeatureOpcmV2() internal view returns (bool) { return vm.envOr("DEV_FEATURE__OPCM_V2", false); diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json index 33f8880d27e..b5871765022 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManager.json @@ -163,26 +163,6 @@ "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", diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerContainer.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerContainer.json index f81f6cda750..3552e0c8e4e 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerContainer.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerContainer.json @@ -27,26 +27,6 @@ "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 OPContractsManagerContainer.Blueprints", @@ -194,26 +174,6 @@ "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 OPContractsManagerContainer.Blueprints", diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerContractsContainer.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerContractsContainer.json index a3dc49e896f..0014bbfb7ce 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerContractsContainer.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerContractsContainer.json @@ -27,26 +27,6 @@ "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", @@ -202,26 +182,6 @@ "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", diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json index a8798bdcb95..08c026dbfc7 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerDeployer.json @@ -53,26 +53,6 @@ "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", @@ -523,17 +503,6 @@ "name": "InvalidChainId", "type": "error" }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "devFeature", - "type": "bytes32" - } - ], - "name": "InvalidDevFeatureAccess", - "type": "error" - }, { "inputs": [ { diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerGameTypeAdder.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerGameTypeAdder.json index 16debf40454..60a7aed15bb 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerGameTypeAdder.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerGameTypeAdder.json @@ -146,26 +146,6 @@ "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", @@ -437,17 +417,6 @@ "name": "IdentityPrecompileCallFailed", "type": "error" }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "devFeature", - "type": "bytes32" - } - ], - "name": "InvalidDevFeatureAccess", - "type": "error" - }, { "inputs": [], "name": "InvalidGameArgsLength", diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerInteropMigrator.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerInteropMigrator.json index 5644c7edb07..3e06c679ab7 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerInteropMigrator.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerInteropMigrator.json @@ -53,26 +53,6 @@ "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", diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerStandardValidator.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerStandardValidator.json index 7d149dff61c..a6810ae7596 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerStandardValidator.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerStandardValidator.json @@ -62,6 +62,16 @@ "internalType": "address", "name": "mipsImpl", "type": "address" + }, + { + "internalType": "address", + "name": "faultDisputeGameImpl", + "type": "address" + }, + { + "internalType": "address", + "name": "permissionedDisputeGameImpl", + "type": "address" } ], "internalType": "struct OPContractsManagerStandardValidator.Implementations", @@ -175,6 +185,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "faultDisputeGameImpl", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "l1CrossDomainMessengerImpl", @@ -281,12 +304,12 @@ }, { "inputs": [], - "name": "permissionedDisputeGameVersion", + "name": "permissionedDisputeGameImpl", "outputs": [ { - "internalType": "string", + "internalType": "address", "name": "", - "type": "string" + "type": "address" } ], "stateMutability": "view", diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUpgrader.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUpgrader.json index 2df9bcd824a..29f55b0bbc7 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUpgrader.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUpgrader.json @@ -53,26 +53,6 @@ "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", @@ -351,17 +331,6 @@ "name": "IdentityPrecompileCallFailed", "type": "error" }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "devFeature", - "type": "bytes32" - } - ], - "name": "InvalidDevFeatureAccess", - "type": "error" - }, { "inputs": [], "name": "InvalidGameArgsLength", diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUtils.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUtils.json index b862e0fc33f..50668d05a9b 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUtils.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerUtils.json @@ -40,26 +40,6 @@ "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 IOPContractsManagerContainer.Blueprints", @@ -371,6 +351,42 @@ "stateMutability": "pure", "type": "function" }, + { + "inputs": [ + { + "components": [ + { + "internalType": "string", + "name": "key", + "type": "string" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "internalType": "struct OPContractsManagerUtils.ExtraInstruction", + "name": "_instruction", + "type": "tuple" + }, + { + "internalType": "string", + "name": "_key", + "type": "string" + } + ], + "name": "isMatchingInstructionByKey", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "pure", + "type": "function" + }, { "inputs": [ { diff --git a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerV2.json b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerV2.json index 55019089504..3c406c0c88f 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPContractsManagerV2.json +++ b/packages/contracts-bedrock/snapshots/abi/OPContractsManagerV2.json @@ -50,26 +50,6 @@ "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 IOPContractsManagerContainer.Blueprints", @@ -232,6 +212,11 @@ "internalType": "struct OPContractsManagerV2.DisputeGameConfig[]", "name": "disputeGameConfigs", "type": "tuple[]" + }, + { + "internalType": "bool", + "name": "useCustomGasToken", + "type": "bool" } ], "internalType": "struct OPContractsManagerV2.FullConfig", @@ -700,6 +685,11 @@ "name": "NotABlueprint", "type": "error" }, + { + "inputs": [], + "name": "OPContractsManagerV2_CannotUpgradeToCustomGasToken", + "type": "error" + }, { "inputs": [], "name": "OPContractsManagerV2_InvalidGameConfigs", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 4b748b3cd1e..9b35c8a4945 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -24,12 +24,12 @@ "sourceCodeHash": "0xfca613b5d055ffc4c3cbccb0773ddb9030abedc1aa6508c9e2e7727cc0cd617b" }, "src/L1/OPContractsManager.sol:OPContractsManager": { - "initCodeHash": "0x0cb3dfc803e34cd2cae5798e17f29bebbbb861c82cbe8258d6677aedcebcda74", - "sourceCodeHash": "0xb1dffb96b3ba20aaaadaf1110be06540ab03be79b7c6603e0b1ff3486957c8fc" + "initCodeHash": "0x51bb4fa1d01503ec16e8611ac1e2f042ea51280310f1cca2a15a4826acfc2db5", + "sourceCodeHash": "0xacc5a0e75797686ad9545dcae82c89b2ca847ba42988eb63466ef03f4e1c739e" }, "src/L1/OPContractsManagerStandardValidator.sol:OPContractsManagerStandardValidator": { - "initCodeHash": "0x0c8b15453d0f0bc5d9af07f104505e0bbb2b358f0df418289822fb73a8652b30", - "sourceCodeHash": "0x8c156f9f46ae60d928dcc49355519281d019cafabb327103db3094f28ed03537" + "initCodeHash": "0xdec828fdb9f9bb7a35ca03d851b041fcd088681957642e949b5d320358d9b9a1", + "sourceCodeHash": "0x17231caf75773e159b91ad37d798c600ed9662b77c236143022456dc9eb83e47" }, "src/L1/OptimismPortal2.sol:OptimismPortal2": { "initCodeHash": "0x2c01bc6c0a55a1a27263224e05c1b28703ff85c61075bae7ab384b3043820ed2", @@ -52,8 +52,8 @@ "sourceCodeHash": "0xb3184aa5d95a82109e7134d1f61941b30e25f655b9849a0e303d04bbce0cde0b" }, "src/L1/opcm/OPContractsManagerV2.sol:OPContractsManagerV2": { - "initCodeHash": "0x9b6b84f8b87f60c5a38460e78aa6768205f741b17fa60c6d93a1094a8681bd79", - "sourceCodeHash": "0x846dca230392d8e378a2e208a708fc9be342715fd1ca3e3d7c1700f667e25fff" + "initCodeHash": "0x08ce57f50f66685b62b09597a30b5080a140bb3c3cb4f282524200f96c321996", + "sourceCodeHash": "0x0a49fca3800abd9da52ca7fccb018af451c7f988328c826f9a0acb80395bff15" }, "src/L2/BaseFeeVault.sol:BaseFeeVault": { "initCodeHash": "0x838bbd7f381e84e21887f72bd1da605bfc4588b3c39aed96cbce67c09335b3ee", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerContainer.json b/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerContainer.json index 368a144ddcb..891f048bf30 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerContainer.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerContainer.json @@ -1,6 +1,6 @@ [ { - "bytes": "288", + "bytes": "160", "label": "bps", "offset": 0, "slot": "0", @@ -10,7 +10,7 @@ "bytes": "608", "label": "impls", "offset": 0, - "slot": "9", + "slot": "5", "type": "struct OPContractsManagerContainer.Implementations" } ] \ 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 9487d230af8..a6f0bb30b8c 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerContractsContainer.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerContractsContainer.json @@ -1,6 +1,6 @@ [ { - "bytes": "288", + "bytes": "160", "label": "blueprint", "offset": 0, "slot": "0", @@ -10,7 +10,7 @@ "bytes": "576", "label": "implementation", "offset": 0, - "slot": "9", + "slot": "5", "type": "struct OPContractsManager.Implementations" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerStandardValidator.json b/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerStandardValidator.json index 4b248cfef65..c9a280db30b 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerStandardValidator.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OPContractsManagerStandardValidator.json @@ -111,11 +111,25 @@ "slot": "15", "type": "address" }, + { + "bytes": "20", + "label": "faultDisputeGameImpl", + "offset": 0, + "slot": "16", + "type": "address" + }, + { + "bytes": "20", + "label": "permissionedDisputeGameImpl", + "offset": 0, + "slot": "17", + "type": "address" + }, { "bytes": "32", "label": "devFeatureBitmap", "offset": 0, - "slot": "16", + "slot": "18", "type": "bytes32" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index 20ab0f10f79..b35ee3d98a4 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -473,9 +473,6 @@ abstract contract OPContractsManagerBase { ) internal { - if (!isSuperGameVariant(_gameType) && !isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - revert OPContractsManager.InvalidDevFeatureAccess(DevFeatures.DEPLOY_V2_DISPUTE_GAMES); - } _dgf.setImplementation(_gameType, _newGame, _gameArgs); } @@ -602,142 +599,48 @@ contract OPContractsManagerGameTypeAdder is OPContractsManagerBase { IFaultDisputeGame existingGame = IFaultDisputeGame(address(getGameImplementation(dgf, gameConfig.disputeGameType))); - if ( - isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES) - || isSuperGameVariant(gameConfig.disputeGameType) - ) { - if ( - isCannonGameVariant(gameConfig.disputeGameType) - || (isDevFeatureEnabled(DevFeatures.CANNON_KONA) && isKonaGameVariant(gameConfig.disputeGameType)) - ) { - address impl = getDisputeGameImplementation(gameConfig.disputeGameType); - bytes memory gameArgs = LibGameArgs.encode( - LibGameArgs.GameArgs({ - absolutePrestate: gameConfig.disputeAbsolutePrestate.raw(), - vm: address(gameConfig.vm), - anchorStateRegistry: address(getAnchorStateRegistry(ISystemConfig(gameConfig.systemConfig))), - weth: address(outputs[i].delayedWETH), - // must be zero for SUPER game types - l2ChainId: isSuperGameVariant(gameConfig.disputeGameType) ? 0 : l2ChainId, - proposer: address(0), - challenger: address(0) - }) - ); - - setDGFImplementation(dgf, gameConfig.disputeGameType, IDisputeGame(impl), gameArgs); - outputs[i].faultDisputeGame = IFaultDisputeGame(impl); - } else if ( - gameConfig.disputeGameType.raw() == GameTypes.PERMISSIONED_CANNON.raw() - || gameConfig.disputeGameType.raw() == GameTypes.SUPER_PERMISSIONED_CANNON.raw() - ) { - address impl = getDisputeGameImplementation(gameConfig.disputeGameType); - bytes memory gameArgs = LibGameArgs.encode( - LibGameArgs.GameArgs({ - absolutePrestate: gameConfig.disputeAbsolutePrestate.raw(), - vm: address(gameConfig.vm), - anchorStateRegistry: address(getAnchorStateRegistry(ISystemConfig(gameConfig.systemConfig))), - weth: address(outputs[i].delayedWETH), - l2ChainId: gameConfig.disputeGameType.raw() == GameTypes.PERMISSIONED_CANNON.raw() - ? l2ChainId - : 0, // must be zero for SUPER gam types - proposer: getProposer( - dgf, IPermissionedDisputeGame(address(existingGame)), gameConfig.disputeGameType - ), - challenger: getChallenger( - dgf, IPermissionedDisputeGame(address(existingGame)), gameConfig.disputeGameType - ) - }) - ); - setDGFImplementation(dgf, gameConfig.disputeGameType, IDisputeGame(impl), gameArgs); - outputs[i].faultDisputeGame = IFaultDisputeGame(payable(impl)); - } else { - revert OPContractsManagerGameTypeAdder_UnsupportedGameType(); - } - } else { - // Determine the contract name and blueprints for the game type. - string memory gameContractName; - address blueprint1; - address blueprint2; - uint256 gameL2ChainId; - - // Separate context to avoid stack too deep. - { - // Grab the blueprints once since we'll need it multiple times below. - OPContractsManager.Blueprints memory bps = getBlueprints(); - - // Determine the contract name and blueprints for the game type. - if ( - gameConfig.disputeGameType.raw() == GameTypes.CANNON.raw() - || ( - isDevFeatureEnabled(DevFeatures.CANNON_KONA) - && gameConfig.disputeGameType.raw() == GameTypes.CANNON_KONA.raw() - ) - ) { - gameContractName = "FaultDisputeGame"; - blueprint1 = bps.permissionlessDisputeGame1; - blueprint2 = bps.permissionlessDisputeGame2; - gameL2ChainId = l2ChainId; - } else if (gameConfig.disputeGameType.raw() == GameTypes.PERMISSIONED_CANNON.raw()) { - gameContractName = "PermissionedDisputeGame"; - blueprint1 = bps.permissionedDisputeGame1; - blueprint2 = bps.permissionedDisputeGame2; - gameL2ChainId = l2ChainId; - } else { - revert OPContractsManagerGameTypeAdder_UnsupportedGameType(); - } - } + if (isCannonGameVariant(gameConfig.disputeGameType) || isKonaGameVariant(gameConfig.disputeGameType)) { + address impl = getDisputeGameImplementation(gameConfig.disputeGameType); + bytes memory gameArgs = LibGameArgs.encode( + LibGameArgs.GameArgs({ + absolutePrestate: gameConfig.disputeAbsolutePrestate.raw(), + vm: address(gameConfig.vm), + anchorStateRegistry: address(getAnchorStateRegistry(ISystemConfig(gameConfig.systemConfig))), + weth: address(outputs[i].delayedWETH), + // must be zero for SUPER game types + l2ChainId: isSuperGameVariant(gameConfig.disputeGameType) ? 0 : l2ChainId, + proposer: address(0), + challenger: address(0) + }) + ); - // Encode the constructor data for the game type. - bytes memory constructorData; - if (gameConfig.permissioned) { - constructorData = encodePermissionedFDGConstructor( - IFaultDisputeGame.GameConstructorParams( - gameConfig.disputeGameType, - gameConfig.disputeAbsolutePrestate, - gameConfig.disputeMaxGameDepth, - gameConfig.disputeSplitDepth, - gameConfig.disputeClockExtension, - gameConfig.disputeMaxClockDuration, - gameConfig.vm, - outputs[i].delayedWETH, - getAnchorStateRegistry(gameConfig.systemConfig), - gameL2ChainId + setDGFImplementation(dgf, gameConfig.disputeGameType, IDisputeGame(impl), gameArgs); + outputs[i].faultDisputeGame = IFaultDisputeGame(impl); + } else if ( + gameConfig.disputeGameType.raw() == GameTypes.PERMISSIONED_CANNON.raw() + || gameConfig.disputeGameType.raw() == GameTypes.SUPER_PERMISSIONED_CANNON.raw() + ) { + address impl = getDisputeGameImplementation(gameConfig.disputeGameType); + bytes memory gameArgs = LibGameArgs.encode( + LibGameArgs.GameArgs({ + absolutePrestate: gameConfig.disputeAbsolutePrestate.raw(), + vm: address(gameConfig.vm), + anchorStateRegistry: address(getAnchorStateRegistry(ISystemConfig(gameConfig.systemConfig))), + weth: address(outputs[i].delayedWETH), + l2ChainId: gameConfig.disputeGameType.raw() == GameTypes.PERMISSIONED_CANNON.raw() ? l2ChainId : 0, // must + // be zero for SUPER gam types + proposer: getProposer( + dgf, IPermissionedDisputeGame(address(existingGame)), gameConfig.disputeGameType ), - getProposer(dgf, IPermissionedDisputeGame(address(existingGame)), gameConfig.disputeGameType), - getChallenger(dgf, IPermissionedDisputeGame(address(existingGame)), gameConfig.disputeGameType) - ); - } else { - constructorData = encodePermissionlessFDGConstructor( - IFaultDisputeGame.GameConstructorParams( - gameConfig.disputeGameType, - gameConfig.disputeAbsolutePrestate, - gameConfig.disputeMaxGameDepth, - gameConfig.disputeSplitDepth, - gameConfig.disputeClockExtension, - gameConfig.disputeMaxClockDuration, - gameConfig.vm, - outputs[i].delayedWETH, - getAnchorStateRegistry(gameConfig.systemConfig), - gameL2ChainId + challenger: getChallenger( + dgf, IPermissionedDisputeGame(address(existingGame)), gameConfig.disputeGameType ) - ); - } - - // Deploy the new game type. - outputs[i].faultDisputeGame = IFaultDisputeGame( - Blueprint.deployFrom( - blueprint1, - blueprint2, - computeSalt(l2ChainId, gameConfig.saltMixer, gameContractName), - constructorData - ) - ); - - // As a last step, register the new game type with the DisputeGameFactory. If the game - // type already exists, then its implementation will be overwritten. - setDGFImplementation( - dgf, gameConfig.disputeGameType, IDisputeGame(address(outputs[i].faultDisputeGame)) + }) ); + setDGFImplementation(dgf, gameConfig.disputeGameType, IDisputeGame(impl), gameArgs); + outputs[i].faultDisputeGame = IFaultDisputeGame(payable(impl)); + } else { + revert OPContractsManagerGameTypeAdder_UnsupportedGameType(); } dgf.setInitBond(gameConfig.disputeGameType, gameConfig.initialBond); @@ -762,17 +665,14 @@ contract OPContractsManagerGameTypeAdder is OPContractsManagerBase { IDisputeGameFactory dgf = IDisputeGameFactory(_prestateUpdateInputs[i].systemConfigProxy.disputeGameFactory()); - uint256 numGameTypes = isDevFeatureEnabled(DevFeatures.CANNON_KONA) ? 6 : 4; // Create an array of all of the potential game types to update. - GameType[] memory gameTypes = new GameType[](numGameTypes); + GameType[] memory gameTypes = new GameType[](6); gameTypes[0] = GameTypes.CANNON; gameTypes[1] = GameTypes.PERMISSIONED_CANNON; gameTypes[2] = GameTypes.SUPER_CANNON; gameTypes[3] = GameTypes.SUPER_PERMISSIONED_CANNON; - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - gameTypes[4] = GameTypes.CANNON_KONA; - gameTypes[5] = GameTypes.SUPER_CANNON_KONA; - } + gameTypes[4] = GameTypes.CANNON_KONA; + gameTypes[5] = GameTypes.SUPER_CANNON_KONA; // Track if we have a legacy game, super game, or both. We will revert if this function // is ever called with a mix of legacy and super games. Should never happen in @@ -1004,86 +904,50 @@ contract OPContractsManagerUpgrader is OPContractsManagerBase { // All chains have the PermissionedDisputeGame, grab that. IDisputeGame permissionedDisputeGame = getGameImplementation(dgf, GameTypes.PERMISSIONED_CANNON); - if (!isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - // Update the PermissionedDisputeGame. - // We're reusing the same DelayedWETH and ASR contracts. - deployAndSetNewGameImplV1({ - _l2ChainId: _l2ChainId, - _disputeGame: permissionedDisputeGame, - _newDelayedWeth: getWETHV1(IFaultDisputeGame(address(permissionedDisputeGame))), - _newAnchorStateRegistryProxy: getAnchorStateRegistryV1(IFaultDisputeGame(address(permissionedDisputeGame))), - _gameType: GameTypes.PERMISSIONED_CANNON, - _opChainConfig: _opChainConfig - }); - - // Now retrieve the permissionless game. - IDisputeGame permissionlessDisputeGame = getGameImplementation(dgf, GameTypes.CANNON); + setNewPermissionedGameImplV2({ + _impls: _impls, + _l2ChainId: _l2ChainId, + _disputeGame: permissionedDisputeGame, + _newDelayedWeth: getWETH(dgf, permissionedDisputeGame, GameTypes.PERMISSIONED_CANNON), + _newAnchorStateRegistryProxy: getAnchorStateRegistry( + dgf, permissionedDisputeGame, GameTypes.PERMISSIONED_CANNON + ), + _opChainConfig: _opChainConfig + }); - // If it exists, replace its implementation. - // We're reusing the same DelayedWETH and ASR contracts. - if (address(permissionlessDisputeGame) != address(0)) { - deployAndSetNewGameImplV1({ - _l2ChainId: _l2ChainId, - _disputeGame: permissionlessDisputeGame, - _newDelayedWeth: getWETHV1(IFaultDisputeGame(address(permissionlessDisputeGame))), - _newAnchorStateRegistryProxy: getAnchorStateRegistryV1( - IFaultDisputeGame(address(permissionlessDisputeGame)) - ), - _gameType: GameTypes.CANNON, - _opChainConfig: _opChainConfig - }); - } - } else { - setNewPermissionedGameImplV2({ + IDisputeGame permissionlessDisputeGame = getGameImplementation(dgf, GameTypes.CANNON); + + // If it exists, replace its implementation. + // We're reusing the same DelayedWETH and ASR contracts. + if (address(permissionlessDisputeGame) != address(0)) { + IDisputeGameFactory disputeGameFactory = + IDisputeGameFactory(_opChainConfig.systemConfigProxy.disputeGameFactory()); + Claim cannonPrestate = _opChainConfig.cannonPrestate.raw() != bytes32(0) + ? _opChainConfig.cannonPrestate + : getAbsolutePrestate(disputeGameFactory, address(permissionlessDisputeGame), GameTypes.CANNON); + setNewPermissionlessGameImplV2({ _impls: _impls, _l2ChainId: _l2ChainId, - _disputeGame: permissionedDisputeGame, - _newDelayedWeth: getWETH(dgf, permissionedDisputeGame, GameTypes.PERMISSIONED_CANNON), - _newAnchorStateRegistryProxy: getAnchorStateRegistry( - dgf, permissionedDisputeGame, GameTypes.PERMISSIONED_CANNON - ), - _opChainConfig: _opChainConfig + _newAbsolutePrestate: cannonPrestate, + _newDelayedWeth: getWETH(dgf, permissionlessDisputeGame, GameTypes.CANNON), + _newAnchorStateRegistryProxy: getAnchorStateRegistry(dgf, permissionlessDisputeGame, GameTypes.CANNON), + _gameType: GameTypes.CANNON, + _disputeGameFactory: disputeGameFactory }); - IDisputeGame permissionlessDisputeGame = getGameImplementation(dgf, GameTypes.CANNON); - - // If it exists, replace its implementation. - // We're reusing the same DelayedWETH and ASR contracts. - if (address(permissionlessDisputeGame) != address(0)) { - IDisputeGameFactory disputeGameFactory = - IDisputeGameFactory(_opChainConfig.systemConfigProxy.disputeGameFactory()); - Claim cannonPrestate = _opChainConfig.cannonPrestate.raw() != bytes32(0) - ? _opChainConfig.cannonPrestate - : getAbsolutePrestate(disputeGameFactory, address(permissionlessDisputeGame), GameTypes.CANNON); + if (_opChainConfig.cannonKonaPrestate.raw() != bytes32(0)) { setNewPermissionlessGameImplV2({ _impls: _impls, _l2ChainId: _l2ChainId, - _newAbsolutePrestate: cannonPrestate, + _newAbsolutePrestate: _opChainConfig.cannonKonaPrestate, + // CANNON and CANNON_KONA use the same weth and asr proxy addresses _newDelayedWeth: getWETH(dgf, permissionlessDisputeGame, GameTypes.CANNON), _newAnchorStateRegistryProxy: getAnchorStateRegistry(dgf, permissionlessDisputeGame, GameTypes.CANNON), - _gameType: GameTypes.CANNON, + _gameType: GameTypes.CANNON_KONA, _disputeGameFactory: disputeGameFactory }); - - if ( - isDevFeatureEnabled(DevFeatures.CANNON_KONA) - && _opChainConfig.cannonKonaPrestate.raw() != bytes32(0) - ) { - setNewPermissionlessGameImplV2({ - _impls: _impls, - _l2ChainId: _l2ChainId, - _newAbsolutePrestate: _opChainConfig.cannonKonaPrestate, - // CANNON and CANNON_KONA use the same weth and asr proxy addresses - _newDelayedWeth: getWETH(dgf, permissionlessDisputeGame, GameTypes.CANNON), - _newAnchorStateRegistryProxy: getAnchorStateRegistry( - dgf, permissionlessDisputeGame, GameTypes.CANNON - ), - _gameType: GameTypes.CANNON_KONA, - _disputeGameFactory: disputeGameFactory - }); - uint256 initialCannonGameBond = disputeGameFactory.initBonds(GameTypes.CANNON); - disputeGameFactory.setInitBond(GameTypes.CANNON_KONA, initialCannonGameBond); - } + uint256 initialCannonGameBond = disputeGameFactory.initBonds(GameTypes.CANNON); + disputeGameFactory.setInitBond(GameTypes.CANNON_KONA, initialCannonGameBond); } } } @@ -1124,80 +988,6 @@ contract OPContractsManagerUpgrader is OPContractsManagerBase { assertValidContractAddress(address(_config.systemConfigProxy)); } - /// @notice Deploys and sets a new v1 dispute game implementation - /// @param _l2ChainId The L2 chain ID - /// @param _disputeGame The current dispute game implementation - /// @param _newDelayedWeth The new delayed WETH implementation - /// @param _newAnchorStateRegistryProxy The new anchor state registry proxy - /// @param _gameType The type of game to deploy - /// @param _opChainConfig The OP chain configuration - function deployAndSetNewGameImplV1( - uint256 _l2ChainId, - IDisputeGame _disputeGame, - IDelayedWETH _newDelayedWeth, - IAnchorStateRegistry _newAnchorStateRegistryProxy, - GameType _gameType, - 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.weth = _newDelayedWeth; - params.anchorStateRegistry = _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.cannonPrestate) != bytes32(0)) { - params.absolutePrestate = _opChainConfig.cannonPrestate; - } - - // As a sanity check, if the prestate is zero here, revert. - if (params.absolutePrestate.raw() == bytes32(0)) { - revert OPContractsManager.PrestateNotSet(); - } - - IDisputeGame newGame; - if (GameType.unwrap(_gameType) == GameType.unwrap(GameTypes.PERMISSIONED_CANNON)) { - address proposer = getProposerV1(IPermissionedDisputeGame(address(_disputeGame))); - address challenger = getChallengerV1(IPermissionedDisputeGame(address(_disputeGame))); - newGame = IDisputeGame( - Blueprint.deployFrom( - bps.permissionedDisputeGame1, - bps.permissionedDisputeGame2, - computeSalt( - _l2ChainId, reusableSaltMixer(_opChainConfig.systemConfigProxy), "PermissionedDisputeGame" - ), - encodePermissionedFDGConstructor(params, proposer, challenger) - ) - ); - } else { - newGame = IDisputeGame( - Blueprint.deployFrom( - bps.permissionlessDisputeGame1, - bps.permissionlessDisputeGame2, - computeSalt( - _l2ChainId, reusableSaltMixer(_opChainConfig.systemConfigProxy), "PermissionlessDisputeGame" - ), - encodePermissionlessFDGConstructor(params) - ) - ); - } - - // Grab the DisputeGameFactory from the SystemConfig. - IDisputeGameFactory dgf = IDisputeGameFactory(_opChainConfig.systemConfigProxy.disputeGameFactory()); - - // Set the new implementation. - setDGFImplementation(dgf, _gameType, IDisputeGame(newGame)); - } - /// @notice Sets the latest permissioned dispute game v2 implementation /// @param _impls The container for the new dispute game implementations. /// @param _l2ChainId The L2 chain ID @@ -1418,32 +1208,6 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { ) ); - if (!isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - output.permissionedDisputeGame = IPermissionedDisputeGame( - Blueprint.deployFrom( - blueprint.permissionedDisputeGame1, - blueprint.permissionedDisputeGame2, - computeSalt(_input.l2ChainId, _input.saltMixer, "PermissionedDisputeGame"), - encodePermissionedFDGConstructor( - IFaultDisputeGame.GameConstructorParams({ - gameType: GameTypes.PERMISSIONED_CANNON, - absolutePrestate: _input.disputeAbsolutePrestate, - maxGameDepth: _input.disputeMaxGameDepth, - splitDepth: _input.disputeSplitDepth, - clockExtension: _input.disputeClockExtension, - maxClockDuration: _input.disputeMaxClockDuration, - vm: IBigStepper(implementation.mipsImpl), - weth: IDelayedWETH(payable(address(output.delayedWETHPermissionedGameProxy))), - anchorStateRegistry: IAnchorStateRegistry(address(output.anchorStateRegistryProxy)), - l2ChainId: _input.l2ChainId - }), - _input.roles.proposer, - _input.roles.challenger - ) - ) - ); - } - // -------- Set and Initialize Proxy Implementations -------- bytes memory data; @@ -1534,18 +1298,8 @@ contract OPContractsManagerDeployer is OPContractsManagerBase { implementation.disputeGameFactoryImpl, data ); - // Register the appropriate dispute game implementation based on the feature flag - if (isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - // Extracted to helper function to avoid stack too deep error - _registerPermissionedGameV2(_input, implementation, output); - } else { - // Register v1 implementation for PERMISSIONED_CANNON game type - setDGFImplementation( - output.disputeGameFactoryProxy, - GameTypes.PERMISSIONED_CANNON, - IDisputeGame(address(output.permissionedDisputeGame)) - ); - } + // Extracted to helper function to avoid stack too deep error + _registerPermissionedGameV2(_input, implementation, output); transferOwnership(address(output.disputeGameFactoryProxy), address(_input.roles.opChainProxyAdminOwner)); @@ -1850,13 +1604,9 @@ contract OPContractsManagerInteropMigrator is OPContractsManagerBase { if (_input.opChainConfigs[i].cannonPrestate.raw() != _input.opChainConfigs[0].cannonPrestate.raw()) { revert OPContractsManagerInteropMigrator_AbsolutePrestateMismatch(); } - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - if ( - _input.opChainConfigs[i].cannonKonaPrestate.raw() - != _input.opChainConfigs[0].cannonKonaPrestate.raw() - ) { - revert OPContractsManagerInteropMigrator_AbsolutePrestateMismatch(); - } + if (_input.opChainConfigs[i].cannonKonaPrestate.raw() != _input.opChainConfigs[0].cannonKonaPrestate.raw()) + { + revert OPContractsManagerInteropMigrator_AbsolutePrestateMismatch(); } } @@ -1982,10 +1732,8 @@ contract OPContractsManagerInteropMigrator is OPContractsManagerBase { clearGameImplementation(oldDisputeGameFactory, GameTypes.SUPER_CANNON); clearGameImplementation(oldDisputeGameFactory, GameTypes.PERMISSIONED_CANNON); clearGameImplementation(oldDisputeGameFactory, GameTypes.SUPER_PERMISSIONED_CANNON); - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - clearGameImplementation(oldDisputeGameFactory, GameTypes.CANNON_KONA); - clearGameImplementation(oldDisputeGameFactory, GameTypes.SUPER_CANNON_KONA); - } + clearGameImplementation(oldDisputeGameFactory, GameTypes.CANNON_KONA); + clearGameImplementation(oldDisputeGameFactory, GameTypes.SUPER_CANNON_KONA); // Migrate the portal to the new ETHLockbox and AnchorStateRegistry. portals[i].migrateToSuperRoots(newEthLockbox, newAnchorStateRegistry); @@ -2078,7 +1826,7 @@ contract OPContractsManagerInteropMigrator is OPContractsManagerBase { // If the cannon-kona game is being used, set that up too. bytes32 cannonKonaPrestate = _input.opChainConfigs[0].cannonKonaPrestate.raw(); - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA) && cannonKonaPrestate != bytes32(0)) { + if (cannonKonaPrestate != bytes32(0)) { gameArgs = LibGameArgs.encode( LibGameArgs.GameArgs({ absolutePrestate: cannonKonaPrestate, @@ -2099,11 +1847,7 @@ contract OPContractsManagerInteropMigrator is OPContractsManagerBase { } function clearGameImplementation(IDisputeGameFactory _dgf, GameType _gameType) internal { - if (isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - _dgf.setImplementation(_gameType, IDisputeGame(address(0)), hex""); - } else { - _dgf.setImplementation(_gameType, IDisputeGame(address(0))); - } + _dgf.setImplementation(_gameType, IDisputeGame(address(0)), hex""); } } @@ -2173,10 +1917,6 @@ contract OPContractsManager is ISemver { address proxyAdmin; address l1ChugSplashProxy; address resolvedDelegateProxy; - address permissionedDisputeGame1; - address permissionedDisputeGame2; - address permissionlessDisputeGame1; - address permissionlessDisputeGame2; } /// @notice The latest implementation contracts for the OP Stack. @@ -2237,9 +1977,9 @@ contract OPContractsManager is ISemver { // -------- Constants and Variables -------- - /// @custom:semver 5.8.0 + /// @custom:semver 6.0.0 function version() public pure virtual returns (string memory) { - return "5.8.0"; + return "6.0.0"; } OPContractsManagerGameTypeAdder public immutable opcmGameTypeAdder; diff --git a/packages/contracts-bedrock/src/L1/OPContractsManagerStandardValidator.sol b/packages/contracts-bedrock/src/L1/OPContractsManagerStandardValidator.sol index d3f498bbb06..7831f956fde 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManagerStandardValidator.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManagerStandardValidator.sol @@ -40,8 +40,8 @@ import { IBigStepper } from "interfaces/dispute/IBigStepper.sol"; /// before and after an upgrade. contract OPContractsManagerStandardValidator is ISemver { /// @notice The semantic version of the OPContractsManagerStandardValidator contract. - /// @custom:semver 2.2.0 - string public constant version = "2.2.0"; + /// @custom:semver 2.3.0 + string public constant version = "2.3.0"; /// @notice The SuperchainConfig contract. ISuperchainConfig public superchainConfig; @@ -93,6 +93,12 @@ contract OPContractsManagerStandardValidator is ISemver { /// @notice The MIPS implementation address. address public mipsImpl; + /// @notice The FaultDisputeGame implementation address. + address public faultDisputeGameImpl; + + /// @notice The PermissionedFaultDisputeGame implementation address. + address public permissionedDisputeGameImpl; + /// @notice Bitmap of development features, verification may depend on these features. bytes32 public devFeatureBitmap; @@ -110,6 +116,8 @@ contract OPContractsManagerStandardValidator is ISemver { address anchorStateRegistryImpl; address delayedWETHImpl; address mipsImpl; + address faultDisputeGameImpl; + address permissionedDisputeGameImpl; } /// @notice Struct containing the input parameters for the validation process. @@ -183,6 +191,8 @@ contract OPContractsManagerStandardValidator is ISemver { anchorStateRegistryImpl = _implementations.anchorStateRegistryImpl; delayedWETHImpl = _implementations.delayedWETHImpl; mipsImpl = _implementations.mipsImpl; + faultDisputeGameImpl = _implementations.faultDisputeGameImpl; + permissionedDisputeGameImpl = _implementations.permissionedDisputeGameImpl; } /// @notice Returns a string representing the overrides that are set. @@ -217,15 +227,6 @@ contract OPContractsManagerStandardValidator is ISemver { return challenger; } - /// @notice Returns the expected PermissionedDisputeGame version. - function permissionedDisputeGameVersion() public view returns (string memory) { - if (DevFeatures.isDevFeatureEnabled(devFeatureBitmap, DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - return "2.2.0"; - } else { - return "1.8.0"; - } - } - /// @notice Returns the expected PreimageOracle version. function preimageOracleVersion() public pure returns (string memory) { return "1.1.4"; @@ -655,7 +656,12 @@ contract OPContractsManagerStandardValidator is ISemver { IDisputeGameFactory dgf = IDisputeGameFactory(_args.sysCfg.disputeGameFactory()); errors_ = internalRequire( - LibString.eq(getVersion(game.gameAddress), permissionedDisputeGameVersion()), + LibString.eq( + getVersion(game.gameAddress), + _args.gameType.raw() == GameTypes.PERMISSIONED_CANNON.raw() + ? getVersion(permissionedDisputeGameImpl) + : getVersion(faultDisputeGameImpl) + ), string.concat(errorPrefix, "-20"), errors_ ); @@ -887,18 +893,16 @@ contract OPContractsManagerStandardValidator is ISemver { _overrides, "PLDG" ); - if (DevFeatures.isDevFeatureEnabled(devFeatureBitmap, DevFeatures.CANNON_KONA)) { - _errors = assertValidPermissionlessDisputeGame( - _errors, - _input.sysCfg, - GameTypes.CANNON_KONA, - _input.cannonKonaPrestate, - _input.l2ChainID, - _proxyAdmin, - _overrides, - "CKDG" - ); - } + _errors = assertValidPermissionlessDisputeGame( + _errors, + _input.sysCfg, + GameTypes.CANNON_KONA, + _input.cannonKonaPrestate, + _input.l2ChainID, + _proxyAdmin, + _overrides, + "CKDG" + ); _errors = assertValidETHLockbox(_errors, _input.sysCfg, _proxyAdmin); @@ -942,22 +946,16 @@ contract OPContractsManagerStandardValidator is ISemver { string memory _errorPrefix ) internal - view + pure returns (string memory errors_, bool failed_) { _errorPrefix = string.concat(_errorPrefix, "-GARGS"); - if (DevFeatures.isDevFeatureEnabled(devFeatureBitmap, DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - if (_isPermissioned) { - bool ok = LibGameArgs.isValidPermissionedArgs(_gameArgsBytes); - _errors = internalRequire(ok, string.concat(_errorPrefix, "-10"), _errors); - return (_errors, !ok); - } else { - bool ok = LibGameArgs.isValidPermissionlessArgs(_gameArgsBytes); - _errors = internalRequire(ok, string.concat(_errorPrefix, "-10"), _errors); - return (_errors, !ok); - } + if (_isPermissioned) { + bool ok = LibGameArgs.isValidPermissionedArgs(_gameArgsBytes); + _errors = internalRequire(ok, string.concat(_errorPrefix, "-10"), _errors); + return (_errors, !ok); } else { - bool ok = _gameArgsBytes.length == 0; + bool ok = LibGameArgs.isValidPermissionlessArgs(_gameArgsBytes); _errors = internalRequire(ok, string.concat(_errorPrefix, "-10"), _errors); return (_errors, !ok); } @@ -974,23 +972,7 @@ contract OPContractsManagerStandardValidator is ISemver { view returns (DisputeGameImplementation memory gameImpl_) { - GameType gameType; - LibGameArgs.GameArgs memory gameArgs; - if (DevFeatures.isDevFeatureEnabled(devFeatureBitmap, DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - gameType = _gameType; - gameArgs = LibGameArgs.decode(_gameArgsBytes); - } else { - gameType = _game.gameType(); - gameArgs.absolutePrestate = Claim.unwrap(_game.absolutePrestate()); - gameArgs.vm = address(_game.vm()); - gameArgs.anchorStateRegistry = address(_game.anchorStateRegistry()); - gameArgs.weth = address(_game.weth()); - gameArgs.l2ChainId = _game.l2ChainId(); - if (_gameType.raw() == GameTypes.PERMISSIONED_CANNON.raw()) { - gameArgs.challenger = _game.challenger(); - gameArgs.proposer = _game.proposer(); - } - } + LibGameArgs.GameArgs memory gameArgs = LibGameArgs.decode(_gameArgsBytes); gameImpl_ = DisputeGameImplementation({ gameAddress: address(_game), @@ -998,7 +980,7 @@ contract OPContractsManagerStandardValidator is ISemver { splitDepth: _game.splitDepth(), maxClockDuration: _game.maxClockDuration(), clockExtension: _game.clockExtension(), - gameType: gameType, + gameType: _gameType, l2SequenceNumber: _game.l2SequenceNumber(), absolutePrestate: Claim.wrap(gameArgs.absolutePrestate), vm: IBigStepper(gameArgs.vm), diff --git a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerContainer.sol b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerContainer.sol index 7775cc485ee..01bbeeebcee 100644 --- a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerContainer.sol +++ b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerContainer.sol @@ -18,10 +18,6 @@ contract OPContractsManagerContainer { address proxyAdmin; address l1ChugSplashProxy; address resolvedDelegateProxy; - address permissionedDisputeGame1; - address permissionedDisputeGame2; - address permissionlessDisputeGame1; - address permissionlessDisputeGame2; } /// @notice Addresses of the implementation contracts. diff --git a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerUtils.sol b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerUtils.sol index df7025086db..6aad38d7520 100644 --- a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerUtils.sol +++ b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerUtils.sol @@ -89,6 +89,21 @@ contract OPContractsManagerUtils { return keccak256(abi.encode(_l2ChainId, _saltMixer, _contractName)); } + /// @notice Helper function to check if an instruction matches a given key. + /// @param _instruction The instruction to check. + /// @param _key The key of the instruction to check for. + /// @return True if the instruction matches, false otherwise. + function isMatchingInstructionByKey( + ExtraInstruction memory _instruction, + string memory _key + ) + public + pure + returns (bool) + { + return LibString.eq(_instruction.key, _key); + } + /// @notice Helper function to check if an instruction matches a given key and data. /// @param _instruction The instruction to check. /// @param _key The key of the instruction to check for. diff --git a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerUtilsCaller.sol b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerUtilsCaller.sol index 9cdb91f908d..232deac1dac 100644 --- a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerUtilsCaller.sol +++ b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerUtilsCaller.sol @@ -55,6 +55,24 @@ abstract contract OPContractsManagerUtilsCaller { ); } + /// @notice Helper function to check if an instruction matches a given key. + /// @param _instruction The instruction to check. + /// @param _key The key of the instruction to check for. + /// @return True if the instruction matches, false otherwise. + function _isMatchingInstructionByKey( + IOPContractsManagerUtils.ExtraInstruction memory _instruction, + string memory _key + ) + internal + view + returns (bool) + { + return abi.decode( + _staticcall(abi.encodeCall(IOPContractsManagerUtils.isMatchingInstructionByKey, (_instruction, _key))), + (bool) + ); + } + /// @notice Helper function to check if an instruction matches a given key and data. /// @param _instruction The instruction to check. /// @param _key The key of the instruction to check for. diff --git a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol index 9bdb7daa69c..3ff11b7de41 100644 --- a/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol +++ b/packages/contracts-bedrock/src/L1/opcm/OPContractsManagerV2.sol @@ -116,6 +116,8 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { IResourceMetering.ResourceConfig resourceConfig; // Dispute game configuration. DisputeGameConfig[] disputeGameConfigs; + // CGT + bool useCustomGasToken; } /// @notice Partial input required for an upgrade. @@ -146,6 +148,9 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { /// @notice Thrown when an invalid upgrade instruction is provided. error OPContractsManagerV2_InvalidUpgradeInstruction(string _key); + /// @notice Thrown when a chain attempts to upgrade to custom gas token after initial deployment. + error OPContractsManagerV2_CannotUpgradeToCustomGasToken(); + /// @notice Container of blueprint and implementation contract addresses. IOPContractsManagerContainer public immutable contractsContainer; @@ -161,8 +166,8 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { /// - Major bump: New required sequential upgrade /// - Minor bump: Replacement OPCM for same upgrade /// - Patch bump: Development changes (expected for normal dev work) - /// @custom:semver 6.0.4 - string public constant version = "6.0.4"; + /// @custom:semver 6.0.6 + string public constant version = "6.0.6"; /// @param _contractsContainer The container of blueprint and implementation contract addresses. /// @param _standardValidator The standard validator for this OPCM release. @@ -297,7 +302,14 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { if (SemverComp.lt(version, "7.0.0")) { // Unified DelayedWETH is being deployed for the first time. // TODO:(#18382): Remove this allowance after unified DelayedWETH is deployed. - return _isMatchingInstruction(_instruction, Constants.PERMITTED_PROXY_DEPLOYMENT_KEY, "DelayedWETH"); + if (_isMatchingInstruction(_instruction, Constants.PERMITTED_PROXY_DEPLOYMENT_KEY, "DelayedWETH")) { + return true; + } + // Custom Gas Token is being enabled for the first time. + // TODO:(#18502): Remove this allowance after U18 ships. + if (_isMatchingInstructionByKey(_instruction, "overrides.cfg.useCustomGasToken")) { + return true; + } } // Always return false by default. @@ -310,6 +322,7 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { /// @param _saltMixer The salt mixer for creating new proxies if needed. /// @param _extraInstructions The extra upgrade instructions for the chain. /// @return The chain contracts. + function _loadChainContracts( ISystemConfig _systemConfig, uint256 _l2ChainId, @@ -503,6 +516,7 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { { // Load the full config. return FullConfig({ + disputeGameConfigs: _upgradeInput.disputeGameConfigs, saltMixer: string(bytes.concat(bytes32(uint256(uint160(address(_chainContracts.systemConfig)))))), superchainConfig: abi.decode( _loadBytes( @@ -612,7 +626,15 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { ), (GameType) ), - disputeGameConfigs: _upgradeInput.disputeGameConfigs + useCustomGasToken: abi.decode( + _loadBytes( + address(_chainContracts.systemConfig), + _chainContracts.systemConfig.isCustomGasToken.selector, + "overrides.cfg.useCustomGasToken", + _upgradeInput.extraInstructions + ), + (bool) + ) }); } @@ -816,6 +838,19 @@ contract OPContractsManagerV2 is ISemver, OPContractsManagerUtilsCaller { ); } + // If the custom gas token feature was requested, enable it in the SystemConfig. + // If the cgt is enabled, we skip this step. + if (_cfg.useCustomGasToken && !_cts.systemConfig.isCustomGasToken()) { + // NOTE: Enabling the custom gas token feature is only allowed during initial deployment to prevent + // chains from enabling it during upgrades. Passing in true for this flag during an upgrade is considered an + // error and will revert. + // Revert only if trying to upgrade from CGT disabled to CGT enabled. + if (!_isInitialDeployment) { + revert OPContractsManagerV2_CannotUpgradeToCustomGasToken(); + } + _cts.systemConfig.setFeature(Features.CUSTOM_GAS_TOKEN, true); + } + // If critical transfer is allowed, tranfer ownership of the DisputeGameFactory and // ProxyAdmin to the PAO. During deployments, this means transferring ownership from the // OPCM contract to the target PAO. During upgrades, this would theoretically mean diff --git a/packages/contracts-bedrock/src/libraries/DevFeatures.sol b/packages/contracts-bedrock/src/libraries/DevFeatures.sol index ed46b8c8d2b..2a1dc1854c1 100644 --- a/packages/contracts-bedrock/src/libraries/DevFeatures.sol +++ b/packages/contracts-bedrock/src/libraries/DevFeatures.sol @@ -15,10 +15,13 @@ library DevFeatures { bytes32(0x0000000000000000000000000000000000000000000000000000000000000001); /// @notice The feature that enables deployment of the CANNON_KONA fault dispute game. - /// This feature depends on the DEPLOY_V2_DISPUTE_GAMES feature + /// @custom:legacy + /// This feature is no longer used, but is kept here for legacy reasons. bytes32 public constant CANNON_KONA = bytes32(0x0000000000000000000000000000000000000000000000000000000000000010); /// @notice The feature that enables deployment of V2 dispute game contracts. + /// @custom:legacy + /// This feature is no longer used, but is kept here for legacy reasons. bytes32 public constant DEPLOY_V2_DISPUTE_GAMES = bytes32(0x0000000000000000000000000000000000000000000000000000000000000100); diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index 7a867a9e603..ce5457ead99 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -279,9 +279,7 @@ contract OPContractsManager_Upgrade_Harness is CommonTest, DisputeGames { // checks. Easier to just expect the error in this case. // We add the prefix of OVERRIDES-L1PAOMULTISIG,OVERRIDES-CHALLENGER because we use validationOverrides. if (opChainConfigs[0].cannonPrestate.raw() == bytes32(0)) { - if ( - opChainConfigs[0].cannonKonaPrestate.raw() == bytes32(0) && isDevFeatureEnabled(DevFeatures.CANNON_KONA) - ) { + if (opChainConfigs[0].cannonKonaPrestate.raw() == bytes32(0)) { vm.expectRevert( "OPContractsManagerStandardValidator: OVERRIDES-L1PAOMULTISIG,OVERRIDES-CHALLENGER,PDDG-40,PLDG-40,CKDG-10" ); @@ -290,37 +288,22 @@ contract OPContractsManager_Upgrade_Harness is CommonTest, DisputeGames { "OPContractsManagerStandardValidator: OVERRIDES-L1PAOMULTISIG,OVERRIDES-CHALLENGER,PDDG-40,PLDG-40" ); } - } else if ( - opChainConfigs[0].cannonKonaPrestate.raw() == bytes32(0) && isDevFeatureEnabled(DevFeatures.CANNON_KONA) - ) { + } else if (opChainConfigs[0].cannonKonaPrestate.raw() == bytes32(0)) { vm.expectRevert("OPContractsManagerStandardValidator: OVERRIDES-L1PAOMULTISIG,OVERRIDES-CHALLENGER,CKDG-10"); } // Run the StandardValidator checks. - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - validator.validateWithOverrides( - IOPContractsManagerStandardValidator.ValidationInputDev({ - sysCfg: opChainConfigs[0].systemConfigProxy, - cannonPrestate: opChainConfigs[0].cannonPrestate.raw(), - cannonKonaPrestate: opChainConfigs[0].cannonKonaPrestate.raw(), - l2ChainID: l2ChainId, - proposer: initialProposer - }), - false, - validationOverrides - ); - } else { - validator.validateWithOverrides( - IOPContractsManagerStandardValidator.ValidationInput({ - sysCfg: opChainConfigs[0].systemConfigProxy, - absolutePrestate: opChainConfigs[0].cannonPrestate.raw(), - l2ChainID: l2ChainId, - proposer: initialProposer - }), - false, - validationOverrides - ); - } + validator.validateWithOverrides( + IOPContractsManagerStandardValidator.ValidationInputDev({ + sysCfg: opChainConfigs[0].systemConfigProxy, + cannonPrestate: opChainConfigs[0].cannonPrestate.raw(), + cannonKonaPrestate: opChainConfigs[0].cannonKonaPrestate.raw(), + l2ChainID: l2ChainId, + proposer: initialProposer + }), + false, + validationOverrides + ); _runPostUpgradeSmokeTests(_opcm, opChainConfigs[0], initialChallenger, initialProposer); } @@ -341,8 +324,7 @@ contract OPContractsManager_Upgrade_Harness is CommonTest, DisputeGames { (, uint256 rootBlockNumber) = optimismPortal2.anchorStateRegistry().getAnchorRoot(); uint256 l2BlockNumber = rootBlockNumber + 1; - bool expectCannonKonaGameSet = - isDevFeatureEnabled(DevFeatures.CANNON_KONA) && _opChainConfig.cannonKonaPrestate.raw() != bytes32(0); + bool expectCannonKonaGameSet = _opChainConfig.cannonKonaPrestate.raw() != bytes32(0); // Deploy live games and ensure they're configured correctly GameType[] memory gameTypes = new GameType[](expectCannonKonaGameSet ? 3 : 2); @@ -628,28 +610,26 @@ contract OPContractsManager_AddGameType_Test is OPContractsManager_TestInit { // L2 chain ID call should not revert because this is not a Super game. assertEq(newPDG.l2ChainId(), chain1L2ChainId, "l2ChainId should be set correctly"); - if (isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - // Get the v2 implementation address from OPCM - IOPContractsManager.Implementations memory impls = opcm.implementations(); + // Get the v2 implementation address from OPCM + IOPContractsManager.Implementations memory impls = opcm.implementations(); - // Verify v2 implementation is registered in DisputeGameFactory - address registeredImpl = - address(chainDeployOutput1.disputeGameFactoryProxy.gameImpls(GameTypes.PERMISSIONED_CANNON)); + // Verify v2 implementation is registered in DisputeGameFactory + address registeredImpl = + address(chainDeployOutput1.disputeGameFactoryProxy.gameImpls(GameTypes.PERMISSIONED_CANNON)); - // Verify implementation address matches permissionedDisputeGameV2Impl - assertEq( - registeredImpl, - address(impls.permissionedDisputeGameV2Impl), - "DisputeGameFactory should have v2 PermissionedDisputeGame implementation registered" - ); + // Verify implementation address matches permissionedDisputeGameV2Impl + assertEq( + registeredImpl, + address(impls.permissionedDisputeGameV2Impl), + "DisputeGameFactory should have v2 PermissionedDisputeGame implementation registered" + ); - // Verify that the returned fault dispute game is the v2 implementation - assertEq( - address(output.faultDisputeGame), - address(impls.permissionedDisputeGameV2Impl), - "addGameType should return v2 PermissionedDisputeGame implementation" - ); - } + // Verify that the returned fault dispute game is the v2 implementation + assertEq( + address(output.faultDisputeGame), + address(impls.permissionedDisputeGameV2Impl), + "addGameType should return v2 PermissionedDisputeGame implementation" + ); } /// @notice Tests that we can add a FaultDisputeGame implementation with addGameType. @@ -675,24 +655,22 @@ contract OPContractsManager_AddGameType_Test is OPContractsManager_TestInit { address registeredImpl = address(chainDeployOutput1.disputeGameFactoryProxy.gameImpls(input.disputeGameType)); assertNotEq(registeredImpl, address(0), "Implementation should have been set"); - if (isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - // Get the v2 implementation address from OPCM - IOPContractsManager.Implementations memory impls = opcm.implementations(); + // Get the v2 implementation address from OPCM + IOPContractsManager.Implementations memory impls = opcm.implementations(); - // Verify implementation address matches permissionedDisputeGameV2Impl - assertEq( - registeredImpl, - address(impls.faultDisputeGameV2Impl), - "DisputeGameFactory should have v2 FaultDisputeGame implementation registered" - ); + // Verify implementation address matches permissionedDisputeGameV2Impl + assertEq( + registeredImpl, + address(impls.faultDisputeGameV2Impl), + "DisputeGameFactory should have v2 FaultDisputeGame implementation registered" + ); - // Verify that the returned fault dispute game is the v2 implementation - assertEq( - address(output.faultDisputeGame), - address(impls.faultDisputeGameV2Impl), - "addGameType should return v2 FaultDisputeGame implementation" - ); - } + // Verify that the returned fault dispute game is the v2 implementation + assertEq( + address(output.faultDisputeGame), + address(impls.faultDisputeGameV2Impl), + "addGameType should return v2 FaultDisputeGame implementation" + ); } /// @notice Tests that we can add a SuperPermissionedDisputeGame implementation with addGameType. @@ -784,30 +762,6 @@ contract OPContractsManager_AddGameType_Test is OPContractsManager_TestInit { assertFalse(success, "addGameType should have failed"); } - /// @notice Tests that addGameType will revert if the game type is cannon-kona and the dev feature is not enabled - function test_addGameType_cannonKonaGameTypeDisabled_reverts() public { - skipIfDevFeatureEnabled(DevFeatures.CANNON_KONA); - IOPContractsManager.AddGameInput memory input = newGameInputFactory(GameTypes.CANNON_KONA); - - // Run the addGameType call, should revert. - IOPContractsManager.AddGameInput[] memory inputs = new IOPContractsManager.AddGameInput[](1); - inputs[0] = input; - (bool success,) = address(opcm).delegatecall(abi.encodeCall(IOPContractsManager.addGameType, (inputs))); - assertFalse(success, "addGameType should have failed"); - } - - /// @notice Tests that addGameType will revert if the game type is cannon-kona and the dev feature is not enabled - function test_addGameType_superCannonKonaGameTypeDisabled_reverts() public { - skipIfDevFeatureEnabled(DevFeatures.CANNON_KONA); - IOPContractsManager.AddGameInput memory input = newGameInputFactory(GameTypes.SUPER_CANNON_KONA); - - // Run the addGameType call, should revert. - IOPContractsManager.AddGameInput[] memory inputs = new IOPContractsManager.AddGameInput[](1); - inputs[0] = input; - (bool success,) = address(opcm).delegatecall(abi.encodeCall(IOPContractsManager.addGameType, (inputs))); - assertFalse(success, "addGameType should have failed"); - } - function test_addGameType_reusedDelayedWETH_succeeds() public { IDelayedWETH delayedWETH = IDelayedWETH( DeployUtils.create1({ @@ -922,7 +876,6 @@ contract OPContractsManager_AddGameType_Test is OPContractsManager_TestInit { /// @notice Tests that addGameType will revert if the game type is cannon-kona and the dev feature is not enabled function test_addGameType_cannonKonaGameType_succeeds() public { - skipIfDevFeatureDisabled(DevFeatures.CANNON_KONA); // Create the input for the cannon-kona game type. IOPContractsManager.AddGameInput memory input = newGameInputFactory(GameTypes.CANNON_KONA); @@ -944,7 +897,6 @@ contract OPContractsManager_AddGameType_Test is OPContractsManager_TestInit { /// @notice Tests that addGameType will revert if the game type is cannon-kona and the dev feature is not enabled function test_addGameType_superCannonKonaGameType_succeeds() public { skipIfDevFeatureDisabled(DevFeatures.OPTIMISM_PORTAL_INTEROP); - skipIfDevFeatureDisabled(DevFeatures.CANNON_KONA); // Create the input for the cannon-kona game type. IOPContractsManager.AddGameInput memory input = newGameInputFactory(GameTypes.SUPER_CANNON_KONA); @@ -1273,7 +1225,6 @@ contract OPContractsManager_UpdatePrestate_Test is OPContractsManager_TestInit { /// @notice Tests that we can update the prestate for both CANNON and CANNON_KONA game types. function test_updatePrestate_bothGamesAndCannonKonaWithValidInput_succeeds() public { - skipIfDevFeatureDisabled(DevFeatures.CANNON_KONA); // Add a FaultDisputeGame implementation via addGameType. IOPContractsManager.AddGameInput memory input = newGameInputFactory(GameTypes.CANNON); addGameType(input); @@ -1292,7 +1243,6 @@ contract OPContractsManager_UpdatePrestate_Test is OPContractsManager_TestInit { } function test_updatePrestate_cannonKonaWithSuperGame_succeeds() public { - skipIfDevFeatureDisabled(DevFeatures.CANNON_KONA); skipIfDevFeatureDisabled(DevFeatures.OPTIMISM_PORTAL_INTEROP); _mockSuperPermissionedGame(); @@ -1352,7 +1302,6 @@ contract OPContractsManager_UpdatePrestate_Test is OPContractsManager_TestInit { /// @notice Tests that we can update the prestate when both the PermissionedDisputeGame and /// FaultDisputeGame exist, and the FaultDisputeGame is of type CANNON_KONA. function test_updatePrestate_pdgAndCannonKonaOnly_succeeds() public { - skipIfDevFeatureDisabled(DevFeatures.CANNON_KONA); IOPContractsManager.AddGameInput memory input = newGameInputFactory(GameTypes.CANNON_KONA); addGameType(input); @@ -1369,7 +1318,6 @@ contract OPContractsManager_UpdatePrestate_Test is OPContractsManager_TestInit { /// mixed game types (i.e. CANNON and SUPER_CANNON_KONA). function test_updatePrestate_cannonKonaMixedGameTypes_reverts() public { skipIfDevFeatureDisabled(DevFeatures.OPTIMISM_PORTAL_INTEROP); - skipIfDevFeatureDisabled(DevFeatures.CANNON_KONA); // Add a SuperFaultDisputeGame implementation via addGameType. IOPContractsManager.AddGameInput memory input = newGameInputFactory(GameTypes.SUPER_CANNON_KONA); @@ -1393,7 +1341,6 @@ contract OPContractsManager_UpdatePrestate_Test is OPContractsManager_TestInit { function test_updatePrestate_presetCannonKonaWhenOnlyCannonPrestateIsZeroAndCannonGameTypeDisabled_reverts() public { - skipIfDevFeatureDisabled(DevFeatures.CANNON_KONA); IOPContractsManager.AddGameInput memory input = newGameInputFactory(GameTypes.CANNON_KONA); addGameType(input); @@ -1411,7 +1358,6 @@ contract OPContractsManager_UpdatePrestate_Test is OPContractsManager_TestInit { /// @notice Tests that the updatePrestate function will revert if the provided prestate is the /// zero hash. function test_updatePrestate_whenCannonKonaPrestateIsZero_reverts() public { - skipIfDevFeatureDisabled(DevFeatures.CANNON_KONA); IOPContractsManager.AddGameInput memory input = newGameInputFactory(GameTypes.CANNON_KONA); addGameType(input); @@ -1498,31 +1444,19 @@ contract OPContractsManager_Upgrade_Test is OPContractsManager_Upgrade_Harness { runCurrentUpgrade(upgrader); // Get the absolute prestate after the upgrade - Claim pdgPrestateAfter; - Claim fdgPrestateAfter; - if (isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - pdgPrestateAfter = getDisputeGameV2AbsolutePrestate(GameTypes.PERMISSIONED_CANNON); - fdgPrestateAfter = getDisputeGameV2AbsolutePrestate(GameTypes.CANNON); - } else { - pdgPrestateAfter = IPermissionedDisputeGame( - address(disputeGameFactory.gameImpls(GameTypes.PERMISSIONED_CANNON)) - ).absolutePrestate(); - fdgPrestateAfter = - IFaultDisputeGame(address(disputeGameFactory.gameImpls(GameTypes.CANNON))).absolutePrestate(); - } + Claim pdgPrestateAfter = getDisputeGameV2AbsolutePrestate(GameTypes.PERMISSIONED_CANNON); + Claim fdgPrestateAfter = getDisputeGameV2AbsolutePrestate(GameTypes.CANNON); // Assert that the absolute prestate is the non-zero value we set. assertEq(pdgPrestateAfter.raw(), bytes32(uint256(1))); assertEq(fdgPrestateAfter.raw(), bytes32(uint256(1))); - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - LibGameArgs.GameArgs memory cannonArgs = LibGameArgs.decode(disputeGameFactory.gameArgs(GameTypes.CANNON)); - LibGameArgs.GameArgs memory cannonKonaArgs = - LibGameArgs.decode(disputeGameFactory.gameArgs(GameTypes.CANNON_KONA)); - assertEq(cannonKonaArgs.weth, cannonArgs.weth); - assertEq(cannonKonaArgs.anchorStateRegistry, cannonArgs.anchorStateRegistry); - assertEq(cannonKonaArgs.absolutePrestate, bytes32(uint256(2))); - } + LibGameArgs.GameArgs memory cannonArgs = LibGameArgs.decode(disputeGameFactory.gameArgs(GameTypes.CANNON)); + LibGameArgs.GameArgs memory cannonKonaArgs = + LibGameArgs.decode(disputeGameFactory.gameArgs(GameTypes.CANNON_KONA)); + assertEq(cannonKonaArgs.weth, cannonArgs.weth); + assertEq(cannonKonaArgs.anchorStateRegistry, cannonArgs.anchorStateRegistry); + assertEq(cannonKonaArgs.absolutePrestate, bytes32(uint256(2))); } /// @notice Tests that the old absolute prestate is used if the upgrade config does not set an @@ -1548,18 +1482,8 @@ contract OPContractsManager_Upgrade_Test is OPContractsManager_Upgrade_Harness { runCurrentUpgrade(upgrader); // Get the absolute prestate after the upgrade - Claim pdgPrestateAfter; - Claim fdgPrestateAfter; - if (isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - pdgPrestateAfter = getDisputeGameV2AbsolutePrestate(GameTypes.PERMISSIONED_CANNON); - fdgPrestateAfter = getDisputeGameV2AbsolutePrestate(GameTypes.CANNON); - } else { - pdgPrestateAfter = IPermissionedDisputeGame( - address(disputeGameFactory.gameImpls(GameTypes.PERMISSIONED_CANNON)) - ).absolutePrestate(); - fdgPrestateAfter = - IFaultDisputeGame(address(disputeGameFactory.gameImpls(GameTypes.CANNON))).absolutePrestate(); - } + Claim pdgPrestateAfter = getDisputeGameV2AbsolutePrestate(GameTypes.PERMISSIONED_CANNON); + Claim fdgPrestateAfter = getDisputeGameV2AbsolutePrestate(GameTypes.CANNON); // Assert that the absolute prestate is the same as before the upgrade. assertEq(pdgPrestateAfter.raw(), pdgPrestateBefore.raw()); @@ -1590,34 +1514,19 @@ contract OPContractsManager_Upgrade_Test is OPContractsManager_Upgrade_Harness { runCurrentUpgrade(upgrader); // Get the absolute prestate after the upgrade - Claim pdgPrestateAfter; - Claim fdgPrestateAfter; - if (isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - pdgPrestateAfter = getDisputeGameV2AbsolutePrestate(GameTypes.PERMISSIONED_CANNON); - fdgPrestateAfter = getDisputeGameV2AbsolutePrestate(GameTypes.CANNON); - } else { - pdgPrestateAfter = IPermissionedDisputeGame( - address(disputeGameFactory.gameImpls(GameTypes.PERMISSIONED_CANNON)) - ).absolutePrestate(); - fdgPrestateAfter = - IFaultDisputeGame(address(disputeGameFactory.gameImpls(GameTypes.CANNON))).absolutePrestate(); - } + Claim pdgPrestateAfter = getDisputeGameV2AbsolutePrestate(GameTypes.PERMISSIONED_CANNON); + Claim fdgPrestateAfter = getDisputeGameV2AbsolutePrestate(GameTypes.CANNON); // Assert that the absolute prestate is the same as before the upgrade. assertEq(pdgPrestateAfter.raw(), pdgPrestateBefore.raw()); assertEq(fdgPrestateAfter.raw(), fdgPrestateBefore.raw()); - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - LibGameArgs.GameArgs memory cannonArgs = LibGameArgs.decode(disputeGameFactory.gameArgs(GameTypes.CANNON)); - LibGameArgs.GameArgs memory cannonKonaArgs = - LibGameArgs.decode(disputeGameFactory.gameArgs(GameTypes.CANNON_KONA)); - assertEq(cannonKonaArgs.weth, cannonArgs.weth); - assertEq(cannonKonaArgs.anchorStateRegistry, cannonArgs.anchorStateRegistry); - assertEq(cannonKonaArgs.absolutePrestate, cannonKonaPrestate.raw()); - } else { - assertEq(address(0), address(disputeGameFactory.gameImpls(GameTypes.CANNON_KONA))); - assertEq(0, disputeGameFactory.gameArgs(GameTypes.CANNON_KONA).length); - } + LibGameArgs.GameArgs memory cannonArgs = LibGameArgs.decode(disputeGameFactory.gameArgs(GameTypes.CANNON)); + LibGameArgs.GameArgs memory cannonKonaArgs = + LibGameArgs.decode(disputeGameFactory.gameArgs(GameTypes.CANNON_KONA)); + assertEq(cannonKonaArgs.weth, cannonArgs.weth); + assertEq(cannonKonaArgs.anchorStateRegistry, cannonArgs.anchorStateRegistry); + assertEq(cannonKonaArgs.absolutePrestate, cannonKonaPrestate.raw()); } /// @notice Tests that the cannon absolute prestate is updated even if the cannon kona prestate is not specified @@ -1641,18 +1550,8 @@ contract OPContractsManager_Upgrade_Test is OPContractsManager_Upgrade_Harness { runCurrentUpgrade(upgrader); // Get the absolute prestate after the upgrade - Claim pdgPrestateAfter; - Claim fdgPrestateAfter; - if (isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - pdgPrestateAfter = getDisputeGameV2AbsolutePrestate(GameTypes.PERMISSIONED_CANNON); - fdgPrestateAfter = getDisputeGameV2AbsolutePrestate(GameTypes.CANNON); - } else { - pdgPrestateAfter = IPermissionedDisputeGame( - address(disputeGameFactory.gameImpls(GameTypes.PERMISSIONED_CANNON)) - ).absolutePrestate(); - fdgPrestateAfter = - IFaultDisputeGame(address(disputeGameFactory.gameImpls(GameTypes.CANNON))).absolutePrestate(); - } + Claim pdgPrestateAfter = getDisputeGameV2AbsolutePrestate(GameTypes.PERMISSIONED_CANNON); + Claim fdgPrestateAfter = getDisputeGameV2AbsolutePrestate(GameTypes.CANNON); // Assert that the absolute prestate is the non-zero value we set. assertEq(pdgPrestateAfter.raw(), bytes32(uint256(1))); @@ -1872,11 +1771,9 @@ contract OPContractsManager_Migrate_Test is OPContractsManager_TestInit { _assertGameIsEmpty(_disputeGameFactory, GameTypes.SUPER_CANNON, "SUPER_CANNON"); _assertGameIsEmpty(_disputeGameFactory, GameTypes.PERMISSIONED_CANNON, "PERMISSIONED_CANNON"); _assertGameIsEmpty(_disputeGameFactory, GameTypes.SUPER_PERMISSIONED_CANNON, "SUPER_PERMISSIONED_CANNON"); - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - // Only explicitly zeroed out if feature is enabled. Otherwise left unchanged (which may still be 0). - _assertGameIsEmpty(_disputeGameFactory, GameTypes.CANNON_KONA, "CANNON_KONA"); - _assertGameIsEmpty(_disputeGameFactory, GameTypes.SUPER_CANNON_KONA, "SUPER_CANNON_KONA"); - } + // Only explicitly zeroed out if feature is enabled. Otherwise left unchanged (which may still be 0). + _assertGameIsEmpty(_disputeGameFactory, GameTypes.CANNON_KONA, "CANNON_KONA"); + _assertGameIsEmpty(_disputeGameFactory, GameTypes.SUPER_CANNON_KONA, "SUPER_CANNON_KONA"); } function _assertGameIsEmpty(IDisputeGameFactory _dgf, GameType _gameType, string memory _label) internal view { @@ -1957,14 +1854,14 @@ contract OPContractsManager_Migrate_Test is OPContractsManager_TestInit { function _getPostMigrateExpectedGameTypes(IOPContractsManagerInteropMigrator.MigrateInput memory _input) internal - view + pure returns (GameType[] memory gameTypes_) { uint256 gameCount = 1; bytes32 cannonKonaPrestate = _input.opChainConfigs[0].cannonKonaPrestate.raw(); if (_input.usePermissionlessGame) { gameCount += 1; - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA) && cannonKonaPrestate != bytes32(0)) { + if (cannonKonaPrestate != bytes32(0)) { gameCount += 1; } } @@ -1973,7 +1870,7 @@ contract OPContractsManager_Migrate_Test is OPContractsManager_TestInit { gameTypes_[0] = GameTypes.SUPER_PERMISSIONED_CANNON; if (_input.usePermissionlessGame) { gameTypes_[1] = GameTypes.SUPER_CANNON; - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA) && cannonKonaPrestate != bytes32(0)) { + if (cannonKonaPrestate != bytes32(0)) { gameTypes_[2] = GameTypes.SUPER_CANNON_KONA; } } @@ -1997,26 +1894,16 @@ contract OPContractsManager_Migrate_Test is OPContractsManager_TestInit { input.gameParameters.initBond, "Super Permissioned Cannon init bond mismatch" ); - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - assertEq( - dgf.initBonds(GameTypes.SUPER_CANNON_KONA), - input.gameParameters.initBond, - "Super CannonKona init bond mismatch" - ); - } else { - assertEq( - dgf.initBonds(GameTypes.SUPER_CANNON_KONA), uint256(0), "Super CannonKona init bond should be zero" - ); - } + assertEq( + dgf.initBonds(GameTypes.SUPER_CANNON_KONA), + input.gameParameters.initBond, + "Super CannonKona init bond mismatch" + ); // Check game configuration _validateSuperGameImplParams(input, dgf, GameTypes.SUPER_PERMISSIONED_CANNON, "SUPER_PERMISSIONED_CANNON"); _validateSuperGameImplParams(input, dgf, GameTypes.SUPER_CANNON, "SUPER_CANNON"); - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - _validateSuperGameImplParams(input, dgf, GameTypes.SUPER_CANNON_KONA, "SUPER_CANNON_KONA"); - } else { - _assertGameIsEmpty(dgf, GameTypes.SUPER_CANNON_KONA, "SUPER_CANNON_KONA"); - } + _validateSuperGameImplParams(input, dgf, GameTypes.SUPER_CANNON_KONA, "SUPER_CANNON_KONA"); _runPostMigrateSmokeTests(input); } @@ -2248,16 +2135,10 @@ contract OPContractsManager_Migrate_Test is OPContractsManager_TestInit { input.opChainConfigs[1].cannonKonaPrestate = cannonKonaPrestate2; // Execute the migration. - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - // We should revert if there is a mismatch and cannonaKona is enabled - _doMigration( - input, - OPContractsManagerInteropMigrator.OPContractsManagerInteropMigrator_AbsolutePrestateMismatch.selector - ); - } else { - // Otherwise, migration should run without reverting - _doMigration(input); - } + // We should revert if there is a mismatch and cannonaKona is enabled + _doMigration( + input, OPContractsManagerInteropMigrator.OPContractsManagerInteropMigrator_AbsolutePrestateMismatch.selector + ); } /// @notice Tests that the migration function reverts when the SuperchainConfig addresses are @@ -2284,7 +2165,6 @@ contract OPContractsManager_Migrate_Test is OPContractsManager_TestInit { } function test_migrate_zerosOutCannonKonaGameTypes_succeeds() public { - skipIfDevFeatureDisabled(DevFeatures.CANNON_KONA); IOPContractsManagerInteropMigrator.MigrateInput memory input = _getDefaultInput(); // Grab the existing DisputeGameFactory for each chain. @@ -2374,29 +2254,21 @@ contract OPContractsManager_Deploy_Test is DeployOPChain_TestBase, DisputeGames /// @notice Test that deploy sets the permissioned dispute game implementation function test_deployPermissioned_succeeds() public { - bool isV2 = isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES); - // Sanity-check setup is consistent with devFeatures flag IOPContractsManager.Implementations memory impls = opcm.implementations(); address pdgImpl = address(impls.permissionedDisputeGameV2Impl); address fdgImpl = address(impls.faultDisputeGameV2Impl); - if (isV2) { - assertFalse(pdgImpl == address(0), "PDG implementation address should be non-zero"); - assertFalse(fdgImpl == address(0), "FDG implementation address should be non-zero"); - } else { - assertTrue(pdgImpl == address(0), "PDG implementation address should be zero"); - assertTrue(fdgImpl == address(0), "FDG implementation address should be zero"); - } + assertFalse(pdgImpl == address(0), "PDG implementation address should be non-zero"); + assertFalse(fdgImpl == address(0), "FDG implementation address should be non-zero"); // Run OPCM.deploy IOPContractsManager.DeployInput memory opcmInput = toOPCMDeployInput(deployOPChainInput); IOPContractsManager.DeployOutput memory opcmOutput = opcm.deploy(opcmInput); // Verify that the DisputeGameFactory has registered an implementation for the PERMISSIONED_CANNON game type - address expectedPDGAddress = isV2 ? pdgImpl : address(opcmOutput.permissionedDisputeGame); address actualPDGAddress = address(opcmOutput.disputeGameFactoryProxy.gameImpls(GameTypes.PERMISSIONED_CANNON)); assertNotEq(actualPDGAddress, address(0), "DisputeGameFactory should have a registered PERMISSIONED_CANNON"); - assertEq(actualPDGAddress, address(expectedPDGAddress), "PDG address should match"); + assertEq(actualPDGAddress, pdgImpl, "PDG address should match"); // Create a game proxy to test immutable fields Claim claim = Claim.wrap(bytes32(uint256(9876))); diff --git a/packages/contracts-bedrock/test/L1/OPContractsManagerStandardValidator.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManagerStandardValidator.t.sol index be961a98f37..9ca73cd450f 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManagerStandardValidator.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManagerStandardValidator.t.sol @@ -84,6 +84,33 @@ contract BadDisputeGameFactoryReturner { } } +/// @title BadVersionReturner +contract BadVersionReturner { + /// @notice Address of the OPContractsManagerStandardValidator instance. + IOPContractsManagerStandardValidator public immutable validator; + + /// @notice Address of the versioned contract. + ISemver public immutable versioned; + + /// @notice The mock semver + string public mockVersion; + + constructor(IOPContractsManagerStandardValidator _validator, ISemver _versioned, string memory _mockVersion) { + validator = _validator; + versioned = _versioned; + mockVersion = _mockVersion; + } + + /// @notice Returns the real or fake semver + function version() external view returns (string memory) { + if (msg.sender == address(validator)) { + return mockVersion; + } else { + return versioned.version(); + } + } +} + /// @title OPContractsManagerStandardValidator_TestInit /// @notice Base contract for `OPContractsManagerStandardValidator` tests, handles common setup. abstract contract OPContractsManagerStandardValidator_TestInit is CommonTest, DisputeGames { @@ -203,9 +230,7 @@ abstract contract OPContractsManagerStandardValidator_TestInit is CommonTest, Di fdgImpl = output.faultDisputeGame; // Deploy cannon-kona - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - addGameType(GameTypes.CANNON_KONA, cannonKonaPrestate); - } + addGameType(GameTypes.CANNON_KONA, cannonKonaPrestate); } else { // Get the ProxyAdmin owner. address owner = proxyAdmin.owner(); @@ -232,7 +257,7 @@ abstract contract OPContractsManagerStandardValidator_TestInit is CommonTest, Di ) }); disputeGameConfigs[2] = IOPContractsManagerV2.DisputeGameConfig({ - enabled: isDevFeatureEnabled(DevFeatures.CANNON_KONA), + enabled: true, initBond: disputeGameFactory.initBonds(GameTypes.CANNON_KONA), gameType: GameTypes.CANNON_KONA, gameArgs: abi.encode( @@ -266,28 +291,16 @@ abstract contract OPContractsManagerStandardValidator_TestInit is CommonTest, Di /// @param _allowFailure Whether to allow failure. /// @return The error message(s) from the validate function. function _validate(bool _allowFailure) internal view returns (string memory) { - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - return standardValidator.validate( - IOPContractsManagerStandardValidator.ValidationInputDev({ - sysCfg: systemConfig, - cannonPrestate: cannonPrestate.raw(), - cannonKonaPrestate: cannonKonaPrestate.raw(), - l2ChainID: l2ChainId, - proposer: proposer - }), - _allowFailure - ); - } else { - return standardValidator.validate( - IOPContractsManagerStandardValidator.ValidationInput({ - sysCfg: systemConfig, - absolutePrestate: cannonPrestate.raw(), - l2ChainID: l2ChainId, - proposer: proposer - }), - _allowFailure - ); - } + return standardValidator.validate( + IOPContractsManagerStandardValidator.ValidationInputDev({ + sysCfg: systemConfig, + cannonPrestate: cannonPrestate.raw(), + cannonKonaPrestate: cannonKonaPrestate.raw(), + l2ChainID: l2ChainId, + proposer: proposer + }), + _allowFailure + ); } /// @notice Runs the OPContractsManagerStandardValidator.validateWithOverrides function. @@ -301,30 +314,17 @@ abstract contract OPContractsManagerStandardValidator_TestInit is CommonTest, Di view returns (string memory) { - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - return standardValidator.validateWithOverrides( - IOPContractsManagerStandardValidator.ValidationInputDev({ - sysCfg: systemConfig, - cannonPrestate: cannonPrestate.raw(), - cannonKonaPrestate: cannonKonaPrestate.raw(), - l2ChainID: l2ChainId, - proposer: proposer - }), - _allowFailure, - _overrides - ); - } else { - return standardValidator.validateWithOverrides( - IOPContractsManagerStandardValidator.ValidationInput({ - sysCfg: systemConfig, - absolutePrestate: cannonPrestate.raw(), - l2ChainID: l2ChainId, - proposer: proposer - }), - _allowFailure, - _overrides - ); - } + return standardValidator.validateWithOverrides( + IOPContractsManagerStandardValidator.ValidationInputDev({ + sysCfg: systemConfig, + cannonPrestate: cannonPrestate.raw(), + cannonKonaPrestate: cannonKonaPrestate.raw(), + l2ChainID: l2ChainId, + proposer: proposer + }), + _allowFailure, + _overrides + ); } function _defaultValidationOverrides() @@ -421,17 +421,10 @@ contract OPContractsManagerStandardValidator_GeneralOverride_Test is OPContracts IOPContractsManagerStandardValidator.ValidationOverrides memory overrides = _defaultValidationOverrides(); overrides.l1PAOMultisig = address(0xace); overrides.challenger = address(0xbad); - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - assertEq( - "OVERRIDES-L1PAOMULTISIG,OVERRIDES-CHALLENGER,PROXYA-10,DF-30,PDDG-DWETH-30,PDDG-130,PLDG-DWETH-30,CKDG-DWETH-30", - _validateWithOverrides(true, overrides) - ); - } else { - assertEq( - "OVERRIDES-L1PAOMULTISIG,OVERRIDES-CHALLENGER,PROXYA-10,DF-30,PDDG-DWETH-30,PDDG-130,PLDG-DWETH-30", - _validateWithOverrides(true, overrides) - ); - } + assertEq( + "OVERRIDES-L1PAOMULTISIG,OVERRIDES-CHALLENGER,PROXYA-10,DF-30,PDDG-DWETH-30,PDDG-130,PLDG-DWETH-30,CKDG-DWETH-30", + _validateWithOverrides(true, overrides) + ); } /// @notice Tests that the validate function (with the L1PAOMultisig and Challenger overridden) @@ -462,19 +455,11 @@ contract OPContractsManagerStandardValidator_GeneralOverride_Test is OPContracts IOPContractsManagerStandardValidator.ValidationOverrides memory overrides = IOPContractsManagerStandardValidator .ValidationOverrides({ l1PAOMultisig: address(0xbad), challenger: address(0xc0ffee) }); - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - vm.expectRevert( - bytes( - "OPContractsManagerStandardValidator: OVERRIDES-L1PAOMULTISIG,OVERRIDES-CHALLENGER,PROXYA-10,DF-30,PDDG-DWETH-30,PDDG-130,PLDG-DWETH-30,CKDG-DWETH-30" - ) - ); - } else { - vm.expectRevert( - bytes( - "OPContractsManagerStandardValidator: OVERRIDES-L1PAOMULTISIG,OVERRIDES-CHALLENGER,PROXYA-10,DF-30,PDDG-DWETH-30,PDDG-130,PLDG-DWETH-30" - ) - ); - } + vm.expectRevert( + bytes( + "OPContractsManagerStandardValidator: OVERRIDES-L1PAOMULTISIG,OVERRIDES-CHALLENGER,PROXYA-10,DF-30,PDDG-DWETH-30,PDDG-130,PLDG-DWETH-30,CKDG-DWETH-30" + ) + ); _validateWithOverrides(false, overrides); } @@ -505,11 +490,7 @@ contract OPContractsManagerStandardValidator_ProxyAdmin_Test is OPContractsManag vm.mockCall( address(delayedWeth), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(address(0xbad)) ); - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - assertEq("PROXYA-10,PDDG-DWETH-30,PLDG-DWETH-30,CKDG-DWETH-30", _validate(true)); - } else { - assertEq("PROXYA-10,PDDG-DWETH-30,PLDG-DWETH-30", _validate(true)); - } + assertEq("PROXYA-10,PDDG-DWETH-30,PLDG-DWETH-30,CKDG-DWETH-30", _validate(true)); } /// @notice Tests that the validate function successfully returns the right overrides error @@ -532,17 +513,10 @@ contract OPContractsManagerStandardValidator_ProxyAdmin_Test is OPContractsManag function test_validateOverrideL1PAOMultisig_invalidProxyAdminOwner_succeeds() public view { IOPContractsManagerStandardValidator.ValidationOverrides memory overrides = _defaultValidationOverrides(); overrides.l1PAOMultisig = address(0xbad); - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - assertEq( - "OVERRIDES-L1PAOMULTISIG,PROXYA-10,DF-30,PDDG-DWETH-30,PLDG-DWETH-30,CKDG-DWETH-30", - _validateWithOverrides(true, overrides) - ); - } else { - assertEq( - "OVERRIDES-L1PAOMULTISIG,PROXYA-10,DF-30,PDDG-DWETH-30,PLDG-DWETH-30", - _validateWithOverrides(true, overrides) - ); - } + assertEq( + "OVERRIDES-L1PAOMULTISIG,PROXYA-10,DF-30,PDDG-DWETH-30,PLDG-DWETH-30,CKDG-DWETH-30", + _validateWithOverrides(true, overrides) + ); } } @@ -1043,20 +1017,13 @@ contract OPContractsManagerStandardValidator_PermissionedDisputeGame_Test is /// @notice Tests that the validate function successfully returns the right error when the /// PermissionedDisputeGame version is invalid. function test_validate_permissionedDisputeGameInvalidVersion_succeeds() public { - vm.mockCall(address(pdgImpl), abi.encodeCall(ISemver.version, ()), abi.encode("0.0.0")); + BadVersionReturner bad = new BadVersionReturner(standardValidator, ISemver(address(pdgImpl)), "0.0.0"); + bytes32 slot = + bytes32(ForgeArtifacts.getSlot("OPContractsManagerStandardValidator", "permissionedDisputeGameImpl").slot); + vm.store(address(standardValidator), slot, bytes32(uint256(uint160(address(bad))))); assertEq("PDDG-20", _validate(true)); } - /// @notice Tests that the validate function successfully returns the right error when the - /// PermissionedDisputeGame game type is invalid. - function test_validate_permissionedDisputeGameInvalidGameType_succeeds() public { - // For v2 game contracts, we don't store the game type anywhere other than the DGF gameImpls and gameArgs maps - // So, there's not really an obvious way to return an invalid gameType - skipIfDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES); - vm.mockCall(address(pdgImpl), abi.encodeCall(IDisputeGame.gameType, ()), abi.encode(GameTypes.CANNON)); - assertEq("PDDG-30", _validate(true)); - } - /// @notice Tests that the validate function successfully returns the right error when the /// PermissionedDisputeGame game args are invalid. function test_validate_permissionedDisputeGameInvalidGameArgs_succeeds() public { @@ -1140,11 +1107,7 @@ contract OPContractsManagerStandardValidator_PermissionedDisputeGame_Test is /// PermissionedDisputeGame VM's state version is invalid. function test_validate_permissionedDisputeGameInvalidVMStateVersion_succeeds() public { vm.mockCall(address(mips), abi.encodeCall(IMIPS64.stateVersion, ()), abi.encode(6)); - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - assertEq("PDDG-VM-30,PLDG-VM-30,CKDG-VM-30", _validate(true)); - } else { - assertEq("PDDG-VM-30,PLDG-VM-30", _validate(true)); - } + assertEq("PDDG-VM-30,PLDG-VM-30,CKDG-VM-30", _validate(true)); } /// @notice Tests that the validate function successfully returns the right error when the @@ -1206,11 +1169,7 @@ contract OPContractsManagerStandardValidator_PermissionedDisputeGame_Test is abi.encodeCall(IAnchorStateRegistry.getAnchorRoot, ()), abi.encode(bytes32(0), 1) ); - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - assertEq("PDDG-120,PLDG-120,CKDG-120", _validate(true)); - } else { - assertEq("PDDG-120,PLDG-120", _validate(true)); - } + assertEq("PDDG-120,PLDG-120,CKDG-120", _validate(true)); } /// @notice Tests that the validate function successfully returns the right error when the @@ -1259,11 +1218,7 @@ contract OPContractsManagerStandardValidator_AnchorStateRegistry_Test is /// AnchorStateRegistry version is invalid. function test_validate_anchorStateRegistryInvalidVersion_succeeds() public { vm.mockCall(address(anchorStateRegistry), abi.encodeCall(ISemver.version, ()), abi.encode("0.0.1")); - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - assertEq("PDDG-ANCHORP-10,PLDG-ANCHORP-10,CKDG-ANCHORP-10", _validate(true)); - } else { - assertEq("PDDG-ANCHORP-10,PLDG-ANCHORP-10", _validate(true)); - } + assertEq("PDDG-ANCHORP-10,PLDG-ANCHORP-10,CKDG-ANCHORP-10", _validate(true)); } /// @notice Tests that the validate function successfully returns the right error when the @@ -1274,11 +1229,7 @@ contract OPContractsManagerStandardValidator_AnchorStateRegistry_Test is abi.encodeCall(IProxyAdmin.getProxyImplementation, (address(anchorStateRegistry))), abi.encode(address(0xbad)) ); - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - assertEq("PDDG-ANCHORP-20,PLDG-ANCHORP-20,CKDG-ANCHORP-20", _validate(true)); - } else { - assertEq("PDDG-ANCHORP-20,PLDG-ANCHORP-20", _validate(true)); - } + assertEq("PDDG-ANCHORP-20,PLDG-ANCHORP-20,CKDG-ANCHORP-20", _validate(true)); } /// @notice Tests that the validate function successfully returns the right error when the @@ -1289,11 +1240,7 @@ contract OPContractsManagerStandardValidator_AnchorStateRegistry_Test is address(badDisputeGameFactoryReturner), abi.encodeCall(IAnchorStateRegistry.disputeGameFactory, ()) ); - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - assertEq("PDDG-ANCHORP-30,PLDG-ANCHORP-30,CKDG-ANCHORP-30", _validate(true)); - } else { - assertEq("PDDG-ANCHORP-30,PLDG-ANCHORP-30", _validate(true)); - } + assertEq("PDDG-ANCHORP-30,PLDG-ANCHORP-30,CKDG-ANCHORP-30", _validate(true)); } /// @notice Tests that the validate function successfully returns the right error when the @@ -1304,11 +1251,7 @@ contract OPContractsManagerStandardValidator_AnchorStateRegistry_Test is abi.encodeCall(IAnchorStateRegistry.systemConfig, ()), abi.encode(address(0xbad)) ); - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - assertEq("PDDG-ANCHORP-40,PLDG-ANCHORP-40,CKDG-ANCHORP-40", _validate(true)); - } else { - assertEq("PDDG-ANCHORP-40,PLDG-ANCHORP-40", _validate(true)); - } + assertEq("PDDG-ANCHORP-40,PLDG-ANCHORP-40,CKDG-ANCHORP-40", _validate(true)); } /// @notice Tests that the validate function successfully returns the right error when the @@ -1319,11 +1262,7 @@ contract OPContractsManagerStandardValidator_AnchorStateRegistry_Test is abi.encodeCall(IProxyAdminOwnedBase.proxyAdmin, ()), abi.encode(address(0xbad)) ); - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - assertEq("PDDG-ANCHORP-50,PLDG-ANCHORP-50,CKDG-ANCHORP-50", _validate(true)); - } else { - assertEq("PDDG-ANCHORP-50,PLDG-ANCHORP-50", _validate(true)); - } + assertEq("PDDG-ANCHORP-50,PLDG-ANCHORP-50,CKDG-ANCHORP-50", _validate(true)); } /// @notice Tests that the validate function successfully returns the right error when the @@ -1332,11 +1271,7 @@ contract OPContractsManagerStandardValidator_AnchorStateRegistry_Test is vm.mockCall( address(anchorStateRegistry), abi.encodeCall(IAnchorStateRegistry.retirementTimestamp, ()), abi.encode(0) ); - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - assertEq("PDDG-ANCHORP-60,PLDG-ANCHORP-60,CKDG-ANCHORP-60", _validate(true)); - } else { - assertEq("PDDG-ANCHORP-60,PLDG-ANCHORP-60", _validate(true)); - } + assertEq("PDDG-ANCHORP-60,PLDG-ANCHORP-60,CKDG-ANCHORP-60", _validate(true)); } } @@ -1357,11 +1292,7 @@ contract OPContractsManagerStandardValidator_DelayedWETH_Test is OPContractsMana if (isForkTest()) { assertEq("PDDG-DWETH-10", _validate(true)); } else { - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - assertEq("PLDG-DWETH-10,CKDG-DWETH-10", _validate(true)); - } else { - assertEq("PLDG-DWETH-10", _validate(true)); - } + assertEq("PLDG-DWETH-10,CKDG-DWETH-10", _validate(true)); } } } @@ -1381,11 +1312,7 @@ contract OPContractsManagerStandardValidator_DelayedWETH_Test is OPContractsMana if (isForkTest()) { assertEq("PDDG-DWETH-20", _validate(true)); } else { - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - assertEq("PLDG-DWETH-20,CKDG-DWETH-20", _validate(true)); - } else { - assertEq("PLDG-DWETH-20", _validate(true)); - } + assertEq("PLDG-DWETH-20,CKDG-DWETH-20", _validate(true)); } } } @@ -1403,11 +1330,7 @@ contract OPContractsManagerStandardValidator_DelayedWETH_Test is OPContractsMana if (isForkTest()) { assertEq("PDDG-DWETH-30", _validate(true)); } else { - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - assertEq("PLDG-DWETH-30,CKDG-DWETH-30", _validate(true)); - } else { - assertEq("PLDG-DWETH-30", _validate(true)); - } + assertEq("PLDG-DWETH-30,CKDG-DWETH-30", _validate(true)); } } } @@ -1423,11 +1346,7 @@ contract OPContractsManagerStandardValidator_DelayedWETH_Test is OPContractsMana if (isForkTest()) { assertEq("PDDG-DWETH-40", _validate(true)); } else { - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - assertEq("PLDG-DWETH-40,CKDG-DWETH-40", _validate(true)); - } else { - assertEq("PLDG-DWETH-40", _validate(true)); - } + assertEq("PLDG-DWETH-40,CKDG-DWETH-40", _validate(true)); } } } @@ -1443,11 +1362,7 @@ contract OPContractsManagerStandardValidator_DelayedWETH_Test is OPContractsMana if (isForkTest()) { assertEq("PDDG-DWETH-50", _validate(true)); } else { - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - assertEq("PLDG-DWETH-50,CKDG-DWETH-50", _validate(true)); - } else { - assertEq("PLDG-DWETH-50", _validate(true)); - } + assertEq("PLDG-DWETH-50,CKDG-DWETH-50", _validate(true)); } } } @@ -1465,11 +1380,7 @@ contract OPContractsManagerStandardValidator_DelayedWETH_Test is OPContractsMana if (isForkTest()) { assertEq("PDDG-DWETH-60", _validate(true)); } else { - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - assertEq("PLDG-DWETH-60,CKDG-DWETH-60", _validate(true)); - } else { - assertEq("PLDG-DWETH-60", _validate(true)); - } + assertEq("PLDG-DWETH-60,CKDG-DWETH-60", _validate(true)); } } } @@ -1482,33 +1393,21 @@ contract OPContractsManagerStandardValidator_PreimageOracle_Test is OPContractsM /// PreimageOracle version is invalid. function test_validate_preimageOracleInvalidVersion_succeeds() public { vm.mockCall(address(preimageOracle), abi.encodeCall(ISemver.version, ()), abi.encode("0.0.1")); - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - assertEq("PDDG-PIMGO-10,PLDG-PIMGO-10,CKDG-PIMGO-10", _validate(true)); - } else { - assertEq("PDDG-PIMGO-10,PLDG-PIMGO-10", _validate(true)); - } + assertEq("PDDG-PIMGO-10,PLDG-PIMGO-10,CKDG-PIMGO-10", _validate(true)); } /// @notice Tests that the validate function successfully returns the right error when the /// PreimageOracle challengePeriod is invalid. function test_validate_preimageOracleInvalidChallengePeriod_succeeds() public { vm.mockCall(address(preimageOracle), abi.encodeCall(IPreimageOracle.challengePeriod, ()), abi.encode(1000)); - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - assertEq("PDDG-PIMGO-20,PLDG-PIMGO-20,CKDG-PIMGO-20", _validate(true)); - } else { - assertEq("PDDG-PIMGO-20,PLDG-PIMGO-20", _validate(true)); - } + assertEq("PDDG-PIMGO-20,PLDG-PIMGO-20,CKDG-PIMGO-20", _validate(true)); } /// @notice Tests that the validate function successfully returns the right error when the /// PreimageOracle minProposalSize is invalid. function test_validate_preimageOracleInvalidMinProposalSize_succeeds() public { vm.mockCall(address(preimageOracle), abi.encodeCall(IPreimageOracle.minProposalSize, ()), abi.encode(1000)); - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - assertEq("PDDG-PIMGO-30,PLDG-PIMGO-30,CKDG-PIMGO-30", _validate(true)); - } else { - assertEq("PDDG-PIMGO-30,PLDG-PIMGO-30", _validate(true)); - } + assertEq("PDDG-PIMGO-30,PLDG-PIMGO-30,CKDG-PIMGO-30", _validate(true)); } } @@ -1529,7 +1428,6 @@ contract OPContractsManagerStandardValidator_FaultDisputeGame_Test is OPContract /// @notice Tests that the validate function successfully returns the right error when the /// FaultDisputeGame (permissionless) CannonKona implementation is null. function test_validate_faultDisputeGameNullCannonKonaImplementation_succeeds() public { - skipIfDevFeatureDisabled(DevFeatures.CANNON_KONA); vm.mockCall( address(disputeGameFactory), abi.encodeCall(IDisputeGameFactory.gameImpls, (GameTypes.CANNON_KONA)), @@ -1541,24 +1439,11 @@ contract OPContractsManagerStandardValidator_FaultDisputeGame_Test is OPContract /// @notice Tests that the validate function successfully returns the right error when the /// FaultDisputeGame (permissionless) version is invalid. function test_validate_faultDisputeGameInvalidVersion_succeeds() public { - vm.mockCall(address(fdgImpl), abi.encodeCall(ISemver.version, ()), abi.encode("0.0.0")); - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - assertEq("PLDG-20,CKDG-20", _validate(true)); - } else { - assertEq("PLDG-20", _validate(true)); - } - } - - /// @notice Tests that the validate function successfully returns the right error when the - /// FaultDisputeGame (permissionless) game type is invalid. - function test_validate_faultDisputeGameInvalidGameType_succeeds() public { - // For v2 game contracts, we don't store the game type anywhere other than the DGF gameImpls and gameArgs maps - // So, there's not really an obvious way to return an invalid gameType - skipIfDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES); - vm.mockCall( - address(fdgImpl), abi.encodeCall(IDisputeGame.gameType, ()), abi.encode(GameTypes.PERMISSIONED_CANNON) - ); - assertEq("PLDG-30", _validate(true)); + BadVersionReturner bad = new BadVersionReturner(standardValidator, ISemver(address(pdgImpl)), "0.0.0"); + bytes32 slot = + bytes32(ForgeArtifacts.getSlot("OPContractsManagerStandardValidator", "faultDisputeGameImpl").slot); + vm.store(address(standardValidator), slot, bytes32(uint256(uint160(address(bad))))); + assertEq("PLDG-20,CKDG-20", _validate(true)); } /// @notice Tests that the validate function successfully returns the right error when the @@ -1574,7 +1459,6 @@ contract OPContractsManagerStandardValidator_FaultDisputeGame_Test is OPContract /// @notice Tests that the validate function successfully returns the right error when the /// FaultDisputeGame (permissionless) CannonKona game args are invalid. function test_validate_faultDisputeGameInvalidCannonKonaGameArgs_succeeds() public { - skipIfDevFeatureDisabled(DevFeatures.CANNON_KONA); bytes memory invalidGameArgs = hex"123456"; GameType gameType = GameTypes.CANNON_KONA; vm.mockCall(address(dgf), abi.encodeCall(IDisputeGameFactory.gameArgs, (gameType)), abi.encode(invalidGameArgs)); @@ -1594,7 +1478,6 @@ contract OPContractsManagerStandardValidator_FaultDisputeGame_Test is OPContract /// @notice Tests that the validate function successfully returns the right error when the /// FaultDisputeGame (permissionless) CannonKona absolute prestate is invalid. function test_validate_faultDisputeGameInvalidCannonKonaAbsolutePrestate_succeeds() public { - skipIfDevFeatureDisabled(DevFeatures.CANNON_KONA); bytes32 badPrestate = cannonPrestate.raw(); // Use the wrong prestate mockGameImplPrestate(dgf, GameTypes.CANNON_KONA, badPrestate); @@ -1615,7 +1498,6 @@ contract OPContractsManagerStandardValidator_FaultDisputeGame_Test is OPContract /// @notice Tests that the validate function successfully returns the right error when the /// FaultDisputeGame (permissionless) CannonKona VM address is invalid. function test_validate_faultDisputeGameInvalidCannonKonaVM_succeeds() public { - skipIfDevFeatureDisabled(DevFeatures.CANNON_KONA); address badVM = address(0xbad); mockGameImplVM(dgf, GameTypes.CANNON_KONA, badVM); vm.mockCall(badVM, abi.encodeCall(ISemver.version, ()), abi.encode("0.0.0")); @@ -1634,7 +1516,6 @@ contract OPContractsManagerStandardValidator_FaultDisputeGame_Test is OPContract /// @notice Tests that the validate function successfully returns the right error when the /// FaultDisputeGame (permissionless) CannonKona ASR address is invalid. function test_validate_faultDisputeGameInvalidCannonKonaASR_succeeds() public { - skipIfDevFeatureDisabled(DevFeatures.CANNON_KONA); _mockInvalidASR(GameTypes.CANNON_KONA); assertEq("CKDG-ANCHORP-10,CKDG-ANCHORP-20", _validate(true)); } @@ -1669,7 +1550,6 @@ contract OPContractsManagerStandardValidator_FaultDisputeGame_Test is OPContract /// @notice Tests that the validate function successfully returns the right error when the /// FaultDisputeGame (permissionless) CannonKona Weth address is invalid. function test_validate_faultDisputeGameInvalidCannonKonaWeth_succeeds() public { - skipIfDevFeatureDisabled(DevFeatures.CANNON_KONA); _mockInvalidWeth(GameTypes.CANNON_KONA); assertEq("CKDG-DWETH-10,CKDG-DWETH-20", _validate(true)); } @@ -1699,11 +1579,7 @@ contract OPContractsManagerStandardValidator_FaultDisputeGame_Test is OPContract /// FaultDisputeGame (permissionless) VM's state version is invalid. function test_validate_faultDisputeGameInvalidVMStateVersion_succeeds() public { vm.mockCall(address(mips), abi.encodeCall(IMIPS64.stateVersion, ()), abi.encode(6)); - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - assertEq("PDDG-VM-30,PLDG-VM-30,CKDG-VM-30", _validate(true)); - } else { - assertEq("PDDG-VM-30,PLDG-VM-30", _validate(true)); - } + assertEq("PDDG-VM-30,PLDG-VM-30,CKDG-VM-30", _validate(true)); } /// @notice Tests that the validate function successfully returns the right error when the @@ -1718,7 +1594,6 @@ contract OPContractsManagerStandardValidator_FaultDisputeGame_Test is OPContract /// @notice Tests that the validate function successfully returns the right error when the /// FaultDisputeGame (permissionless) CannonKona L2 Chain ID is invalid. function test_validate_faultDisputeGameInvalidCannonKonaL2ChainId_succeeds() public { - skipIfDevFeatureDisabled(DevFeatures.CANNON_KONA); uint256 badChainId = l2ChainId + 1; mockGameImplL2ChainId(dgf, GameTypes.CANNON_KONA, badChainId); @@ -1729,11 +1604,7 @@ contract OPContractsManagerStandardValidator_FaultDisputeGame_Test is OPContract /// FaultDisputeGame (permissionless) L2 Sequence Number is invalid. function test_validate_faultDisputeGameInvalidL2SequenceNumber_succeeds() public { vm.mockCall(address(fdgImpl), abi.encodeCall(IDisputeGame.l2SequenceNumber, ()), abi.encode(123)); - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - assertEq("PLDG-70,CKDG-70", _validate(true)); - } else { - assertEq("PLDG-70", _validate(true)); - } + assertEq("PLDG-70,CKDG-70", _validate(true)); } /// @notice Tests that the validate function successfully returns the right error when the @@ -1742,33 +1613,21 @@ contract OPContractsManagerStandardValidator_FaultDisputeGame_Test is OPContract vm.mockCall( address(fdgImpl), abi.encodeCall(IFaultDisputeGame.clockExtension, ()), abi.encode(Duration.wrap(1000)) ); - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - assertEq("PLDG-80,CKDG-80", _validate(true)); - } else { - assertEq("PLDG-80", _validate(true)); - } + assertEq("PLDG-80,CKDG-80", _validate(true)); } /// @notice Tests that the validate function successfully returns the right error when the /// FaultDisputeGame (permissionless) splitDepth is invalid. function test_validate_faultDisputeGameInvalidSplitDepth_succeeds() public { vm.mockCall(address(fdgImpl), abi.encodeCall(IFaultDisputeGame.splitDepth, ()), abi.encode(20)); - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - assertEq("PLDG-90,CKDG-90", _validate(true)); - } else { - assertEq("PLDG-90", _validate(true)); - } + assertEq("PLDG-90,CKDG-90", _validate(true)); } /// @notice Tests that the validate function successfully returns the right error when the /// FaultDisputeGame (permissionless) maxGameDepth is invalid. function test_validate_faultDisputeGameInvalidMaxGameDepth_succeeds() public { vm.mockCall(address(fdgImpl), abi.encodeCall(IFaultDisputeGame.maxGameDepth, ()), abi.encode(50)); - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - assertEq("PLDG-100,CKDG-100", _validate(true)); - } else { - assertEq("PLDG-100", _validate(true)); - } + assertEq("PLDG-100,CKDG-100", _validate(true)); } /// @notice Tests that the validate function successfully returns the right error when the @@ -1777,11 +1636,7 @@ contract OPContractsManagerStandardValidator_FaultDisputeGame_Test is OPContract vm.mockCall( address(fdgImpl), abi.encodeCall(IFaultDisputeGame.maxClockDuration, ()), abi.encode(Duration.wrap(1000)) ); - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - assertEq("PLDG-110,CKDG-110", _validate(true)); - } else { - assertEq("PLDG-110", _validate(true)); - } + assertEq("PLDG-110,CKDG-110", _validate(true)); } } @@ -1875,6 +1730,14 @@ contract OPContractsManagerStandardValidator_Versions_Test is OPContractsManager "l1StandardBridgeVersion empty" ); assertTrue(bytes(ISemver(standardValidator.mipsImpl()).version()).length > 0, "mipsVersion empty"); + assertTrue( + bytes(ISemver(standardValidator.faultDisputeGameImpl()).version()).length > 0, + "faultDisputeGameVersion empty" + ); + assertTrue( + bytes(ISemver(standardValidator.permissionedDisputeGameImpl()).version()).length > 0, + "permissionedDisputeGameVersion empty" + ); assertTrue( bytes(ISemver(standardValidator.optimismMintableERC20FactoryImpl()).version()).length > 0, "optimismMintableERC20FactoryVersion empty" @@ -1888,9 +1751,6 @@ contract OPContractsManagerStandardValidator_Versions_Test is OPContractsManager "anchorStateRegistryVersion empty" ); assertTrue(bytes(ISemver(standardValidator.delayedWETHImpl()).version()).length > 0, "delayedWETHVersion empty"); - assertTrue( - bytes(standardValidator.permissionedDisputeGameVersion()).length > 0, "permissionedDisputeGameVersion empty" - ); assertTrue(bytes(standardValidator.preimageOracleVersion()).length > 0, "preimageOracleVersion empty"); assertTrue(bytes(ISemver(standardValidator.ethLockboxImpl()).version()).length > 0, "ethLockboxVersion empty"); } diff --git a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerContainer.t.sol b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerContainer.t.sol index 8f42a642360..78eafbdf763 100644 --- a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerContainer.t.sol +++ b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerContainer.t.sol @@ -22,11 +22,7 @@ contract OPContractsManagerContainer_TestInit is Test { proxy: makeAddr("proxy"), proxyAdmin: makeAddr("proxyAdmin"), l1ChugSplashProxy: makeAddr("l1ChugSplashProxy"), - resolvedDelegateProxy: makeAddr("resolvedDelegateProxy"), - permissionedDisputeGame1: makeAddr("permissionedDisputeGame1"), - permissionedDisputeGame2: makeAddr("permissionedDisputeGame2"), - permissionlessDisputeGame1: makeAddr("permissionlessDisputeGame1"), - permissionlessDisputeGame2: makeAddr("permissionlessDisputeGame2") + resolvedDelegateProxy: makeAddr("resolvedDelegateProxy") }); implementations = OPContractsManagerContainer.Implementations({ diff --git a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerUtils.t.sol b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerUtils.t.sol index 48085b3d6a6..5ca424905f4 100644 --- a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerUtils.t.sol +++ b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerUtils.t.sol @@ -78,11 +78,7 @@ contract OPContractsManagerUtils_TestInit is Test { proxy: makeAddr("proxy"), proxyAdmin: makeAddr("proxyAdmin"), l1ChugSplashProxy: makeAddr("l1ChugSplashProxy"), - resolvedDelegateProxy: makeAddr("resolvedDelegateProxy"), - permissionedDisputeGame1: makeAddr("permissionedDisputeGame1"), - permissionedDisputeGame2: makeAddr("permissionedDisputeGame2"), - permissionlessDisputeGame1: makeAddr("permissionlessDisputeGame1"), - permissionlessDisputeGame2: makeAddr("permissionlessDisputeGame2") + resolvedDelegateProxy: makeAddr("resolvedDelegateProxy") }); // Set up implementations - use real StorageSetter, mocks for the rest. @@ -608,3 +604,65 @@ contract OPContractsManagerUtils_ContractsContainer_Test is OPContractsManagerUt assertEq(address(utils.contractsContainer()), address(container)); } } + +/// @title OPContractsManagerUtils_IsMatchingInstruction_Test +/// @notice Tests the isMatchingInstruction function. +contract OPContractsManagerUtils_IsMatchingInstruction_Test is OPContractsManagerUtils_TestInit { + /// @notice Tests that isMatchingInstruction returns true when the instruction matches the key and data. + function testFuzz_isMatchingInstruction_succeeds(OPContractsManagerUtils.ExtraInstruction memory _instruction) + public + view + { + assertTrue(utils.isMatchingInstruction(_instruction, _instruction.key, _instruction.data)); + } + + /// @notice Tests that isMatchingInstruction returns false when the instruction does not match the key. + function testFuzz_isMatchingInstruction_notMatchingKey_fails( + OPContractsManagerUtils.ExtraInstruction memory _instruction + ) + public + view + { + // Create a key that is not the same as the instruction key. + string memory _key = string.concat("not:", _instruction.key); + + assertFalse(utils.isMatchingInstruction(_instruction, _key, _instruction.data)); + } + + /// @notice Tests that isMatchingInstruction returns false when the instruction does not match the data. + function testFuzz_isMatchingInstruction_notMatchingData_fails( + OPContractsManagerUtils.ExtraInstruction memory _instruction + ) + public + view + { + // Create a data that is not the same as the instruction data. + bytes memory _data = bytes.concat("not:", _instruction.data); + + assertFalse(utils.isMatchingInstruction(_instruction, _instruction.key, _data)); + } +} + +/// @title OPContractsManagerUtils_IsMatchingInstructionByKey_Test +/// @notice Tests the isMatchingInstructionByKey function. +contract OPContractsManagerUtils_IsMatchingInstructionByKey_Test is OPContractsManagerUtils_TestInit { + /// @notice Tests that isMatchingInstructionByKey returns true when the instruction matches the key. + function testFuzz_isMatchingInstructionByKey_succeeds(OPContractsManagerUtils.ExtraInstruction memory _instruction) + public + view + { + assertTrue(utils.isMatchingInstructionByKey(_instruction, _instruction.key)); + } + + /// @notice Tests that isMatchingInstructionKey returns false when the instruction does not match the key. + function testFuzz_isMatchingInstructionByKey_notMatchingKey_fails( + OPContractsManagerUtils.ExtraInstruction memory _instruction + ) + public + view + { + // Create a key that is not the same as the instruction key. + string memory _key = string.concat("not:", _instruction.key); + assertFalse(utils.isMatchingInstructionByKey(_instruction, _key)); + } +} diff --git a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol index 53d4b8f3da4..e21b262715a 100644 --- a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol +++ b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerV2.t.sol @@ -132,30 +132,17 @@ contract OPContractsManagerV2_TestInit is CommonTest, DisputeGames { } // Run the StandardValidator checks on the newly deployed chain. - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - validator.validateWithOverrides( - IOPContractsManagerStandardValidator.ValidationInputDev({ - sysCfg: cts_.systemConfig, - cannonPrestate: cannonPrestate.raw(), - cannonKonaPrestate: cannonKonaPrestate.raw(), - l2ChainID: _deployConfig.l2ChainId, - proposer: deployProposer - }), - false, - validationOverrides - ); - } else { - validator.validateWithOverrides( - IOPContractsManagerStandardValidator.ValidationInput({ - sysCfg: cts_.systemConfig, - absolutePrestate: cannonPrestate.raw(), - l2ChainID: _deployConfig.l2ChainId, - proposer: deployProposer - }), - false, - validationOverrides - ); - } + validator.validateWithOverrides( + IOPContractsManagerStandardValidator.ValidationInputDev({ + sysCfg: cts_.systemConfig, + cannonPrestate: cannonPrestate.raw(), + cannonKonaPrestate: cannonKonaPrestate.raw(), + l2ChainID: _deployConfig.l2ChainId, + proposer: deployProposer + }), + false, + validationOverrides + ); return cts_; } @@ -275,7 +262,7 @@ contract OPContractsManagerV2_Upgrade_TestInit is OPContractsManagerV2_TestInit ); v2UpgradeInput.disputeGameConfigs.push( IOPContractsManagerV2.DisputeGameConfig({ - enabled: isDevFeatureEnabled(DevFeatures.CANNON_KONA), + enabled: true, initBond: disputeGameFactory.initBonds(GameTypes.CANNON_KONA), gameType: GameTypes.CANNON_KONA, gameArgs: abi.encode(IOPContractsManagerV2.FaultDisputeGameConfig({ absolutePrestate: cannonKonaPrestate })) @@ -286,6 +273,11 @@ contract OPContractsManagerV2_Upgrade_TestInit is OPContractsManagerV2_TestInit v2UpgradeInput.extraInstructions.push( IOPContractsManagerUtils.ExtraInstruction({ key: "PermittedProxyDeployment", data: bytes("DelayedWETH") }) ); + + // TODO(#18502): Remove the extra instruction for custom gas token after U18 ships. + v2UpgradeInput.extraInstructions.push( + IOPContractsManagerUtils.ExtraInstruction({ key: "overrides.cfg.useCustomGasToken", data: abi.encode(false) }) + ); } /// @notice Helper function that runs an OPCM V2 upgrade, asserts that the upgrade was successful, @@ -396,30 +388,17 @@ contract OPContractsManagerV2_Upgrade_TestInit is OPContractsManagerV2_TestInit } // Run the StandardValidator checks. - if (isDevFeatureEnabled(DevFeatures.CANNON_KONA)) { - validator.validateWithOverrides( - IOPContractsManagerStandardValidator.ValidationInputDev({ - sysCfg: v2UpgradeInput.systemConfig, - cannonPrestate: cannonPrestate.raw(), - cannonKonaPrestate: cannonKonaPrestate.raw(), - l2ChainID: l2ChainId, - proposer: initialProposer - }), - false, - validationOverrides - ); - } else { - validator.validateWithOverrides( - IOPContractsManagerStandardValidator.ValidationInput({ - sysCfg: v2UpgradeInput.systemConfig, - absolutePrestate: cannonPrestate.raw(), - l2ChainID: l2ChainId, - proposer: initialProposer - }), - false, - validationOverrides - ); - } + validator.validateWithOverrides( + IOPContractsManagerStandardValidator.ValidationInputDev({ + sysCfg: v2UpgradeInput.systemConfig, + cannonPrestate: cannonPrestate.raw(), + cannonKonaPrestate: cannonKonaPrestate.raw(), + l2ChainID: l2ChainId, + proposer: initialProposer + }), + false, + validationOverrides + ); } /// @notice Executes all past upgrades that have not yet been executed on mainnet as of the @@ -658,6 +637,22 @@ contract OPContractsManagerV2_Upgrade_Test is OPContractsManagerV2_Upgrade_TestI ); } + /// @notice Tests that the V2 upgrade function reverts when the user attempts to upgrade enabling custom gas token + /// after initial deployment. + function test_upgrade_enableCustomGasTokenAfterInitialDeployment_reverts() public { + // Override the extra instruction for custom gas token to attempt to enable it. + v2UpgradeInput.extraInstructions[1] = IOPContractsManagerUtils.ExtraInstruction({ + key: "overrides.cfg.useCustomGasToken", + data: abi.encode(true) + }); + + // nosemgrep: sol-style-use-abi-encodecall + runCurrentUpgradeV2( + chainPAO, + abi.encodeWithSelector(IOPContractsManagerV2.OPContractsManagerV2_CannotUpgradeToCustomGasToken.selector) + ); + } + /// @notice Tests that repeatedly upgrading can enable a previously disabled game type. function test_upgrade_enableGameType_succeeds() public { uint256 originalBond = disputeGameFactory.initBonds(GameTypes.CANNON); @@ -897,8 +892,8 @@ contract OPContractsManagerV2_Deploy_Test is OPContractsManagerV2_TestInit { ); deployConfig.disputeGameConfigs.push( IOPContractsManagerV2.DisputeGameConfig({ - enabled: isDevFeatureEnabled(DevFeatures.CANNON_KONA), - initBond: isDevFeatureEnabled(DevFeatures.CANNON_KONA) ? 0.08 ether : 0, // Standard init bond + enabled: true, + initBond: 0.08 ether, // Standard init bond gameType: GameTypes.CANNON_KONA, gameArgs: abi.encode(IOPContractsManagerV2.FaultDisputeGameConfig({ absolutePrestate: cannonKonaPrestate })) }) diff --git a/packages/contracts-bedrock/test/dispute/DisputeGameFactory.t.sol b/packages/contracts-bedrock/test/dispute/DisputeGameFactory.t.sol index 5cc183833f6..e17fde3c0a6 100644 --- a/packages/contracts-bedrock/test/dispute/DisputeGameFactory.t.sol +++ b/packages/contracts-bedrock/test/dispute/DisputeGameFactory.t.sol @@ -11,7 +11,6 @@ import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; // Libraries import "src/dispute/lib/Types.sol"; import "src/dispute/lib/Errors.sol"; -import { DevFeatures } from "src/libraries/DevFeatures.sol"; // Interfaces import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; @@ -187,11 +186,7 @@ abstract contract DisputeGameFactory_TestInit is CommonTest { internal returns (address gameImpl_, AlphabetVM vm_, IPreimageOracle preimageOracle_) { - if (isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - return setupFaultDisputeGameV2(_absolutePrestate); - } else { - return setupFaultDisputeGameV1(_absolutePrestate); - } + return setupFaultDisputeGameV2(_absolutePrestate); } /// @notice Sets up a fault game implementation @@ -274,11 +269,7 @@ abstract contract DisputeGameFactory_TestInit is CommonTest { internal returns (address gameImpl_, AlphabetVM vm_, IPreimageOracle preimageOracle_) { - if (isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - return setupPermissionedDisputeGameV2(_absolutePrestate, _proposer, _challenger); - } else { - return setupPermissionedDisputeGameV1(_absolutePrestate, _proposer, _challenger); - } + return setupPermissionedDisputeGameV2(_absolutePrestate, _proposer, _challenger); } function setupPermissionedDisputeGameV1( @@ -456,7 +447,7 @@ contract DisputeGameFactory_Create_Test is DisputeGameFactory_TestInit { { // Ensure that the `gameType` is within the bounds of the `GameType` enum's possible // values. - uint32 maxGameType = isDevFeatureEnabled(DevFeatures.CANNON_KONA) ? 8 : 2; + uint32 maxGameType = 8; GameType gt = GameType.wrap(uint8(bound(gameType, 0, maxGameType))); // Ensure the rootClaim has a VMStatus that disagrees with the validity. rootClaim = changeClaimStatus(rootClaim, VMStatuses.INVALID); @@ -523,7 +514,7 @@ contract DisputeGameFactory_Create_Test is DisputeGameFactory_TestInit { // Ensure that the `gameType` is within the bounds of the `GameType` enum's possible // values. We skip over game type = 0, since the deploy script set the implementation for // that game type. - uint32 maxGameType = isDevFeatureEnabled(DevFeatures.CANNON_KONA) ? 8 : 2; + uint32 maxGameType = 8; GameType gt = GameType.wrap(uint32(bound(gameType, maxGameType + 1, type(uint32).max))); // Ensure the rootClaim has a VMStatus that disagrees with the validity. rootClaim = changeClaimStatus(rootClaim, VMStatuses.INVALID); @@ -537,7 +528,7 @@ contract DisputeGameFactory_Create_Test is DisputeGameFactory_TestInit { function testFuzz_create_sameUUID_reverts(uint32 gameType, Claim rootClaim, bytes calldata extraData) public { // Ensure that the `gameType` is within the bounds of the `GameType` enum's possible // values. - uint32 maxGameType = isDevFeatureEnabled(DevFeatures.CANNON_KONA) ? 8 : 2; + uint32 maxGameType = 8; GameType gt = GameType.wrap(uint8(bound(gameType, 0, maxGameType))); // Ensure the rootClaim has a VMStatus that disagrees with the validity. rootClaim = changeClaimStatus(rootClaim, VMStatuses.INVALID); diff --git a/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol b/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol index e4970e0ad76..3ad1f2a0b9e 100644 --- a/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol +++ b/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol @@ -21,7 +21,6 @@ import { LibClock } from "src/dispute/lib/LibUDT.sol"; import { LibPosition } from "src/dispute/lib/LibPosition.sol"; import "src/dispute/lib/Types.sol"; import "src/dispute/lib/Errors.sol"; -import { DevFeatures } from "src/libraries/DevFeatures.sol"; // Interfaces import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; @@ -29,7 +28,6 @@ import { IPreimageOracle } from "interfaces/dispute/IBigStepper.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; import { IFaultDisputeGameV2 } from "interfaces/dispute/v2/IFaultDisputeGameV2.sol"; -import { IDelayedWETH } from "interfaces/dispute/IDelayedWETH.sol"; contract ClaimCreditReenter { Vm internal immutable vm; @@ -233,268 +231,11 @@ contract FaultDisputeGame_Version_Test is FaultDisputeGame_TestInit { } } -/// @title FaultDisputeGame_Constructor_Test -/// @notice Tests the constructor of the `FaultDisputeGame` contract. -contract FaultDisputeGame_Constructor_Test is FaultDisputeGame_TestInit { - function setUp() public virtual override { - super.setUp(); - skipIfDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES); - } - - /// @notice Tests that the constructor of the `FaultDisputeGame` reverts when the - /// `MAX_GAME_DEPTH` parameter is greater than `LibPosition.MAX_POSITION_BITLEN - 1`. - function testFuzz_constructor_maxDepthTooLarge_reverts(uint256 _maxGameDepth) public { - IPreimageOracle oracle = IPreimageOracle( - DeployUtils.create1({ - _name: "PreimageOracle", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IPreimageOracle.__constructor__, (0, 0))) - }) - ); - AlphabetVM alphabetVM = new AlphabetVM(absolutePrestate, oracle); - - _maxGameDepth = bound(_maxGameDepth, LibPosition.MAX_POSITION_BITLEN, type(uint256).max - 1); - vm.expectRevert(MaxDepthTooLarge.selector); - DeployUtils.create1({ - _name: "FaultDisputeGame", - _args: DeployUtils.encodeConstructor( - abi.encodeCall( - IFaultDisputeGame.__constructor__, - ( - IFaultDisputeGame.GameConstructorParams({ - gameType: GAME_TYPE, - absolutePrestate: absolutePrestate, - maxGameDepth: _maxGameDepth, - splitDepth: _maxGameDepth + 1, - clockExtension: Duration.wrap(3 hours), - maxClockDuration: Duration.wrap(3.5 days), - vm: alphabetVM, - weth: IDelayedWETH(payable(address(0))), - anchorStateRegistry: IAnchorStateRegistry(address(0)), - l2ChainId: 10 - }) - ) - ) - ) - }); - } - - /// @notice Tests that the constructor of the `FaultDisputeGame` reverts when the challenge - /// period of the preimage oracle being used by the game's VM is too large. - /// @param _challengePeriod The challenge period of the preimage oracle. - function testFuzz_constructor_oracleChallengePeriodTooLarge_reverts(uint256 _challengePeriod) public { - _challengePeriod = bound(_challengePeriod, uint256(type(uint64).max) + 1, type(uint256).max); - - IPreimageOracle oracle = IPreimageOracle( - DeployUtils.create1({ - _name: "PreimageOracle", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IPreimageOracle.__constructor__, (0, 0))) - }) - ); - AlphabetVM alphabetVM = new AlphabetVM(absolutePrestate, IPreimageOracle(address(oracle))); - - // PreimageOracle constructor will revert if the challenge period is too large, so we need - // to mock the call to pretend this is a bugged implementation where the challenge period - // is allowed to be too large. - vm.mockCall(address(oracle), abi.encodeCall(IPreimageOracle.challengePeriod, ()), abi.encode(_challengePeriod)); - - vm.expectRevert(InvalidChallengePeriod.selector); - DeployUtils.create1({ - _name: "FaultDisputeGame", - _args: DeployUtils.encodeConstructor( - abi.encodeCall( - IFaultDisputeGame.__constructor__, - ( - IFaultDisputeGame.GameConstructorParams({ - gameType: GAME_TYPE, - absolutePrestate: absolutePrestate, - maxGameDepth: 2 ** 3, - splitDepth: 2 ** 2, - clockExtension: Duration.wrap(3 hours), - maxClockDuration: Duration.wrap(3.5 days), - vm: alphabetVM, - weth: IDelayedWETH(payable(address(0))), - anchorStateRegistry: IAnchorStateRegistry(address(0)), - l2ChainId: 10 - }) - ) - ) - ) - }); - } - - /// @notice Tests that the constructor of the `FaultDisputeGame` reverts when the `_splitDepth` - /// parameter is greater than or equal to the `MAX_GAME_DEPTH` - function testFuzz_constructor_invalidSplitDepth_reverts(uint256 _splitDepth) public { - AlphabetVM alphabetVM = new AlphabetVM( - absolutePrestate, - IPreimageOracle( - DeployUtils.create1({ - _name: "PreimageOracle", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IPreimageOracle.__constructor__, (0, 0))) - }) - ) - ); - - uint256 maxGameDepth = 2 ** 3; - _splitDepth = bound(_splitDepth, maxGameDepth - 1, type(uint256).max); - vm.expectRevert(InvalidSplitDepth.selector); - DeployUtils.create1({ - _name: "FaultDisputeGame", - _args: DeployUtils.encodeConstructor( - abi.encodeCall( - IFaultDisputeGame.__constructor__, - ( - IFaultDisputeGame.GameConstructorParams({ - gameType: GAME_TYPE, - absolutePrestate: absolutePrestate, - maxGameDepth: maxGameDepth, - splitDepth: _splitDepth, - clockExtension: Duration.wrap(3 hours), - maxClockDuration: Duration.wrap(3.5 days), - vm: alphabetVM, - weth: IDelayedWETH(payable(address(0))), - anchorStateRegistry: IAnchorStateRegistry(address(0)), - l2ChainId: 10 - }) - ) - ) - ) - }); - } - - /// @notice Tests that the constructor of the `FaultDisputeGame` reverts when the `_splitDepth` - /// parameter is less than the minimum split depth (currently 2). - function testFuzz_constructor_lowSplitDepth_reverts(uint256 _splitDepth) public { - AlphabetVM alphabetVM = new AlphabetVM( - absolutePrestate, - IPreimageOracle( - DeployUtils.create1({ - _name: "PreimageOracle", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IPreimageOracle.__constructor__, (0, 0))) - }) - ) - ); - - uint256 minSplitDepth = 2; - _splitDepth = bound(_splitDepth, 0, minSplitDepth - 1); - vm.expectRevert(InvalidSplitDepth.selector); - DeployUtils.create1({ - _name: "FaultDisputeGame", - _args: DeployUtils.encodeConstructor( - abi.encodeCall( - IFaultDisputeGame.__constructor__, - ( - IFaultDisputeGame.GameConstructorParams({ - gameType: GAME_TYPE, - absolutePrestate: absolutePrestate, - maxGameDepth: 2 ** 3, - splitDepth: _splitDepth, - clockExtension: Duration.wrap(3 hours), - maxClockDuration: Duration.wrap(3.5 days), - vm: alphabetVM, - weth: IDelayedWETH(payable(address(0))), - anchorStateRegistry: IAnchorStateRegistry(address(0)), - l2ChainId: 10 - }) - ) - ) - ) - }); - } - - /// @notice Tests that the constructor of the `FaultDisputeGame` reverts when clock - /// extension * 2 is greater than the max clock duration. - function testFuzz_constructor_clockExtensionTooLong_reverts( - uint64 _maxClockDuration, - uint64 _clockExtension - ) - public - { - AlphabetVM alphabetVM = new AlphabetVM( - absolutePrestate, - IPreimageOracle( - DeployUtils.create1({ - _name: "PreimageOracle", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IPreimageOracle.__constructor__, (0, 0))) - }) - ) - ); - - // Force the clock extension * 2 to be greater than the max clock duration, but keep things - // within bounds of the uint64 type. - _maxClockDuration = uint64(bound(_maxClockDuration, 0, type(uint64).max / 2 - 1)); - _clockExtension = uint64(bound(_clockExtension, _maxClockDuration / 2 + 1, type(uint64).max / 2)); - - vm.expectRevert(InvalidClockExtension.selector); - DeployUtils.create1({ - _name: "FaultDisputeGame", - _args: DeployUtils.encodeConstructor( - abi.encodeCall( - IFaultDisputeGame.__constructor__, - ( - IFaultDisputeGame.GameConstructorParams({ - gameType: GAME_TYPE, - absolutePrestate: absolutePrestate, - maxGameDepth: 16, - splitDepth: 8, - clockExtension: Duration.wrap(_clockExtension), - maxClockDuration: Duration.wrap(_maxClockDuration), - vm: alphabetVM, - weth: IDelayedWETH(payable(address(0))), - anchorStateRegistry: IAnchorStateRegistry(address(0)), - l2ChainId: 10 - }) - ) - ) - ) - }); - } - - /// @notice Tests that the constructor of the `FaultDisputeGame` reverts when the `_gameType` - /// parameter is set to the reserved `type(uint32).max` game type. - function test_constructor_reservedGameType_reverts() public { - AlphabetVM alphabetVM = new AlphabetVM( - absolutePrestate, - IPreimageOracle( - DeployUtils.create1({ - _name: "PreimageOracle", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IPreimageOracle.__constructor__, (0, 0))) - }) - ) - ); - - vm.expectRevert(ReservedGameType.selector); - DeployUtils.create1({ - _name: "FaultDisputeGame", - _args: DeployUtils.encodeConstructor( - abi.encodeCall( - IFaultDisputeGame.__constructor__, - ( - IFaultDisputeGame.GameConstructorParams({ - gameType: GameType.wrap(type(uint32).max), - absolutePrestate: absolutePrestate, - maxGameDepth: 16, - splitDepth: 8, - clockExtension: Duration.wrap(3 hours), - maxClockDuration: Duration.wrap(3.5 days), - vm: alphabetVM, - weth: IDelayedWETH(payable(address(0))), - anchorStateRegistry: IAnchorStateRegistry(address(0)), - l2ChainId: 10 - }) - ) - ) - ) - }); - } -} - /// @title FaultDisputeGame_Constructor_Test /// @notice Tests the constructor of the `FaultDisputeGame` contract. contract FaultDisputeGameV2_Constructor_Test is FaultDisputeGame_TestInit { function setUp() public virtual override { super.setUp(); - skipIfDevFeatureDisabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES); } /// @notice Tests that the constructor of the `FaultDisputeGame` reverts when the @@ -668,7 +409,6 @@ contract FaultDisputeGame_Initialize_Test is FaultDisputeGame_TestInit { /// @notice Tests that the game cannot be initialized with incorrect CWIA calldata length /// caused by additional immutable args data function test_initialize_extraImmutableArgsBytes_reverts(uint256 _extraByteCount) public { - skipIfDevFeatureDisabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES); (bytes memory correctArgs,,) = getFaultDisputeGameV2ImmutableArgs(absolutePrestate); // We bound the upper end to 23.5KB to ensure that the minimal proxy never surpasses the @@ -694,7 +434,6 @@ contract FaultDisputeGame_Initialize_Test is FaultDisputeGame_TestInit { /// @notice Tests that the game cannot be initialized with incorrect CWIA calldata length /// caused by missing immutable args data function test_initialize_missingImmutableArgsBytes_reverts(uint256 _truncatedByteCount) public { - skipIfDevFeatureDisabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES); (bytes memory correctArgs,,) = getFaultDisputeGameV2ImmutableArgs(absolutePrestate); _truncatedByteCount = (_truncatedByteCount % correctArgs.length) + 1; @@ -2876,14 +2615,8 @@ contract FaultDisputeGame_Uncategorized_Test is FaultDisputeGame_TestInit { // Construct the expected CWIA data that the proxy will pass to the implementation, // alongside any extra calldata passed by the user. Hash l1Head = gameProxy.l1Head(); - bytes memory cwiaData; - if (isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - cwiaData = abi.encodePacked( - address(this), gameProxy.rootClaim(), l1Head, gameProxy.gameType(), gameProxy.extraData() - ); - } else { - cwiaData = abi.encodePacked(address(this), gameProxy.rootClaim(), l1Head, gameProxy.extraData()); - } + bytes memory cwiaData = + abi.encodePacked(address(this), gameProxy.rootClaim(), l1Head, gameProxy.gameType(), gameProxy.extraData()); // We expect a `ReceiveETH` event to be emitted when 0 bytes of calldata are sent; The // fallback is always reached *within the minimal proxy* in `LibClone`'s version of diff --git a/packages/contracts-bedrock/test/dispute/PermissionedDisputeGame.t.sol b/packages/contracts-bedrock/test/dispute/PermissionedDisputeGame.t.sol index f2876bc39da..e5f15c3424c 100644 --- a/packages/contracts-bedrock/test/dispute/PermissionedDisputeGame.t.sol +++ b/packages/contracts-bedrock/test/dispute/PermissionedDisputeGame.t.sol @@ -8,7 +8,6 @@ import { AlphabetVM } from "test/mocks/AlphabetVM.sol"; // Libraries import "src/dispute/lib/Types.sol"; import "src/dispute/lib/Errors.sol"; -import { DevFeatures } from "src/libraries/DevFeatures.sol"; // Interfaces import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; @@ -286,7 +285,6 @@ contract PermissionedDisputeGame_Initialize_Test is PermissionedDisputeGame_Test /// @notice Tests that the game cannot be initialized with incorrect CWIA calldata length /// caused by additional immutable args data function test_initialize_extraImmutableArgsBytes_reverts(uint256 _extraByteCount) public { - skipIfDevFeatureDisabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES); (bytes memory correctArgs,,) = getPermissionedDisputeGameV2ImmutableArgs(absolutePrestate, PROPOSER, CHALLENGER); // We bound the upper end to 23.5KB to ensure that the minimal proxy never surpasses the @@ -313,7 +311,6 @@ contract PermissionedDisputeGame_Initialize_Test is PermissionedDisputeGame_Test /// @notice Tests that the game cannot be initialized with incorrect CWIA calldata length /// caused by missing immutable args data function test_initialize_missingImmutableArgsBytes_reverts(uint256 _truncatedByteCount) public { - skipIfDevFeatureDisabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES); (bytes memory correctArgs,,) = getPermissionedDisputeGameV2ImmutableArgs(absolutePrestate, PROPOSER, CHALLENGER); _truncatedByteCount = (_truncatedByteCount % correctArgs.length) + 1; diff --git a/packages/contracts-bedrock/test/opcm/DeployDisputeGame.t.sol b/packages/contracts-bedrock/test/opcm/DeployDisputeGame.t.sol index 1c429e245a9..6931a6e13c2 100644 --- a/packages/contracts-bedrock/test/opcm/DeployDisputeGame.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployDisputeGame.t.sol @@ -13,7 +13,6 @@ import { IPreimageOracle } from "interfaces/cannon/IPreimageOracle.sol"; import { LibPosition } from "src/dispute/lib/LibPosition.sol"; import { GameType } from "src/dispute/lib/Types.sol"; import { LibString } from "@solady/utils/LibString.sol"; -import { Config } from "scripts/libraries/Config.sol"; import { PreimageOracle } from "src/cannon/PreimageOracle.sol"; import { DeployDisputeGame } from "scripts/deploy/DeployDisputeGame.s.sol"; @@ -74,7 +73,6 @@ contract DeployDisputeGame_Test is Test { _input.maxGameDepth = _maxGameDepth; _input.splitDepth = bound(_splitDepth, 2, _maxGameDepth - 2); _input.vmAddress = bigStepper; - _input.useV2 = Config.devFeatureDeployV2DisputeGames(); // Run the deployment script. deployDisputeGame.run(_input); @@ -181,7 +179,6 @@ contract DeployDisputeGame_Test is Test { function defaultFaultDisputeGameInput() private view returns (DeployDisputeGame.Input memory input_) { input_ = DeployDisputeGame.Input({ release: "op-contracts", - useV2: Config.devFeatureDeployV2DisputeGames(), gameKind: "FaultDisputeGame", gameType: GameType.wrap(1), absolutePrestate: bytes32(uint256(1)), @@ -201,7 +198,6 @@ contract DeployDisputeGame_Test is Test { function defaultPermissionedDisputeGameInput() private view returns (DeployDisputeGame.Input memory input_) { input_ = DeployDisputeGame.Input({ release: "op-contracts", - useV2: Config.devFeatureDeployV2DisputeGames(), gameKind: "PermissionedDisputeGame", gameType: GameType.wrap(1), absolutePrestate: bytes32(uint256(1)), diff --git a/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol b/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol index e5f945dba26..3136aaeb0c2 100644 --- a/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol @@ -51,83 +51,40 @@ contract DeployImplementations_Test is Test, FeatureFlags { assertNotEq(address(output.systemConfigImpl), address(0)); - if (isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - assertNotEq(address(output.faultDisputeGameV2Impl), address(0), "FaultDisputeGameV2 should be deployed"); - assertNotEq( - address(output.permissionedDisputeGameV2Impl), - address(0), - "PermissionedDisputeGameV2 should be deployed" - ); - - // Validate constructor args for FaultDisputeGameV2 - assertEq(output.faultDisputeGameV2Impl.maxGameDepth(), 73, "FaultDisputeGameV2 maxGameDepth incorrect"); - assertEq(output.faultDisputeGameV2Impl.splitDepth(), 30, "FaultDisputeGameV2 splitDepth incorrect"); - assertEq( - output.faultDisputeGameV2Impl.clockExtension().raw(), - 10800, - "FaultDisputeGameV2 clockExtension incorrect" - ); - assertEq( - output.faultDisputeGameV2Impl.maxClockDuration().raw(), - 302400, - "FaultDisputeGameV2 maxClockDuration incorrect" - ); - - // Validate constructor args for PermissionedDisputeGameV2 - assertEq( - output.permissionedDisputeGameV2Impl.maxGameDepth(), - 73, - "PermissionedDisputeGameV2 maxGameDepth incorrect" - ); - assertEq( - output.permissionedDisputeGameV2Impl.splitDepth(), 30, "PermissionedDisputeGameV2 splitDepth incorrect" - ); - assertEq( - output.permissionedDisputeGameV2Impl.clockExtension().raw(), - 10800, - "PermissionedDisputeGameV2 clockExtension incorrect" - ); - assertEq( - output.permissionedDisputeGameV2Impl.maxClockDuration().raw(), - 302400, - "PermissionedDisputeGameV2 maxClockDuration incorrect" - ); + assertNotEq(address(output.faultDisputeGameV2Impl), address(0), "FaultDisputeGameV2 should be deployed"); + assertNotEq( + address(output.permissionedDisputeGameV2Impl), address(0), "PermissionedDisputeGameV2 should be deployed" + ); - // Ensure legacy blueprints were not deployed - assertEq( - output.opcm.blueprints().permissionedDisputeGame1, - address(0), - "PermissionedDisputeGame1 blueprint should not be deployed" - ); - assertEq( - output.opcm.blueprints().permissionedDisputeGame2, - address(0), - "PermissionedDisputeGame2 blueprint should not be deployed" - ); - assertEq( - output.opcm.blueprints().permissionlessDisputeGame1, - address(0), - "PermissionlessDisputeGame1 blueprint should not be deployed" - ); - assertEq( - output.opcm.blueprints().permissionlessDisputeGame2, - address(0), - "PermissionlessDisputeGame2 blueprint should not be deployed" - ); - } else { - assertEq(address(output.faultDisputeGameV2Impl), address(0), "FaultDisputeGameV2 should not be deployed"); - assertEq( - address(output.permissionedDisputeGameV2Impl), - address(0), - "PermissionedDisputeGameV2 should not be deployed" - ); + // Validate constructor args for FaultDisputeGameV2 + assertEq(output.faultDisputeGameV2Impl.maxGameDepth(), 73, "FaultDisputeGameV2 maxGameDepth incorrect"); + assertEq(output.faultDisputeGameV2Impl.splitDepth(), 30, "FaultDisputeGameV2 splitDepth incorrect"); + assertEq( + output.faultDisputeGameV2Impl.clockExtension().raw(), 10800, "FaultDisputeGameV2 clockExtension incorrect" + ); + assertEq( + output.faultDisputeGameV2Impl.maxClockDuration().raw(), + 302400, + "FaultDisputeGameV2 maxClockDuration incorrect" + ); - // Ensure other contracts are still deployed - assertNotEq(address(output.systemConfigImpl), address(0), "SystemConfig should still be deployed"); - assertNotEq( - address(output.disputeGameFactoryImpl), address(0), "DisputeGameFactory should still be deployed" - ); - } + // Validate constructor args for PermissionedDisputeGameV2 + assertEq( + output.permissionedDisputeGameV2Impl.maxGameDepth(), 73, "PermissionedDisputeGameV2 maxGameDepth incorrect" + ); + assertEq( + output.permissionedDisputeGameV2Impl.splitDepth(), 30, "PermissionedDisputeGameV2 splitDepth incorrect" + ); + assertEq( + output.permissionedDisputeGameV2Impl.clockExtension().raw(), + 10800, + "PermissionedDisputeGameV2 clockExtension incorrect" + ); + assertEq( + output.permissionedDisputeGameV2Impl.maxClockDuration().raw(), + 302400, + "PermissionedDisputeGameV2 maxClockDuration incorrect" + ); // for the super DG implementation deployments if (isDevFeatureEnabled(DevFeatures.OPTIMISM_PORTAL_INTEROP)) { @@ -213,13 +170,8 @@ contract DeployImplementations_Test is Test, FeatureFlags { assertEq(address(output1.faultDisputeGameV2Impl), address(output2.faultDisputeGameV2Impl), "1400"); assertEq(address(output1.permissionedDisputeGameV2Impl), address(output2.permissionedDisputeGameV2Impl), "1500"); - if (isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - assertNotEq(address(output1.faultDisputeGameV2Impl), address(0), "V2 contracts should not be null"); - assertNotEq(address(output1.permissionedDisputeGameV2Impl), address(0), "V2 contracts should not be null"); - } else { - assertEq(address(output1.faultDisputeGameV2Impl), address(0), "V2 contracts should be null"); - assertEq(address(output1.permissionedDisputeGameV2Impl), address(0), "V2 contracts should be null"); - } + assertNotEq(address(output1.faultDisputeGameV2Impl), address(0), "V2 contracts should not be null"); + assertNotEq(address(output1.permissionedDisputeGameV2Impl), address(0), "V2 contracts should not be null"); } function testFuzz_run_memory_succeeds( @@ -272,18 +224,6 @@ contract DeployImplementations_Test is Test, FeatureFlags { _faultGameV2ClockExtension = bound(_faultGameV2ClockExtension, 1, 7 days); _faultGameV2MaxClockDuration = bound(_faultGameV2MaxClockDuration, _faultGameV2ClockExtension * 2, 30 days); - bool usesV2GameParameters = DevFeatures.isDevFeatureEnabled( - _devFeatureBitmap, DevFeatures.DEPLOY_V2_DISPUTE_GAMES - ) || DevFeatures.isDevFeatureEnabled(_devFeatureBitmap, DevFeatures.OPTIMISM_PORTAL_INTEROP); - // When V2 is not enabled, set V2 params to 0 to match script expectations - // Otherwise ensure they remain within bounds already set - if (!usesV2GameParameters) { - _faultGameV2MaxGameDepth = 0; - _faultGameV2SplitDepth = 0; - _faultGameV2ClockExtension = 0; - _faultGameV2MaxClockDuration = 0; - } - DeployImplementations.Input memory input = DeployImplementations.Input( _withdrawalDelaySeconds, _minProposalSizeBytes, @@ -319,44 +259,36 @@ contract DeployImplementations_Test is Test, FeatureFlags { assertNotEq(address(output.opcmDeployer), address(0), "1000"); assertNotEq(address(output.opcmGameTypeAdder), address(0), "1100"); - // Check V2 contracts based on feature flag - bool v2Enabled = DevFeatures.isDevFeatureEnabled(_devFeatureBitmap, DevFeatures.DEPLOY_V2_DISPUTE_GAMES); - if (v2Enabled) { - assertNotEq(address(output.faultDisputeGameV2Impl), address(0), "V2 should be deployed when enabled"); - assertNotEq(address(output.permissionedDisputeGameV2Impl), address(0), "V2 should be deployed when enabled"); + assertNotEq(address(output.faultDisputeGameV2Impl), address(0), "V2 should be deployed when enabled"); + assertNotEq(address(output.permissionedDisputeGameV2Impl), address(0), "V2 should be deployed when enabled"); - // Verify V2 constructor parameters match fuzz inputs - assertEq(output.faultDisputeGameV2Impl.maxGameDepth(), _faultGameV2MaxGameDepth, "FDGv2 maxGameDepth"); - assertEq(output.faultDisputeGameV2Impl.splitDepth(), _faultGameV2SplitDepth, "FDGv2 splitDepth"); - assertEq( - output.faultDisputeGameV2Impl.clockExtension().raw(), - uint64(_faultGameV2ClockExtension), - "FDGv2 clockExtension" - ); - assertEq( - output.faultDisputeGameV2Impl.maxClockDuration().raw(), - uint64(_faultGameV2MaxClockDuration), - "FDGv2 maxClockDuration" - ); + // Verify V2 constructor parameters match fuzz inputs + assertEq(output.faultDisputeGameV2Impl.maxGameDepth(), _faultGameV2MaxGameDepth, "FDGv2 maxGameDepth"); + assertEq(output.faultDisputeGameV2Impl.splitDepth(), _faultGameV2SplitDepth, "FDGv2 splitDepth"); + assertEq( + output.faultDisputeGameV2Impl.clockExtension().raw(), + uint64(_faultGameV2ClockExtension), + "FDGv2 clockExtension" + ); + assertEq( + output.faultDisputeGameV2Impl.maxClockDuration().raw(), + uint64(_faultGameV2MaxClockDuration), + "FDGv2 maxClockDuration" + ); + + assertEq(output.permissionedDisputeGameV2Impl.maxGameDepth(), _faultGameV2MaxGameDepth, "PDGv2 maxGameDepth"); + assertEq(output.permissionedDisputeGameV2Impl.splitDepth(), _faultGameV2SplitDepth, "PDGv2 splitDepth"); + assertEq( + output.permissionedDisputeGameV2Impl.clockExtension().raw(), + uint64(_faultGameV2ClockExtension), + "PDGv2 clockExtension" + ); + assertEq( + output.permissionedDisputeGameV2Impl.maxClockDuration().raw(), + uint64(_faultGameV2MaxClockDuration), + "PDGv2 maxClockDuration" + ); - assertEq( - output.permissionedDisputeGameV2Impl.maxGameDepth(), _faultGameV2MaxGameDepth, "PDGv2 maxGameDepth" - ); - assertEq(output.permissionedDisputeGameV2Impl.splitDepth(), _faultGameV2SplitDepth, "PDGv2 splitDepth"); - assertEq( - output.permissionedDisputeGameV2Impl.clockExtension().raw(), - uint64(_faultGameV2ClockExtension), - "PDGv2 clockExtension" - ); - assertEq( - output.permissionedDisputeGameV2Impl.maxClockDuration().raw(), - uint64(_faultGameV2MaxClockDuration), - "PDGv2 maxClockDuration" - ); - } else { - assertEq(address(output.faultDisputeGameV2Impl), address(0), "V2 should be null when disabled"); - assertEq(address(output.permissionedDisputeGameV2Impl), address(0), "V2 should be null when disabled"); - } bool superGamesEnabled = DevFeatures.isDevFeatureEnabled(_devFeatureBitmap, DevFeatures.OPTIMISM_PORTAL_INTEROP); if (superGamesEnabled) { assertNotEq( @@ -424,16 +356,8 @@ contract DeployImplementations_Test is Test, FeatureFlags { assertNotEq(address(output.opcmDeployer).code, empty, "2200"); assertNotEq(address(output.opcmGameTypeAdder).code, empty, "2300"); - // V2 contracts code existence based on feature flag - if (v2Enabled) { - assertNotEq(address(output.faultDisputeGameV2Impl).code, empty, "V2 FDG should have code when enabled"); - assertNotEq( - address(output.permissionedDisputeGameV2Impl).code, empty, "V2 PDG should have code when enabled" - ); - } else { - assertEq(address(output.faultDisputeGameV2Impl).code, empty, "V2 FDG should be empty when disabled"); - assertEq(address(output.permissionedDisputeGameV2Impl).code, empty, "V2 PDG should be empty when disabled"); - } + assertNotEq(address(output.faultDisputeGameV2Impl).code, empty, "V2 FDG should have code when enabled"); + assertNotEq(address(output.permissionedDisputeGameV2Impl).code, empty, "V2 PDG should have code when enabled"); if (superGamesEnabled) { assertNotEq(address(output.superFaultDisputeGameImpl).code, empty, "Super DG should have code when enabled"); assertNotEq( @@ -531,54 +455,7 @@ contract DeployImplementations_Test is Test, FeatureFlags { deployImplementations.run(input); } - function test_v2ParamsValidation_withFlagDisabled_succeeds() public { - // When V2 flag is disabled, V2 params should be 0 or within safe bounds - DeployImplementations.Input memory input = defaultInput(); - input.devFeatureBitmap = bytes32(0); // V2 disabled - - // Test that zero values are accepted - input.faultGameV2MaxGameDepth = 0; - input.faultGameV2SplitDepth = 0; - input.faultGameV2ClockExtension = 0; - input.faultGameV2MaxClockDuration = 0; - - DeployImplementations.Output memory output = deployImplementations.run(input); - assertEq(address(output.faultDisputeGameV2Impl), address(0), "V2 FDG should be null when disabled"); - assertEq(address(output.permissionedDisputeGameV2Impl), address(0), "V2 PDG should be null when disabled"); - } - - function test_invalidV2GameParams_withV2Disabled_succeeds() public { - skipIfDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES); - skipIfDevFeatureEnabled(DevFeatures.OPTIMISM_PORTAL_INTEROP); // for the Super DG - DeployImplementations.Input memory input; - - // When V2 flag is disabled, out-of-range values are ok - input = defaultInput(); - input.faultGameV2ClockExtension = type(uint256).max; - input.faultGameV2MaxClockDuration = type(uint256).max; - input.faultGameV2MaxGameDepth = 300; - input.faultGameV2SplitDepth = 1; // < 2 - input.faultGameV2ClockExtension = 0; - // Should not revert - deployImplementations.run(input); - - // Reset and test invalid split depth (too large, >= maxGameDepth) - input = defaultInput(); - input.faultGameV2MaxGameDepth = 50; - input.faultGameV2SplitDepth = 50; // splitDepth + 1 must be < maxGameDepth - // Should not revert - deployImplementations.run(input); - - // Reset and test maxClockDuration < clockExtension - input = defaultInput(); - input.faultGameV2ClockExtension = 1000; - input.faultGameV2MaxClockDuration = 500; // < clockExtension - // Should not revert - deployImplementations.run(input); - } - function test_invalidV2GameParams_withV2Enabled_reverts() public { - skipIfDevFeatureDisabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES); DeployImplementations.Input memory input; // Test that huge clock extension is rejected @@ -632,81 +509,6 @@ contract DeployImplementations_Test is Test, FeatureFlags { deployImplementations.run(input); } - function test_reuseImplementation_withV2Flags_succeeds() public { - DeployImplementations.Input memory inputEnabled = defaultInput(); - inputEnabled.devFeatureBitmap = DevFeatures.DEPLOY_V2_DISPUTE_GAMES; - DeployImplementations.Output memory output1 = deployImplementations.run(inputEnabled); - - DeployImplementations.Input memory inputDisabled = defaultInput(); - inputDisabled.devFeatureBitmap = bytes32(0); - DeployImplementations.Output memory output2 = deployImplementations.run(inputDisabled); - - // V2 contracts should be different between enabled and disabled - assertTrue( - address(output1.faultDisputeGameV2Impl) != address(output2.faultDisputeGameV2Impl), - "V2 addresses should differ between enabled/disabled" - ); - assertTrue( - address(output1.permissionedDisputeGameV2Impl) != address(output2.permissionedDisputeGameV2Impl), - "V2 addresses should differ between enabled/disabled" - ); - - // Validate constructor args for FaultDisputeGameV2 - assertEq(output1.faultDisputeGameV2Impl.maxGameDepth(), 73, "FaultDisputeGameV2 maxGameDepth incorrect"); - assertEq(output1.faultDisputeGameV2Impl.splitDepth(), 30, "FaultDisputeGameV2 splitDepth incorrect"); - assertEq( - output1.faultDisputeGameV2Impl.clockExtension().raw(), 10800, "FaultDisputeGameV2 clockExtension incorrect" - ); - assertEq( - output1.faultDisputeGameV2Impl.maxClockDuration().raw(), - 302400, - "FaultDisputeGameV2 maxClockDuration incorrect" - ); - - // Validate constructor args for PermissionedDisputeGameV2 - assertEq( - output1.permissionedDisputeGameV2Impl.maxGameDepth(), 73, "PermissionedDisputeGameV2 maxGameDepth incorrect" - ); - assertEq( - output1.permissionedDisputeGameV2Impl.splitDepth(), 30, "PermissionedDisputeGameV2 splitDepth incorrect" - ); - assertEq( - output1.permissionedDisputeGameV2Impl.clockExtension().raw(), - 10800, - "PermissionedDisputeGameV2 clockExtension incorrect" - ); - assertEq( - output1.permissionedDisputeGameV2Impl.maxClockDuration().raw(), - 302400, - "PermissionedDisputeGameV2 maxClockDuration incorrect" - ); - - // Other contracts should remain the same - assertEq( - address(output1.systemConfigImpl), - address(output2.systemConfigImpl), - "SystemConfig addresses should be the same" - ); - assertEq( - address(output1.disputeGameFactoryImpl), - address(output2.disputeGameFactoryImpl), - "DisputeGameFactory addresses should be the same" - ); - - // Running with same flags should produce same results - DeployImplementations.Output memory output3 = deployImplementations.run(inputEnabled); - assertEq( - address(output1.faultDisputeGameV2Impl), - address(output3.faultDisputeGameV2Impl), - "V2 enabled addresses should be deterministic" - ); - assertEq( - address(output1.permissionedDisputeGameV2Impl), - address(output3.permissionedDisputeGameV2Impl), - "V2 enabled addresses should be deterministic" - ); - } - function defaultInput() private view returns (DeployImplementations.Input memory input_) { input_ = DeployImplementations.Input( withdrawalDelaySeconds, diff --git a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol index 3246d9de9b4..53defc933f1 100644 --- a/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployOPChain.t.sol @@ -3,7 +3,6 @@ pragma solidity 0.8.15; import { Test } from "forge-std/Test.sol"; import { FeatureFlags } from "test/setup/FeatureFlags.sol"; -import { DevFeatures } from "src/libraries/DevFeatures.sol"; import { Features } from "src/libraries/Features.sol"; import { DeploySuperchain } from "scripts/deploy/DeploySuperchain.s.sol"; @@ -151,18 +150,10 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { Duration.unwrap(pdg.maxClockDuration()), Duration.unwrap(disputeMaxClockDuration), "PDG maxClockDuration" ); - if (isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - // For v2 contracts, some immutable args are passed in at game creation time from DGF.gameArgs - assertEq(address(pdg.proposer()), address(0), "PDG proposer"); - assertEq(address(pdg.challenger()), address(0), "PDG challenger"); - assertEq(Claim.unwrap(pdg.absolutePrestate()), bytes32(0), "PDG absolutePrestate"); - } else { - assertEq(address(pdg.proposer()), proposer, "PDG proposer"); - assertEq(address(pdg.challenger()), challenger, "PDG challenger"); - assertEq( - Claim.unwrap(pdg.absolutePrestate()), Claim.unwrap(disputeAbsolutePrestate), "PDG absolutePrestate" - ); - } + // For v2 contracts, some immutable args are passed in at game creation time from DGF.gameArgs + assertEq(address(pdg.proposer()), address(0), "PDG proposer"); + assertEq(address(pdg.challenger()), address(0), "PDG challenger"); + assertEq(Claim.unwrap(pdg.absolutePrestate()), bytes32(0), "PDG absolutePrestate"); // Custom gas token feature should reflect input assertEq(doo.systemConfigProxy.isCustomGasToken(), useCustomGasToken, "SystemConfig isCustomGasToken"); @@ -193,18 +184,15 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { // Check dispute game deployments // Validate permissionedDisputeGame (PDG) address - bool isDeployV2Games = isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES); IOPContractsManager.Implementations memory impls = opcm.implementations(); - address expectedPDGAddress = - isDeployV2Games ? impls.permissionedDisputeGameV2Impl : address(doo.permissionedDisputeGame); + address expectedPDGAddress = impls.permissionedDisputeGameV2Impl; address actualPDGAddress = address(doo.disputeGameFactoryProxy.gameImpls(GameTypes.PERMISSIONED_CANNON)); assertNotEq(actualPDGAddress, address(0), "PDG address should be non-zero"); assertEq(actualPDGAddress, expectedPDGAddress, "PDG address should match expected address"); // Check PDG getters IPermissionedDisputeGame pdg = IPermissionedDisputeGame(actualPDGAddress); - bytes32 expectedPrestate = - isDeployV2Games ? bytes32(0) : bytes32(0x038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c); + bytes32 expectedPrestate = bytes32(0); assertEq(pdg.l2BlockNumber(), 0, "3000"); assertEq(Claim.unwrap(pdg.absolutePrestate()), expectedPrestate, "3100"); assertEq(Duration.unwrap(pdg.clockExtension()), 10800, "3200"); @@ -237,18 +225,6 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { ); } - function test_customDisputeGame_customEnabled_succeeds() public { - // For v2 games, these parameters have already been configured at OPCM deploy time - skipIfDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES); - - deployOPChainInput.allowCustomDisputeParameters = true; - deployOPChainInput.disputeSplitDepth = disputeSplitDepth + 1; - DeployOPChain.Output memory doo = deployOPChain.run(deployOPChainInput); - - IPermissionedDisputeGame pdg = getPermissionedDisputeGame(doo); - assertEq(pdg.splitDepth(), disputeSplitDepth + 1); - } - function getPermissionedDisputeGame(DeployOPChain.Output memory doo) internal view diff --git a/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol b/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol index 38a50526439..8455c09462f 100644 --- a/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol +++ b/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol @@ -148,7 +148,6 @@ contract VerifyOPCM_Run_Test is VerifyOPCM_TestInit { // Check if V2 dispute games feature is enabled bytes32 bitmap = opcm.devFeatureBitmap(); - bool v2FeatureEnabled = DevFeatures.isDevFeatureEnabled(bitmap, DevFeatures.DEPLOY_V2_DISPUTE_GAMES); bool superGamesEnabled = DevFeatures.isDevFeatureEnabled(bitmap, DevFeatures.OPTIMISM_PORTAL_INTEROP); // Change 256 bytes at random. @@ -157,10 +156,6 @@ contract VerifyOPCM_Run_Test is VerifyOPCM_TestInit { uint256 randomImplIndex = vm.randomUint(0, refs.length - 1); VerifyOPCM.OpcmContractRef memory ref = refs[randomImplIndex]; - // Skip V2 dispute games when feature disabled - if (_isDisputeGameV2ContractRef(ref) && !v2FeatureEnabled) { - continue; - } // Skip super dispute games when feature disabled if (_isSuperDisputeGameContractRef(ref) && !superGamesEnabled) { continue; @@ -222,7 +217,6 @@ contract VerifyOPCM_Run_Test is VerifyOPCM_TestInit { // Check if V2 dispute games feature is enabled bytes32 bitmap = opcm.devFeatureBitmap(); - bool v2FeatureEnabled = DevFeatures.isDevFeatureEnabled(bitmap, DevFeatures.DEPLOY_V2_DISPUTE_GAMES); bool superGamesEnabled = DevFeatures.isDevFeatureEnabled(bitmap, DevFeatures.OPTIMISM_PORTAL_INTEROP); // Change 256 bytes at random. @@ -231,10 +225,6 @@ contract VerifyOPCM_Run_Test is VerifyOPCM_TestInit { uint256 randomImplIndex = vm.randomUint(0, refs.length - 1); VerifyOPCM.OpcmContractRef memory ref = refs[randomImplIndex]; - // Skip V2 dispute games when feature disabled - if (_isDisputeGameV2ContractRef(ref) && !v2FeatureEnabled) { - continue; - } // Skip super dispute games when feature disabled if (_isSuperDisputeGameContractRef(ref) && !superGamesEnabled) { continue; @@ -298,8 +288,7 @@ contract VerifyOPCM_Run_Test is VerifyOPCM_TestInit { address blueprint = ref.addr; bytes memory blueprintCode = blueprint.code; - // Skip the V2 dispute games blueprint when feature is enabled. - if (blueprintCode.length == 0 && isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { + if (blueprintCode.length == 0) { continue; } diff --git a/packages/contracts-bedrock/test/setup/DisputeGames.sol b/packages/contracts-bedrock/test/setup/DisputeGames.sol index d1cb32e54b2..cd389c9042c 100644 --- a/packages/contracts-bedrock/test/setup/DisputeGames.sol +++ b/packages/contracts-bedrock/test/setup/DisputeGames.sol @@ -10,13 +10,11 @@ import { console2 as console } from "forge-std/console2.sol"; // Libraries import { GameType, Claim } from "src/dispute/lib/LibUDT.sol"; import { GameTypes } from "src/dispute/lib/Types.sol"; -import { DevFeatures } from "src/libraries/DevFeatures.sol"; import { LibGameArgs } from "src/dispute/lib/LibGameArgs.sol"; // Interfaces import "../../interfaces/dispute/IDisputeGame.sol"; import "../../interfaces/dispute/IDisputeGameFactory.sol"; -import { IFaultDisputeGame } from "../../interfaces/dispute/IFaultDisputeGame.sol"; import { IPermissionedDisputeGame } from "../../interfaces/dispute/IPermissionedDisputeGame.sol"; contract DisputeGames is FeatureFlags { @@ -109,73 +107,38 @@ contract DisputeGames is FeatureFlags { } function mockGameImplPrestate(IDisputeGameFactory _dgf, GameType _gameType, bytes32 _prestate) internal { - if (isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - bytes memory value = abi.encodePacked(_prestate); - _mockGameArg(_dgf, _gameType, GameArg.PRESTATE, value); - } else { - address gameAddr = address(_dgf.gameImpls(_gameType)); - vm.mockCall(gameAddr, abi.encodeCall(IFaultDisputeGame.absolutePrestate, ()), abi.encode(_prestate)); - } + bytes memory value = abi.encodePacked(_prestate); + _mockGameArg(_dgf, _gameType, GameArg.PRESTATE, value); } function mockGameImplVM(IDisputeGameFactory _dgf, GameType _gameType, address _vm) internal { - if (isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - bytes memory value = abi.encodePacked(_vm); - _mockGameArg(_dgf, _gameType, GameArg.VM, value); - } else { - address gameAddr = address(_dgf.gameImpls(_gameType)); - vm.mockCall(gameAddr, abi.encodeCall(IFaultDisputeGame.vm, ()), abi.encode(_vm)); - } + bytes memory value = abi.encodePacked(_vm); + _mockGameArg(_dgf, _gameType, GameArg.VM, value); } function mockGameImplASR(IDisputeGameFactory _dgf, GameType _gameType, address _asr) internal { - if (isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - bytes memory value = abi.encodePacked(_asr); - _mockGameArg(_dgf, _gameType, GameArg.ASR, value); - } else { - address gameAddr = address(_dgf.gameImpls(_gameType)); - vm.mockCall(gameAddr, abi.encodeCall(IFaultDisputeGame.anchorStateRegistry, ()), abi.encode(_asr)); - } + bytes memory value = abi.encodePacked(_asr); + _mockGameArg(_dgf, _gameType, GameArg.ASR, value); } function mockGameImplWeth(IDisputeGameFactory _dgf, GameType _gameType, address _weth) internal { - if (isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - bytes memory value = abi.encodePacked(_weth); - _mockGameArg(_dgf, _gameType, GameArg.WETH, value); - } else { - address gameAddr = address(_dgf.gameImpls(_gameType)); - vm.mockCall(gameAddr, abi.encodeCall(IFaultDisputeGame.weth, ()), abi.encode(_weth)); - } + bytes memory value = abi.encodePacked(_weth); + _mockGameArg(_dgf, _gameType, GameArg.WETH, value); } function mockGameImplL2ChainId(IDisputeGameFactory _dgf, GameType _gameType, uint256 _chainId) internal { - if (isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - bytes memory value = abi.encodePacked(_chainId); - _mockGameArg(_dgf, _gameType, GameArg.L2_CHAIN_ID, value); - } else { - address gameAddr = address(_dgf.gameImpls(_gameType)); - vm.mockCall(gameAddr, abi.encodeCall(IFaultDisputeGame.l2ChainId, ()), abi.encode(_chainId)); - } + bytes memory value = abi.encodePacked(_chainId); + _mockGameArg(_dgf, _gameType, GameArg.L2_CHAIN_ID, value); } function mockGameImplProposer(IDisputeGameFactory _dgf, GameType _gameType, address _proposer) internal { - if (isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - bytes memory value = abi.encodePacked(_proposer); - _mockGameArg(_dgf, _gameType, GameArg.PROPOSER, value); - } else { - address gameAddr = address(_dgf.gameImpls(_gameType)); - vm.mockCall(gameAddr, abi.encodeCall(IPermissionedDisputeGame.proposer, ()), abi.encode(_proposer)); - } + bytes memory value = abi.encodePacked(_proposer); + _mockGameArg(_dgf, _gameType, GameArg.PROPOSER, value); } function mockGameImplChallenger(IDisputeGameFactory _dgf, GameType _gameType, address _challenger) internal { - if (isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - bytes memory value = abi.encodePacked(_challenger); - _mockGameArg(_dgf, _gameType, GameArg.CHALLENGER, value); - } else { - address gameAddr = address(_dgf.gameImpls(_gameType)); - vm.mockCall(gameAddr, abi.encodeCall(IPermissionedDisputeGame.challenger, ()), abi.encode(_challenger)); - } + bytes memory value = abi.encodePacked(_challenger); + _mockGameArg(_dgf, _gameType, GameArg.CHALLENGER, value); } function _getGameArgs( @@ -186,10 +149,6 @@ contract DisputeGames is FeatureFlags { view returns (bool gameArgsExist_, bytes memory gameArgs_) { - if (!isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - return (false, gameArgs_); - } - // Safe from issues with EIP150 since this is only used in the testing environment. // eip150-safe try _dgf.gameArgs(_gameType) returns (bytes memory gameArgsRet_) { diff --git a/packages/contracts-bedrock/test/setup/FeatureFlags.sol b/packages/contracts-bedrock/test/setup/FeatureFlags.sol index 10f2d20ffa6..015e8613801 100644 --- a/packages/contracts-bedrock/test/setup/FeatureFlags.sol +++ b/packages/contracts-bedrock/test/setup/FeatureFlags.sol @@ -36,22 +36,9 @@ abstract contract FeatureFlags { console.log("Setup: DEV_FEATURE__OPTIMISM_PORTAL_INTEROP is enabled"); devFeatureBitmap |= DevFeatures.OPTIMISM_PORTAL_INTEROP; } - if (Config.devFeatureCannonKona()) { - console.log("Setup: DEV_FEATURE__CANNON_KONA is enabled"); - devFeatureBitmap |= DevFeatures.CANNON_KONA; - } - if (Config.devFeatureDeployV2DisputeGames()) { - console.log("Setup: DEV_FEATURE__DEPLOY_V2_DISPUTE_GAMES is enabled"); - devFeatureBitmap |= DevFeatures.DEPLOY_V2_DISPUTE_GAMES; - } if (Config.devFeatureOpcmV2()) { - // WARNING: OPCMv2 also automatically implies DEPLOY_V2_DISPUTE_GAMES and CANNON_KONA. console.log("Setup: DEV_FEATURE__OPCM_V2 is enabled"); - console.log("Setup: DEV_FEATURE__DEPLOY_V2_DISPUTE_GAMES is enabled"); - console.log("Setup: DEV_FEATURE__CANNON_KONA is enabled"); devFeatureBitmap |= DevFeatures.OPCM_V2; - devFeatureBitmap |= DevFeatures.DEPLOY_V2_DISPUTE_GAMES; - devFeatureBitmap |= DevFeatures.CANNON_KONA; } } diff --git a/packages/contracts-bedrock/test/setup/ForkLive.s.sol b/packages/contracts-bedrock/test/setup/ForkLive.s.sol index 3c69c17d39b..d8c6f4a5270 100644 --- a/packages/contracts-bedrock/test/setup/ForkLive.s.sol +++ b/packages/contracts-bedrock/test/setup/ForkLive.s.sol @@ -14,16 +14,15 @@ import { Deploy } from "scripts/deploy/Deploy.s.sol"; import { Config } from "scripts/libraries/Config.sol"; // Libraries -import { DevFeatures } from "src/libraries/DevFeatures.sol"; import { GameTypes, Claim } from "src/dispute/lib/Types.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; +import { DevFeatures } from "src/libraries/DevFeatures.sol"; import { LibString } from "@solady/utils/LibString.sol"; import { LibGameArgs } from "src/dispute/lib/LibGameArgs.sol"; // Interfaces import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; -import { IPermissionedDisputeGame } from "interfaces/dispute/IPermissionedDisputeGame.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { IDelayedWETH } from "interfaces/dispute/IDelayedWETH.sol"; import { IAddressManager } from "interfaces/legacy/IAddressManager.sol"; @@ -298,7 +297,7 @@ contract ForkLive is Deployer, StdAssertions, DisputeGames { ) }); disputeGameConfigs[2] = IOPContractsManagerV2.DisputeGameConfig({ - enabled: isDevFeatureEnabled(DevFeatures.CANNON_KONA), + enabled: true, initBond: disputeGameFactory.initBonds(GameTypes.CANNON_KONA), gameType: GameTypes.CANNON_KONA, gameArgs: abi.encode( @@ -309,10 +308,15 @@ contract ForkLive is Deployer, StdAssertions, DisputeGames { }); // Add extra instructions to allow the DelayedWETH proxy to be deployed. + // TODO(#18502): Remove the extra instruction for custom gas token after U18 ships. IOPContractsManagerUtils.ExtraInstruction[] memory extraInstructions = - new IOPContractsManagerUtils.ExtraInstruction[](1); + new IOPContractsManagerUtils.ExtraInstruction[](2); extraInstructions[0] = IOPContractsManagerUtils.ExtraInstruction({ key: "PermittedProxyDeployment", data: bytes("DelayedWETH") }); + extraInstructions[1] = IOPContractsManagerUtils.ExtraInstruction({ + key: "overrides.cfg.useCustomGasToken", + data: abi.encode(false) + }); vm.prank(_delegateCaller, true); (bool upgradeSuccess,) = address(_opcm).delegatecall( @@ -364,14 +368,9 @@ contract ForkLive is Deployer, StdAssertions, DisputeGames { address permissionedDisputeGame = address(disputeGameFactory.gameImpls(GameTypes.PERMISSIONED_CANNON)); artifacts.save("PermissionedDisputeGame", permissionedDisputeGame); - IAnchorStateRegistry newAnchorStateRegistry; - if (isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - newAnchorStateRegistry = IAnchorStateRegistry( - LibGameArgs.decode(disputeGameFactory.gameArgs(GameTypes.PERMISSIONED_CANNON)).anchorStateRegistry - ); - } else { - newAnchorStateRegistry = IPermissionedDisputeGame(permissionedDisputeGame).anchorStateRegistry(); - } + IAnchorStateRegistry newAnchorStateRegistry = IAnchorStateRegistry( + LibGameArgs.decode(disputeGameFactory.gameArgs(GameTypes.PERMISSIONED_CANNON)).anchorStateRegistry + ); artifacts.save("AnchorStateRegistryProxy", address(newAnchorStateRegistry)); // Get the lockbox address from the portal, and save it @@ -380,14 +379,8 @@ contract ForkLive is Deployer, StdAssertions, DisputeGames { artifacts.save("ETHLockboxProxy", lockboxAddress); // Get the new DelayedWETH address and save it (might be a new proxy). - IDelayedWETH newDelayedWeth; - if (isDevFeatureEnabled(DevFeatures.DEPLOY_V2_DISPUTE_GAMES)) { - newDelayedWeth = IDelayedWETH( - payable(LibGameArgs.decode(disputeGameFactory.gameArgs(GameTypes.PERMISSIONED_CANNON)).weth) - ); - } else { - newDelayedWeth = IPermissionedDisputeGame(permissionedDisputeGame).weth(); - } + IDelayedWETH newDelayedWeth = + IDelayedWETH(payable(LibGameArgs.decode(disputeGameFactory.gameArgs(GameTypes.PERMISSIONED_CANNON)).weth)); artifacts.save("DelayedWETHProxy", address(newDelayedWeth)); artifacts.save("DelayedWETHImpl", EIP1967Helper.getImplementation(address(newDelayedWeth))); } diff --git a/rollup-boost b/rollup-boost new file mode 160000 index 00000000000..196237bab2a --- /dev/null +++ b/rollup-boost @@ -0,0 +1 @@ +Subproject commit 196237bab2a02298de994b439e0455abb1ac512f